feat: 第三方素材API配置和流水线任务素材传递

This commit is contained in:
Your Name 2026-01-06 23:45:06 +08:00
parent 72310e82b8
commit f7518f1265
11 changed files with 692 additions and 22 deletions

View File

@ -0,0 +1,22 @@
package com.designhub.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置类
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(10000); // 连接超时10秒
factory.setReadTimeout(30000); // 读取超时30秒
return new RestTemplate(factory);
}
}

View File

@ -0,0 +1,32 @@
package com.designhub.controller;
import com.designhub.common.Result;
import com.designhub.service.ExternalAssetService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 第三方素材API控制器
*/
@Tag(name = "第三方素材API", description = "第三方素材API相关接口")
@RestController
@RequestMapping("/api/external-asset")
@RequiredArgsConstructor
public class ExternalAssetController {
private final ExternalAssetService externalAssetService;
@Operation(summary = "测试API连接")
@PostMapping("/test")
public Result<Boolean> testConnection() {
boolean success = externalAssetService.testConnection();
if (success) {
return Result.success(true);
} else {
return Result.error("连接测试失败,请检查API地址和密钥");
}
}
}

View File

@ -0,0 +1,55 @@
package com.designhub.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 第三方素材API响应DTO
*/
@Data
public class ExternalAssetDTO {
private boolean success;
private AssetData data;
private ErrorInfo error;
@Data
public static class AssetData {
private String taskNo;
private String productModel;
private String categoryName;
private String sceneName;
private String status;
private String createdAt;
private String updatedAt;
private Map<String, List<AssetItem>> assets;
private DownloadUrls downloadUrls;
}
@Data
public static class AssetItem {
private String id;
private String url;
private String downloadUrl;
private String fileName;
private Long fileSize;
private Integer sortOrder;
}
@Data
public static class DownloadUrls {
private String all;
private String main;
private String sku;
private String detail;
private String psd;
}
@Data
public static class ErrorInfo {
private String code;
private String message;
}
}

View File

@ -96,6 +96,16 @@ public class TaskItem implements Serializable {
*/
private String deliveryPath;
/**
* 素材下载链接(从第三方API获取)
*/
private String downloadUrl;
/**
* 前置子任务的素材下载链接(流水线任务中,前一环节完成后传递)
*/
private String prevDownloadUrl;
/**
* 提交时间
*/

View File

@ -0,0 +1,163 @@
package com.designhub.service;
import com.designhub.dto.ExternalAssetDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* 第三方素材API服务
* 用于查询外部素材系统的任务完成状态和下载链接
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ExternalAssetService {
private final SystemConfigService systemConfigService;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
/**
* 查询任务素材
* @param taskNo 任务编号
* @return 素材信息,包含下载链接
*/
public ExternalAssetDTO queryAssets(String taskNo) {
String apiUrl = systemConfigService.getConfigValue("external_asset_api_url");
String apiKey = systemConfigService.getConfigValue("external_asset_api_key");
if (apiUrl == null || apiUrl.isEmpty()) {
log.warn("第三方素材API地址未配置");
ExternalAssetDTO result = new ExternalAssetDTO();
result.setSuccess(false);
ExternalAssetDTO.ErrorInfo error = new ExternalAssetDTO.ErrorInfo();
error.setCode("CONFIG_ERROR");
error.setMessage("第三方素材API未配置");
result.setError(error);
return result;
}
if (apiKey == null || apiKey.isEmpty()) {
log.warn("第三方素材API Key未配置");
ExternalAssetDTO result = new ExternalAssetDTO();
result.setSuccess(false);
ExternalAssetDTO.ErrorInfo error = new ExternalAssetDTO.ErrorInfo();
error.setCode("CONFIG_ERROR");
error.setMessage("第三方素材API Key未配置");
result.setError(error);
return result;
}
try {
// 构建请求URL
String url = apiUrl.endsWith("/") ? apiUrl : apiUrl + "/";
url = url + "product-assets?taskNo=" + taskNo;
log.info("查询第三方素材API: {}", url);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("X-API-Key", apiKey);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(headers);
// 发送请求
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
log.info("第三方素材API响应状态: {}", response.getStatusCode());
log.debug("第三方素材API响应内容: {}", response.getBody());
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return objectMapper.readValue(response.getBody(), ExternalAssetDTO.class);
} else {
ExternalAssetDTO result = new ExternalAssetDTO();
result.setSuccess(false);
ExternalAssetDTO.ErrorInfo error = new ExternalAssetDTO.ErrorInfo();
error.setCode("REQUEST_FAILED");
error.setMessage("请求失败: " + response.getStatusCode());
result.setError(error);
return result;
}
} catch (Exception e) {
log.error("调用第三方素材API失败", e);
ExternalAssetDTO result = new ExternalAssetDTO();
result.setSuccess(false);
ExternalAssetDTO.ErrorInfo error = new ExternalAssetDTO.ErrorInfo();
error.setCode("EXCEPTION");
error.setMessage("调用API失败: " + e.getMessage());
result.setError(error);
return result;
}
}
/**
* 检查任务素材是否完成
* @param taskNo 任务编号
* @return 下载链接(如果完成),否则返回null
*/
public String checkTaskCompletion(String taskNo) {
ExternalAssetDTO result = queryAssets(taskNo);
if (result.isSuccess() && result.getData() != null) {
ExternalAssetDTO.DownloadUrls urls = result.getData().getDownloadUrls();
if (urls != null && urls.getAll() != null && !urls.getAll().isEmpty()) {
log.info("任务 {} 素材已完成,下载链接: {}", taskNo, urls.getAll());
return urls.getAll();
}
}
log.info("任务 {} 素材未完成或查询失败", taskNo);
return null;
}
/**
* 测试API连接
* @return 测试结果
*/
public boolean testConnection() {
String apiUrl = systemConfigService.getConfigValue("external_asset_api_url");
String apiKey = systemConfigService.getConfigValue("external_asset_api_key");
if (apiUrl == null || apiUrl.isEmpty() || apiKey == null || apiKey.isEmpty()) {
return false;
}
try {
// 测试连接(使用一个测试任务号)
String url = apiUrl.endsWith("/") ? apiUrl : apiUrl + "/";
url = url + "product-assets?taskNo=TEST000000000000";
HttpHeaders headers = new HttpHeaders();
headers.set("X-API-Key", apiKey);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
// 只要能收到响应(无论是404还是200),说明连接正常
return response.getStatusCode().is2xxSuccessful() ||
response.getStatusCode() == HttpStatus.NOT_FOUND;
} catch (Exception e) {
log.error("测试第三方素材API连接失败", e);
return false;
}
}
}

View File

@ -35,6 +35,8 @@ public class TaskItemService {
private final NotificationService notificationService;
private final DingTalkService dingTalkService;
private final ConfigService configService;
private final ExternalAssetService externalAssetService;
private final SystemConfigService systemConfigService;
/**
* 获取可接单的子任务列表
@ -262,6 +264,9 @@ public class TaskItemService {
/**
* 完结子任务(关键方法:包含自动解锁逻辑)
* @param itemId 子任务ID
* @param userId 用户ID
* @param deliveryPath 交付路径(可选,如果启用第三方API则自动获取)
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> completeItem(Long itemId, Long userId, String deliveryPath) {
@ -283,10 +288,40 @@ public class TaskItemService {
throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "只能完结进行中的子任务");
}
// 4. 更新子任务状态
// 4. 检查是否启用第三方素材API验证
String downloadUrl = null;
boolean externalApiEnabled = "true".equalsIgnoreCase(
systemConfigService.getConfigValue("external_asset_api_enabled", "false"));
if (externalApiEnabled) {
// 调用第三方素材API验证任务完成状态
String taskNo = item.getItemNo(); // 使用子任务编号作为任务号
try {
downloadUrl = externalAssetService.checkTaskCompletion(taskNo);
} catch (Exception e) {
log.warn("调用第三方素材API失败", e);
}
if (downloadUrl == null) {
// 如果第三方API未返回下载链接,提示用户去上传素材
throw new BusinessException(ResultCode.ERROR,
"素材未提交,请先前往 3D.YYDD.AI 提交任务素材后再完结任务");
}
log.info("第三方素材API返回下载链接: {}", downloadUrl);
} else {
// 未启用第三方API,使用传统方式(需要传入deliveryPath)
if (deliveryPath == null || deliveryPath.trim().isEmpty()) {
throw new BusinessException(ResultCode.ERROR, "交付路径不能为空");
}
}
// 5. 更新子任务状态
item.setStatus("completed");
item.setProgress(100);
item.setDeliveryPath(deliveryPath);
item.setDeliveryPath(downloadUrl != null ? downloadUrl : deliveryPath);
item.setDownloadUrl(downloadUrl);
item.setCompleteTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
taskItemMapper.updateById(item);
@ -321,6 +356,26 @@ public class TaskItemService {
if (allGroupCompleted) {
log.info("✅ 第 {} 组所有子任务已完成,准备解锁下一组", currentGroupNo);
// 🔥 收集当前组所有子任务的下载链接,传递给下一组
StringBuilder prevDownloadUrls = new StringBuilder();
for (TaskItem groupItem : currentGroupItems) {
if (groupItem.getDownloadUrl() != null && !groupItem.getDownloadUrl().isEmpty()) {
if (prevDownloadUrls.length() > 0) {
prevDownloadUrls.append(",");
}
prevDownloadUrls.append(groupItem.getDownloadUrl());
}
// 同时继承前面环节的下载链接
if (groupItem.getPrevDownloadUrl() != null && !groupItem.getPrevDownloadUrl().isEmpty()) {
if (prevDownloadUrls.length() > 0) {
prevDownloadUrls.append(",");
}
prevDownloadUrls.append(groupItem.getPrevDownloadUrl());
}
}
String prevDownloadUrlStr = prevDownloadUrls.toString();
log.info("前置环节素材下载链接: {}", prevDownloadUrlStr);
// 解锁下一组的所有子任务
Integer nextGroupNo = currentGroupNo + 1;
List<TaskItem> nextGroupItems = taskItemMapper.selectList(
@ -331,14 +386,15 @@ public class TaskItemService {
);
if (!nextGroupItems.isEmpty()) {
// 批量解锁下一组的所有子任务,状态设为待审核
// 批量解锁下一组的所有子任务,状态设为待审核,并传递前置环节的下载链接
for (TaskItem nextItem : nextGroupItems) {
nextItem.setStatus("pending_approval"); // 待接单部门负责人审核
nextItem.setIsLocked(0);
nextItem.setPrevDownloadUrl(prevDownloadUrlStr); // 🔥 传递前置环节的下载链接
nextItem.setUpdateTime(LocalDateTime.now());
taskItemMapper.updateById(nextItem);
log.info("✅ 解锁子任务: {} (组号: {}, ID: {}),待接单部门负责人审核",
log.info("✅ 解锁子任务: {} (组号: {}, ID: {}),待接单部门负责人审核,已传递前置素材链接",
nextItem.getItemNo(), nextGroupNo, nextItem.getId());
}
@ -380,10 +436,40 @@ public class TaskItemService {
int totalCount = taskItemMapper.countTotalItems(item.getTaskId());
if (completedCount == totalCount) {
// 🔥 获取最后一组子任务的下载链接,保存到主任务
Integer maxGroupNo = taskItemMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, item.getTaskId())
.orderByDesc(TaskItem::getGroupNo)
.last("LIMIT 1")
).stream().findFirst().map(TaskItem::getGroupNo).orElse(1);
List<TaskItem> lastGroupItems = taskItemMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getTaskId, item.getTaskId())
.eq(TaskItem::getGroupNo, maxGroupNo)
);
// 收集最后一组的下载链接
StringBuilder finalDownloadUrl = new StringBuilder();
for (TaskItem lastItem : lastGroupItems) {
if (lastItem.getDownloadUrl() != null && !lastItem.getDownloadUrl().isEmpty()) {
if (finalDownloadUrl.length() > 0) {
finalDownloadUrl.append(",");
}
finalDownloadUrl.append(lastItem.getDownloadUrl());
}
}
// 所有子任务完成,更新主任务状态
mainTask.setStatus("completed");
mainTask.setCompleteTime(LocalDateTime.now());
mainTask.setUpdateTime(LocalDateTime.now());
// 🔥 保存最后一组的下载链接到主任务
if (finalDownloadUrl.length() > 0) {
mainTask.setDeliveryPath(finalDownloadUrl.toString());
log.info("主任务最终素材下载链接: {}", finalDownloadUrl);
}
taskMapper.updateById(mainTask);
log.info("🎉 主任务 {} 所有子任务已完成,主任务标记为完成", mainTask.getId());

View File

@ -0,0 +1,15 @@
-- 添加第三方素材API相关配置
-- V9: 第三方素材API配置
-- 在task_item表添加下载链接字段
ALTER TABLE task_item ADD COLUMN IF NOT EXISTS download_url VARCHAR(1000) COMMENT '素材下载链接(从第三方API获取)';
ALTER TABLE task_item ADD COLUMN IF NOT EXISTS prev_download_url VARCHAR(2000) COMMENT '前置子任务的素材下载链接(流水线任务中传递)';
-- 添加系统配置:第三方素材API
INSERT INTO sys_config (config_key, config_value, config_group, config_name, description, value_type, built_in, sort_order, create_time, update_time, deleted)
VALUES
('external_asset_api_url', '', 'external_api', '第三方素材API地址', '第三方素材系统的API地址,例如:https://your-domain.com/api/external/', 'string', 1, 1, NOW(), NOW(), 0),
('external_asset_api_key', '', 'external_api', '第三方素材API密钥', '用于调用第三方素材API的密钥', 'string', 1, 2, NOW(), NOW(), 0),
('external_asset_api_enabled', 'false', 'external_api', '启用第三方素材API', '是否启用第三方素材API验证任务完结', 'boolean', 1, 3, NOW(), NOW(), 0)
ON DUPLICATE KEY UPDATE update_time = NOW();

View File

@ -70,6 +70,8 @@ export interface Task {
allDeliveryPaths?: string[]
// 预览图
previewImage?: string
// 素材下载链接
downloadUrl?: string
}
// 任务子项(流水线任务)
@ -107,6 +109,9 @@ export interface TaskItem {
spuInfo?: string
taskSubType?: string
attachmentUrl?: string
previewImage?: string
downloadUrl?: string
prevDownloadUrl?: string // 前置子任务的素材下载链接
}
// 创建流水线任务请求

View File

@ -1188,6 +1188,73 @@
</div>
</a-tab-pane>
<!-- 第三方素材API配置 -->
<a-tab-pane key="external-asset-api" tab="素材API配置">
<div class="settings-section">
<div class="section-header">
<h2 class="section-title">第三方素材API配置</h2>
</div>
<a-alert
message="使用说明"
description="配置第三方素材API后,设计师完结任务时系统会自动调用API验证素材是否已提交。如果素材未提交,系统会提示设计师前往素材平台提交。完结成功后,需求方可以直接下载素材。"
type="info"
show-icon
style="margin-bottom: 24px;"
/>
<a-form :model="externalAssetApiConfig" layout="vertical" class="external-api-form">
<a-form-item label="启用第三方素材API">
<a-switch
v-model:checked="externalAssetApiConfig.enabled"
checked-children="已启用"
un-checked-children="未启用"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
启用后,设计师完结任务时将验证素材是否已在第三方平台提交
</div>
</a-form-item>
<a-form-item label="API地址" :required="externalAssetApiConfig.enabled">
<a-input
v-model:value="externalAssetApiConfig.apiUrl"
placeholder="https://your-domain.com/api/external/"
size="large"
:disabled="!externalAssetApiConfig.enabled"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
第三方素材系统的API地址,需以/结尾
</div>
</a-form-item>
<a-form-item label="API密钥" :required="externalAssetApiConfig.enabled">
<a-input-password
v-model:value="externalAssetApiConfig.apiKey"
placeholder="请输入API密钥"
size="large"
:disabled="!externalAssetApiConfig.enabled"
/>
<div style="margin-top: 8px; font-size: 12px; color: #888;">
用于调用第三方素材API的密钥(X-API-Key)
</div>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" size="large" @click="handleSaveExternalAssetApiConfig" :loading="savingExternalAssetApi">
<template #icon><Icon name="check" size="sm" /></template>
保存配置
</a-button>
<a-button size="large" @click="handleTestExternalAssetApi" :loading="testingExternalAssetApi" :disabled="!externalAssetApiConfig.apiUrl || !externalAssetApiConfig.apiKey">
<template #icon><Icon name="refresh" size="sm" /></template>
测试连接
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
</a-tab-pane>
<!-- 第三方数据库配置 -->
<a-tab-pane key="third-party-db" tab="第三方数据库">
<div class="settings-section">
@ -3506,6 +3573,98 @@ const delayPenaltyConfig = ref({
const savingDelayPenaltyConfig = ref(false)
// ==================== 第三方素材API配置 ====================
const externalAssetApiConfig = reactive({
enabled: false,
apiUrl: '',
apiKey: ''
})
const savingExternalAssetApi = ref(false)
const testingExternalAssetApi = ref(false)
// 加载第三方素材API配置
const loadExternalAssetApiConfig = async () => {
try {
const response = await getSystemConfigsByGroup('external_api')
const configs = response.data || []
const urlConfig = configs.find((c: any) => c.configKey === 'external_asset_api_url')
const keyConfig = configs.find((c: any) => c.configKey === 'external_asset_api_key')
const enabledConfig = configs.find((c: any) => c.configKey === 'external_asset_api_enabled')
externalAssetApiConfig.apiUrl = urlConfig?.configValue || ''
externalAssetApiConfig.apiKey = keyConfig?.configValue || ''
externalAssetApiConfig.enabled = enabledConfig?.configValue === 'true'
} catch (error) {
console.error('加载第三方素材API配置失败:', error)
}
}
// 保存第三方素材API配置
const handleSaveExternalAssetApiConfig = async () => {
if (externalAssetApiConfig.enabled) {
if (!externalAssetApiConfig.apiUrl) {
message.error('请输入API地址')
return
}
if (!externalAssetApiConfig.apiKey) {
message.error('请输入API密钥')
return
}
}
savingExternalAssetApi.value = true
try {
await batchUpdateSystemConfigs({
'external_asset_api_url': externalAssetApiConfig.apiUrl,
'external_asset_api_key': externalAssetApiConfig.apiKey,
'external_asset_api_enabled': externalAssetApiConfig.enabled.toString()
})
message.success('第三方素材API配置保存成功')
} catch (error: any) {
message.error(error.message || '保存失败')
} finally {
savingExternalAssetApi.value = false
}
}
// 测试第三方素材API连接
const handleTestExternalAssetApi = async () => {
if (!externalAssetApiConfig.apiUrl || !externalAssetApiConfig.apiKey) {
message.error('请先填写API地址和密钥')
return
}
testingExternalAssetApi.value = true
try {
// 先保存配置
await batchUpdateSystemConfigs({
'external_asset_api_url': externalAssetApiConfig.apiUrl,
'external_asset_api_key': externalAssetApiConfig.apiKey
})
// 调用测试接口
const response = await fetch('/api/external-asset/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
const result = await response.json()
if (result.code === 200 && result.data === true) {
message.success('连接测试成功')
} else {
message.error(result.message || '连接测试失败')
}
} catch (error: any) {
message.error(error.message || '连接测试失败')
} finally {
testingExternalAssetApi.value = false
}
}
// 加载延误扣分配置
const loadDelayPenaltyConfig = async () => {
try {
@ -3771,6 +3930,8 @@ onMounted(() => {
loadTaskReleaseConfig()
// 加载延误扣分配置
loadDelayPenaltyConfig()
// 加载第三方素材API配置
loadExternalAssetApiConfig()
})
// S3配置相关方法

View File

@ -945,6 +945,17 @@
<!-- 操作按钮 -->
<div class="task-actions">
<a-button size="small" @click="handleViewTaskDetail(task)">查看详情</a-button>
<!-- 上一环节素材下载(流水线任务) -->
<a-button
v-if="task.prevDownloadUrl"
type="default"
size="small"
@click.stop="handleDownloadPrevAssets(task)"
class="prev-assets-btn"
>
<Icon name="download" size="sm" style="margin-right: 4px;" />
上一环节素材
</a-button>
<a-button v-if="canUpdateProgress(task)" type="primary" size="small" @click="handleUpdateProgress(task)">
更新进度
</a-button>
@ -1636,6 +1647,28 @@
</div>
</div>
<!-- 下载素材按钮(已完成任务) -->
<div v-if="task.status === 'completed'" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
<a-button
v-if="task.downloadUrl || task.deliveryPath"
type="primary"
size="small"
@click.stop="handleDownloadAssets(task)"
style="width: 100%;">
<Icon name="download" size="sm" style="margin-right: 4px;" />
下载素材
</a-button>
<a-button
v-else
type="default"
size="small"
disabled
style="width: 100%; opacity: 0.5;">
<Icon name="download" size="sm" style="margin-right: 4px;" />
暂无素材
</a-button>
</div>
<!-- 被驳回或草稿任务的操作按钮 -->
<div v-if="['rejected', 'draft'].includes(task.status) || task.approvalStatus?.includes('rejected')" class="task-actions" style="margin-top: 16px; padding-top: 16px; border-top: 1px solid rgba(49, 240, 139, 0.2);">
<a-button
@ -2311,11 +2344,13 @@ import dayjs from 'dayjs'
interface TaskOrSubTask extends Task {
isSubTask?: boolean // 是否为子任务
itemId?: number // 子任务ID
itemNo?: string // 子任务编号
subTaskNo?: string // 子任务编号
groupNo?: number // 子任务组号
taskDescription?: string // 主任务描述
submitTime?: string // 提交时间
taskTypeName?: string // 任务类型名称(子任务直接返回)
prevDownloadUrl?: string // 前置子任务的素材下载链接
}
const router = useRouter()
@ -3686,6 +3721,67 @@ const handleUpdateProgress = (task: TaskOrSubTask) => {
// 完结任务(支持普通任务和子任务)
// 编辑被驳回的任务
// 下载素材
const handleDownloadAssets = (task: Task) => {
const downloadUrl = task.downloadUrl || task.deliveryPath
if (downloadUrl) {
// 如果是http链接,直接打开
if (downloadUrl.startsWith('http')) {
window.open(downloadUrl, '_blank')
} else {
// 复制到剪贴板
navigator.clipboard.writeText(downloadUrl).then(() => {
message.success('素材路径已复制到剪贴板')
}).catch(() => {
message.info(`素材路径: ${downloadUrl}`)
})
}
} else {
message.warning('暂无可下载的素材')
}
}
// 下载上一环节素材(流水线任务)
const handleDownloadPrevAssets = (task: TaskOrSubTask) => {
const prevDownloadUrl = task.prevDownloadUrl
if (prevDownloadUrl) {
// 如果包含多个链接(逗号分隔),显示弹窗让用户选择
const urls = prevDownloadUrl.split(',').filter(url => url.trim())
if (urls.length === 1) {
// 只有一个链接,直接打开
if (urls[0].startsWith('http')) {
window.open(urls[0], '_blank')
} else {
navigator.clipboard.writeText(urls[0]).then(() => {
message.success('素材路径已复制到剪贴板')
}).catch(() => {
message.info(`素材路径: ${urls[0]}`)
})
}
} else {
// 多个链接,显示列表让用户选择
Modal.info({
title: '上一环节素材下载',
content: h('div', [
h('p', { style: 'margin-bottom: 12px;' }, `共 ${urls.length} 个素材文件:`),
...urls.map((url, index) =>
h('div', { style: 'margin-bottom: 8px;' }, [
h('a', {
href: url,
target: '_blank',
style: 'color: #1890ff; word-break: break-all;'
}, `素材 ${index + 1}: ${url.length > 50 ? url.substring(0, 50) + '...' : url}`)
])
)
]),
width: 600
})
}
} else {
message.warning('暂无上一环节的素材')
}
}
const handleEditRejectedTask = (task: Task) => {
// 跳转到创建任务页面,并传入任务ID作为编辑标识
router.push({
@ -3698,43 +3794,57 @@ const handleEditRejectedTask = (task: Task) => {
}
const handleCompleteTask = (task: TaskOrSubTask) => {
let deliveryPath = ''
const isSubTask = task.isSubTask
const taskType = isSubTask ? '子任务' : '任务'
const taskNo = isSubTask ? (task.itemNo || task.taskNo) : task.taskNo
Modal.confirm({
title: `完结${taskType}`,
content: h('div', [
h('div', { style: 'margin-bottom: 12px;' }, `确认完结${taskType}:${task.title}`),
h('div', { style: 'margin-bottom: 12px;' }, '交付路径:'),
h(Textarea, {
rows: 4,
placeholder: '请输入交付路径或说明',
style: 'width: 100%',
onChange: (e: any) => { deliveryPath = e.target.value }
})
h('div', { style: 'color: #666; font-size: 13px;' }, [
h('p', { style: 'margin-bottom: 8px;' }, '系统将自动从素材平台获取您提交的素材。'),
h('p', { style: 'margin: 0;' }, `任务编号:${taskNo || '--'}`)
])
]),
okText: '确认完结',
cancelText: '取消',
onOk: async () => {
if (!deliveryPath.trim()) {
message.error('请输入交付路径')
return Promise.reject()
}
try {
if (isSubTask) {
// 完结子任务
await taskApi.completeTaskItem(task.itemId || task.id!, { deliveryPath })
message.success('子任务已完成,等待下一个子任务解锁')
// 完结子任务(后端会自动调用第三方API验证)
await taskApi.completeTaskItem(task.itemId || task.id!, { deliveryPath: '' })
message.success('子任务已完成')
} else {
// 完结普通任务
await taskApi.completeTask(task.id!, { deliveryPath })
await taskApi.completeTask(task.id!, { deliveryPath: '' })
message.success('任务已提交审核')
}
loadInProgressTasks()
loadAllTabCounts()
loadDashboardStats()
} catch (error: any) {
message.error(error.message || '任务完结失败')
// 如果是素材未提交的错误,显示特定提示
const errorMsg = error.message || error.msg || '任务完结失败'
if (errorMsg.includes('素材未提交') || errorMsg.includes('3D.YYDD.AI')) {
Modal.warning({
title: '素材未提交',
content: h('div', [
h('p', '请先前往素材平台提交任务素材后再完结任务。'),
h('p', { style: 'margin-top: 8px;' }, [
'任务编号:',
h('strong', taskNo || '--')
]),
h('a', {
href: 'https://3d.yydd.ai',
target: '_blank',
style: 'color: #1890ff; margin-top: 12px; display: inline-block;'
}, '前往素材平台 →')
])
})
} else {
message.error(errorMsg)
}
}
}
})
@ -5124,6 +5234,17 @@ onUnmounted(() => {
gap: $space-sm;
padding-top: $space-md;
border-top: 1px solid $border-primary;
flex-wrap: wrap;
.prev-assets-btn {
background: rgba(24, 144, 255, 0.1);
border-color: #1890ff;
color: #1890ff;
&:hover {
background: rgba(24, 144, 255, 0.2);
}
}
margin-top: $space-md;
button {