feat: 添加执行列表功能以支持工作流执行管理

- 新增执行列表相关接口和数据结构,支持获取执行流和执行项
- 更新创作页面以展示执行流和预览功能,提升用户交互体验
- 优化树形结构展示,确保根据执行流动态生成节点
- 引入文件上传功能,支持用户上传文件并获取文件URL
This commit is contained in:
2026-05-09 11:01:32 +08:00
parent a285c9d982
commit 2e6af6e06c
2 changed files with 145 additions and 71 deletions

View File

@@ -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;

View File

@@ -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>
<div v-if="data.nodeType === 'html' || data.nodeType === 'image'" class="tree-node-actions">
<el-button
v-if="data.nodeType === 'html' || data.nodeType === 'image'"
type="primary"
link
class="tree-download"
@click.stop="downloadNode(data)"
>下载</el-button
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}`,
children: (f.items || []).map((item, ii) => ({
id: `item-${di}-${fi}-${ii}`,
label: item.label || `作品${ii + 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,
})),
{
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>