feat: 添加执行列表功能以支持工作流执行管理
- 新增执行列表相关接口和数据结构,支持获取执行流和执行项 - 更新创作页面以展示执行流和预览功能,提升用户交互体验 - 优化树形结构展示,确保根据执行流动态生成节点 - 引入文件上传功能,支持用户上传文件并获取文件URL
This commit is contained in:
@@ -67,6 +67,23 @@ export interface CreationTreeItem {
|
||||
contentTypes: CreationContentTypeItem[];
|
||||
}
|
||||
|
||||
// 新的执行列表数据结构
|
||||
export interface ExecutionItem {
|
||||
timestamp: string;
|
||||
content: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ExecutionFlowItem {
|
||||
flowName: string;
|
||||
items: ExecutionItem[];
|
||||
}
|
||||
|
||||
export interface ExecutionTreeItem {
|
||||
createDate: string;
|
||||
flows: ExecutionFlowItem[];
|
||||
}
|
||||
|
||||
export interface CreationListData {
|
||||
list: unknown[] | null;
|
||||
total: number;
|
||||
@@ -74,12 +91,23 @@ export interface CreationListData {
|
||||
imgAddressPrefix: string;
|
||||
}
|
||||
|
||||
export interface ExecutionListData {
|
||||
tree: ExecutionTreeItem[];
|
||||
imgAddressPrefix: string;
|
||||
}
|
||||
|
||||
export interface CreationListResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: CreationListData;
|
||||
}
|
||||
|
||||
export interface ExecutionListResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: ExecutionListData;
|
||||
}
|
||||
|
||||
export interface CreationSubmitParams {
|
||||
mode: string;
|
||||
content_type: string;
|
||||
@@ -105,6 +133,14 @@ export function getCreationList(params: CreationListParams, requestOptions?: Req
|
||||
}) as Promise<CreationListResponse>;
|
||||
}
|
||||
|
||||
export function getExecutionList(requestOptions?: RequestOptions) {
|
||||
return request({
|
||||
url: '/ai-agent/flow/execution/list',
|
||||
method: 'get',
|
||||
requestOptions,
|
||||
}) as Promise<ExecutionListResponse>;
|
||||
}
|
||||
|
||||
export function getNodeLibraryList(requestOptions?: RequestOptions) {
|
||||
return request({
|
||||
url: '/ai-agent/node/library/list',
|
||||
@@ -260,7 +296,8 @@ export interface ExecuteFlowParams {
|
||||
desc?: string;
|
||||
fileUrl?: string[];
|
||||
flowContent?: FlowInfo;
|
||||
flowId?: number;
|
||||
flowId?: string;
|
||||
flowName?: string;
|
||||
nodeInputParams?: FlowNode[];
|
||||
sessionId?: string;
|
||||
skillName?: string;
|
||||
|
||||
@@ -15,19 +15,28 @@
|
||||
default-expand-all
|
||||
:highlight-current="true"
|
||||
:expand-on-click-node="false"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="tree-node">
|
||||
<span class="ellipsis">{{ data.label }}</span>
|
||||
<el-button
|
||||
v-if="data.nodeType === 'html' || data.nodeType === 'image'"
|
||||
type="primary"
|
||||
link
|
||||
class="tree-download"
|
||||
@click.stop="downloadNode(data)"
|
||||
>下载</el-button
|
||||
>
|
||||
<div v-if="data.nodeType === 'html' || data.nodeType === 'image'" class="tree-node-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="previewNode(data)"
|
||||
>
|
||||
预览
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="downloadNode(data)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
@@ -249,7 +258,7 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 其他配置字段(使用 fieldMetadata 中的中文名称) -->
|
||||
<!-- 其他配置字段(排除已在 formConfig 中的字段) -->
|
||||
<template v-if="node.config">
|
||||
<el-form-item
|
||||
v-for="(value, key) in node.config"
|
||||
@@ -257,7 +266,7 @@
|
||||
v-show="
|
||||
!['nodeCode', 'width', 'height', 'x', 'y', 'formConfig', 'inputSource', 'fieldMetadata', 'selectedModel'].includes(
|
||||
String(key)
|
||||
)
|
||||
) && !(node.formConfig || []).some((f: any) => f.label === key || f.label === node.config.fieldMetadata?.[key]?.label)
|
||||
"
|
||||
:label="node.config.fieldMetadata?.[key]?.label || String(key)"
|
||||
:required="node.config.fieldMetadata?.[key]?.required || false"
|
||||
@@ -482,6 +491,14 @@
|
||||
|
||||
<!-- 创作技能选择器 -->
|
||||
<SkillSelector v-model="showCreationSkillSelector" :default-skill="selectedCreationSkill" @confirm="handleCreationSkillConfirm" />
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
<el-dialog v-model="previewDialogVisible" title="预览" width="90%" :close-on-click-modal="false" destroy-on-close>
|
||||
<div class="preview-container">
|
||||
<iframe v-if="previewUrl" :src="previewUrl" class="preview-iframe" frameborder="0"></iframe>
|
||||
<el-empty v-else description="无法加载预览内容" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -497,7 +514,7 @@ import SkillSelector from '/@/components/skill/NodeSkillSelector.vue';
|
||||
import type { SkillItem } from '/@/api/digitalHuman/skill';
|
||||
import {
|
||||
downloadToFile,
|
||||
getCreationList,
|
||||
getExecutionList,
|
||||
getNodeLibraryList,
|
||||
getWorkflowList,
|
||||
getWorkflowDetail,
|
||||
@@ -505,13 +522,13 @@ import {
|
||||
deleteWorkflow,
|
||||
saveWorkflow,
|
||||
executeFlow,
|
||||
type CreationListParams,
|
||||
type CreationTreeItem,
|
||||
type ExecutionTreeItem,
|
||||
type NodeLibraryFormItem,
|
||||
type NodeLibraryGroup,
|
||||
type WorkflowItem,
|
||||
type ExecuteFlowParams,
|
||||
} from '/@/api/digitalHuman/creation';
|
||||
import { uploadFile } from '/@/api/common/upload';
|
||||
|
||||
type NodeType = 'date' | 'contentType' | 'theme' | 'title' | 'html' | 'image';
|
||||
type Item = Record<string, any>;
|
||||
@@ -573,6 +590,9 @@ const selectedFiles = ref<File[]>([]);
|
||||
const selectedCreationSkill = ref<SkillItem | null>(null);
|
||||
const showCreationSkillSelector = ref(false);
|
||||
const isCreating = ref(false);
|
||||
// 预览相关状态
|
||||
const previewDialogVisible = ref(false);
|
||||
const previewUrl = ref('');
|
||||
// 会话ID管理(存储在 sessionStorage 中)
|
||||
const getSessionId = () => {
|
||||
let sessionId = sessionStorage.getItem('ai_creation_session_id');
|
||||
@@ -718,7 +738,6 @@ const availableParentParams = computed(() => {
|
||||
return params;
|
||||
});
|
||||
const treeProps = { children: 'children', label: 'label' };
|
||||
const queryParams: CreationListParams = { keyword: '', pageNum: 1, pageSize: 10 };
|
||||
const apiBaseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
|
||||
const nodeLibraryGroups = ref<NodeLibraryGroup[]>([]);
|
||||
const workflowDsl = computed(() => ({
|
||||
@@ -761,44 +780,36 @@ const buildAssetUrl = (p?: string) =>
|
||||
: imgAddressPrefix.value
|
||||
? joinUrl(joinUrl(apiBaseUrl, imgAddressPrefix.value), p)
|
||||
: joinUrl(apiBaseUrl, p);
|
||||
const buildTreeNodes = (tree: CreationTreeItem[]): TreeNode[] =>
|
||||
const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
|
||||
tree.map((d, di) => ({
|
||||
id: `date-${di}`,
|
||||
label: d.createdDate,
|
||||
label: d.createDate,
|
||||
nodeType: 'date',
|
||||
children: (d.contentTypes || []).map((c, ci) => ({
|
||||
id: `content-${di}-${ci}`,
|
||||
label: c.contentType,
|
||||
children: (d.flows || []).map((f, fi) => ({
|
||||
id: `flow-${di}-${fi}`,
|
||||
label: f.flowName || '未命名工作流',
|
||||
nodeType: 'contentType',
|
||||
children: (c.themes || []).map((t, ti) => ({
|
||||
id: `theme-${di}-${ci}-${ti}`,
|
||||
label: t.theme,
|
||||
nodeType: 'theme',
|
||||
children: (t.titles || []).map((title, i) => ({
|
||||
id: `title-${di}-${ci}-${ti}-${i}`,
|
||||
label: title.title || `作品${i + 1}`,
|
||||
nodeType: 'title',
|
||||
children: [
|
||||
...(title.htmlFileUrl
|
||||
? [{ id: `html-${di}-${ci}-${ti}-${i}`, label: 'HTML', nodeType: 'html' as const, fileUrl: title.htmlFileUrl }]
|
||||
: []),
|
||||
...(title.imageUrls || []).map((img, ii) => ({
|
||||
id: `img-${di}-${ci}-${ti}-${i}-${ii}`,
|
||||
label: img.name || `图片 ${ii + 1}`,
|
||||
nodeType: 'image' as const,
|
||||
fileUrl: img.url,
|
||||
})),
|
||||
],
|
||||
})),
|
||||
children: (f.items || []).map((item, ii) => ({
|
||||
id: `item-${di}-${fi}-${ii}`,
|
||||
label: item.label || `作品${ii + 1}`,
|
||||
nodeType: 'title',
|
||||
children: [
|
||||
{
|
||||
id: `html-${di}-${fi}-${ii}`,
|
||||
label: 'HTML',
|
||||
nodeType: 'html' as const,
|
||||
fileUrl: item.content,
|
||||
},
|
||||
],
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
const getList = async () => {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const res = await getCreationList({ ...queryParams, keyword: queryParams.keyword || undefined }, { errorMode: 'page' });
|
||||
const res = await getExecutionList({ errorMode: 'page' });
|
||||
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
|
||||
treeNodes.value = buildTreeNodes(res.data?.Tree || []);
|
||||
treeNodes.value = buildTreeNodes(res.data?.tree || []);
|
||||
} catch {
|
||||
treeNodes.value = [];
|
||||
imgAddressPrefix.value = '';
|
||||
@@ -1048,7 +1059,25 @@ const sendMessage = async () => {
|
||||
isCreating.value = true;
|
||||
|
||||
try {
|
||||
// 1. 构建节点输入参数
|
||||
// 1. 先上传文件到 OSS,获取文件 URL
|
||||
const fileUrls: string[] = [];
|
||||
if (selectedFiles.value.length > 0) {
|
||||
for (const file of selectedFiles.value) {
|
||||
try {
|
||||
const uploadRes = await uploadFile(file, { errorMode: 'page' });
|
||||
// 拼接完整的文件地址
|
||||
const fullUrl = uploadRes.data.fileAddressPrefix
|
||||
? `${uploadRes.data.fileAddressPrefix}${uploadRes.data.fileURL}`
|
||||
: uploadRes.data.fileURL;
|
||||
fileUrls.push(fullUrl);
|
||||
} catch (error) {
|
||||
ElMessage.error(`文件 ${file.name} 上传失败`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 构建节点输入参数
|
||||
const nodeInputParams = currentWorkflowForCreation.value.nodeInputParams?.map((node: any) => {
|
||||
const nodeParam: any = {
|
||||
id: node.id,
|
||||
@@ -1087,44 +1116,26 @@ const sendMessage = async () => {
|
||||
return nodeParam;
|
||||
}) || [];
|
||||
|
||||
// 2. 同步更新 flowContent.nodes,使其与 nodeInputParams 一致
|
||||
// 3. 同步更新 flowContent.nodes,使其与 nodeInputParams 一致
|
||||
const updatedFlowContent = {
|
||||
...currentWorkflowForCreation.value.flowContent,
|
||||
nodes: nodeInputParams, // 使用更新后的节点参数
|
||||
};
|
||||
|
||||
// 3. 构建请求参数
|
||||
// 4. 构建请求参数
|
||||
const params: ExecuteFlowParams = {
|
||||
flowId: parseInt(currentWorkflowForCreation.value.id),
|
||||
flowId: currentWorkflowForCreation.value.id, // ID 是字符串
|
||||
flowContent: updatedFlowContent,
|
||||
nodeInputParams: nodeInputParams,
|
||||
sessionId: getSessionId(),
|
||||
desc: userInput.value,
|
||||
skillName: selectedCreationSkill.value?.name,
|
||||
flowName: currentWorkflowForCreation.value.flowName || currentWorkflowForCreation.value.flowTemplateName, // 工作流名称
|
||||
fileUrl: fileUrls, // 添加文件 URL 数组
|
||||
};
|
||||
|
||||
// 4. 使用 FormData 传递文件流
|
||||
const formData = new FormData();
|
||||
|
||||
// 添加文件
|
||||
if (selectedFiles.value.length > 0) {
|
||||
selectedFiles.value.forEach((file) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加其他参数(转为 JSON 字符串)
|
||||
formData.append('flowId', params.flowId!.toString());
|
||||
formData.append('flowContent', JSON.stringify(params.flowContent));
|
||||
formData.append('nodeInputParams', JSON.stringify(params.nodeInputParams));
|
||||
formData.append('sessionId', params.sessionId!);
|
||||
formData.append('desc', params.desc!);
|
||||
if (params.skillName) {
|
||||
formData.append('skillName', params.skillName);
|
||||
}
|
||||
|
||||
// 5. 调用执行接口
|
||||
await executeFlow(formData, { errorMode: 'page' });
|
||||
// 5. 调用执行接口(不再使用 FormData,直接传 JSON)
|
||||
await executeFlow(params, { errorMode: 'page' });
|
||||
|
||||
ElMessage.success('创作完成!');
|
||||
|
||||
@@ -1151,12 +1162,15 @@ const getFieldClass = (type: string) => {
|
||||
if (type === 'number' || type === 'switch') return 'form-item-small';
|
||||
return 'form-item-medium';
|
||||
};
|
||||
const handleNodeClick = (d: TreeNode) => {
|
||||
// 预览节点
|
||||
const previewNode = (d: TreeNode) => {
|
||||
if (d.nodeType !== 'html' && d.nodeType !== 'image') return;
|
||||
const url = buildAssetUrl(d.fileUrl);
|
||||
if (!url) return ElMessage.warning('当前节点没有可用预览地址');
|
||||
window.open(url, '_blank');
|
||||
previewUrl.value = url;
|
||||
previewDialogVisible.value = true;
|
||||
};
|
||||
// 下载节点
|
||||
const downloadNode = async (d: TreeNode) => {
|
||||
if (d.nodeType !== 'html' && d.nodeType !== 'image') return;
|
||||
if (!d.fileUrl) return ElMessage.warning('当前节点没有可下载地址');
|
||||
@@ -3101,4 +3115,27 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 预览弹窗样式 */
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.preview-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 树节点操作按钮样式 */
|
||||
.tree-node-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user