feat: 更新数字人创作页面以支持技能选择功能
- 在节点库项中新增技能选择选项,允许用户为节点指定技能 - 更新API请求路径,统一为'/ai-agent'前缀 - 优化动态表单逻辑,确保根据节点类型正确显示技能选择器 - 移除冗余的文件上传函数,改为导入公共上传函数以简化代码结构
This commit is contained in:
@@ -58,6 +58,20 @@
|
||||
<el-form-item v-if="selectedModel" label="模型 API Key">
|
||||
<el-input v-model="dynamicFormValues.modelApiKey" placeholder="请输入模型 API Key" type="password" show-password />
|
||||
</el-form-item>
|
||||
<!-- 技能选择(如果节点支持) -->
|
||||
<el-form-item v-if="currentNodeSkillOption" label="选择技能">
|
||||
<div class="skill-selector-wrapper">
|
||||
<el-button type="primary" @click="showSkillSelector = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
选择技能
|
||||
</el-button>
|
||||
<div v-if="selectedSkill" class="selected-skill-tag">
|
||||
<el-tag type="success" size="large" closable @close="handleRemoveSkill">
|
||||
{{ selectedSkill.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<!-- 基础表单 + 模型表单 -->
|
||||
<el-form-item v-for="fieldItem in allFormFields" :key="fieldItem.field" :label="fieldItem.label">
|
||||
<el-input
|
||||
@@ -189,10 +203,7 @@
|
||||
<template v-if="currentWorkflowForCreation?.nodeInputParams">
|
||||
<div v-for="node in currentWorkflowForCreation.nodeInputParams" :key="node.id" class="node-form-wrapper">
|
||||
<!-- 跳过开始节点 -->
|
||||
<div
|
||||
v-if="node.nodeCode !== '__start__' && (node.config?.formConfig?.length > 0 || hasVisibleFields(node))"
|
||||
class="node-form-section"
|
||||
>
|
||||
<div v-if="node.nodeCode !== '__start__' && (node.formConfig?.length > 0 || hasVisibleFields(node))" class="node-form-section">
|
||||
<div class="node-form-title">
|
||||
<el-icon class="node-icon"><Document /></el-icon>
|
||||
<span>{{ node.name }}</span>
|
||||
@@ -200,9 +211,9 @@
|
||||
|
||||
<div class="form-grid">
|
||||
<!-- 自定义表单字段 -->
|
||||
<template v-if="node.config?.formConfig && node.config.formConfig.length > 0">
|
||||
<template v-if="node.formConfig && node.formConfig.length > 0">
|
||||
<el-form-item
|
||||
v-for="field in node.config.formConfig"
|
||||
v-for="field in node.formConfig"
|
||||
:key="`${node.id}_${field.label}`"
|
||||
:label="field.label"
|
||||
:required="field.required"
|
||||
@@ -389,17 +400,22 @@
|
||||
<el-button type="primary" @click="confirmSaveWorkflow" :loading="saving">{{ currentEditingWorkflowId ? '确定更新' : '确定保存' }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 技能选择器 -->
|
||||
<SkillSelector v-model="showSkillSelector" :default-skill="selectedSkill" @confirm="handleSkillConfirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Document } from '@element-plus/icons-vue';
|
||||
import { Document, Plus } from '@element-plus/icons-vue';
|
||||
import LogicFlow from '@logicflow/core';
|
||||
import { Control, SelectionSelect } from '@logicflow/extension';
|
||||
import '@logicflow/core/dist/index.css';
|
||||
import '@logicflow/extension/lib/style/index.css';
|
||||
import SkillSelector from '/@/components/skill/NodeSkillSelector.vue';
|
||||
import type { SkillItem } from '/@/api/digitalHuman/skill';
|
||||
import {
|
||||
downloadToFile,
|
||||
getCreationList,
|
||||
@@ -442,6 +458,8 @@ const selectedElement = ref<SelectedState | null>(null);
|
||||
const customFields = ref<Array<{ label: string; value: string; type: string; required: boolean }>>([]);
|
||||
const selectedParentParam = ref('');
|
||||
const selectedModel = ref('');
|
||||
const showSkillSelector = ref(false);
|
||||
const selectedSkill = ref<SkillItem | null>(null);
|
||||
const saving = ref(false);
|
||||
const leftPanelTab = ref('selected'); // 默认显示"当前选中"Tab
|
||||
const saveDialogVisible = ref(false);
|
||||
@@ -489,7 +507,7 @@ const logicFlowRef = ref<HTMLDivElement | null>(null);
|
||||
const logicFlowInstance = ref<LogicFlow | null>(null);
|
||||
const nodeSpawnIndex = ref(0);
|
||||
const formState = reactive({ text: '', nodeCode: '', field: '' });
|
||||
const dynamicFormValues = reactive<Record<string, any>>({});
|
||||
const dynamicFormValues = reactive<Record<string, any>>({ modelApiKey: '' });
|
||||
const nodeSchemaMap = computed(() => {
|
||||
const map: Record<string, NodeLibraryFormItem[]> = {};
|
||||
nodeLibraryGroups.value.forEach((group) => {
|
||||
@@ -512,6 +530,18 @@ const currentNodeModelConfig = computed(() => {
|
||||
});
|
||||
return modelConfigs;
|
||||
});
|
||||
// 获取当前节点是否支持技能选择
|
||||
const currentNodeSkillOption = computed(() => {
|
||||
let skillOption = false;
|
||||
nodeLibraryGroups.value.forEach((group) => {
|
||||
(group.items || []).forEach((item) => {
|
||||
if (item.nodeCode === formState.nodeCode) {
|
||||
skillOption = item.skillOption || false;
|
||||
}
|
||||
});
|
||||
});
|
||||
return skillOption;
|
||||
});
|
||||
// 获取当前选中模型的表单字段
|
||||
const currentModelForm = computed<NodeLibraryFormItem[]>(() => {
|
||||
if (!selectedModel.value) return [];
|
||||
@@ -595,6 +625,7 @@ const workflowDsl = computed(() => ({
|
||||
nodeCode: n.properties?.nodeCode || 'unknown',
|
||||
name: typeof n.text === 'string' ? n.text : n.text?.value || '',
|
||||
type: n.type || 'rect',
|
||||
skillName: n.properties?.skillName || null,
|
||||
config: {
|
||||
nodeCode: n.properties?.nodeCode || 'unknown',
|
||||
width: n.properties?.width || 100,
|
||||
@@ -708,6 +739,38 @@ const handlePageChange = (page: number) => {
|
||||
workflowPagination.pageNum = page;
|
||||
fetchWorkflowList();
|
||||
};
|
||||
// 处理技能选择确认
|
||||
const handleSkillConfirm = (skill: SkillItem) => {
|
||||
selectedSkill.value = skill;
|
||||
// 将技能名称保存到节点属性中(只保存 skillName)
|
||||
if (selectedElement.value && logicFlowInstance.value) {
|
||||
const nodeData = logicFlowInstance.value.getNodeModelById(selectedElement.value.id);
|
||||
if (nodeData) {
|
||||
logicFlowInstance.value.setProperties(selectedElement.value.id, {
|
||||
...nodeData.properties,
|
||||
skillName: skill.name,
|
||||
});
|
||||
// 同步更新 selectedElement
|
||||
selectedElement.value.properties.skillName = skill.name;
|
||||
}
|
||||
}
|
||||
ElMessage.success('技能选择成功');
|
||||
};
|
||||
// 移除已选择的技能
|
||||
const handleRemoveSkill = () => {
|
||||
selectedSkill.value = null;
|
||||
// 从节点属性中移除技能信息
|
||||
if (selectedElement.value && logicFlowInstance.value) {
|
||||
const nodeData = logicFlowInstance.value.getNodeModelById(selectedElement.value.id);
|
||||
if (nodeData) {
|
||||
const props = { ...nodeData.properties };
|
||||
delete props.skillName;
|
||||
logicFlowInstance.value.setProperties(selectedElement.value.id, props);
|
||||
// 同步更新 selectedElement
|
||||
delete selectedElement.value.properties.skillName;
|
||||
}
|
||||
}
|
||||
};
|
||||
// 使用工作流
|
||||
const useWorkflow = async (workflow: WorkflowItem) => {
|
||||
try {
|
||||
@@ -724,17 +787,18 @@ const useWorkflow = async (workflow: WorkflowItem) => {
|
||||
// 根据 nodeInputParams 初始化表单默认值
|
||||
if (res.data.nodeInputParams && Array.isArray(res.data.nodeInputParams)) {
|
||||
res.data.nodeInputParams.forEach((node: any) => {
|
||||
if (node.config?.formConfig && Array.isArray(node.config.formConfig)) {
|
||||
node.config.formConfig.forEach((field: any) => {
|
||||
// 从节点根级别的 formConfig 读取(不是 node.config.formConfig)
|
||||
if (node.formConfig && Array.isArray(node.formConfig)) {
|
||||
node.formConfig.forEach((field: any) => {
|
||||
const fieldKey = `${node.id}_${field.label}`;
|
||||
creationFormValues[fieldKey] = field.value || '';
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化其他配置字段
|
||||
// 初始化其他配置字段(从 config 中读取)
|
||||
if (node.config) {
|
||||
Object.keys(node.config).forEach((key) => {
|
||||
if (!['nodeCode', 'width', 'height', 'x', 'y', 'formConfig', 'inputSource'].includes(key)) {
|
||||
if (!['nodeCode', 'width', 'height', 'x', 'y', 'formConfig', 'inputSource', 'fieldMetadata', 'selectedModel'].includes(key)) {
|
||||
const fieldKey = `${node.id}_${key}`;
|
||||
creationFormValues[fieldKey] = node.config[key];
|
||||
}
|
||||
@@ -979,18 +1043,11 @@ const addParentParam = (value: string) => {
|
||||
inputSource,
|
||||
});
|
||||
|
||||
// 更新 selectedElement 以触发界面刷新
|
||||
const g = lf.getGraphData() as { nodes: Item[]; edges: Item[] };
|
||||
const n = g.nodes.find((x) => x.id === selectedElement.value?.id);
|
||||
if (n) {
|
||||
selectedElement.value = {
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
kind: 'node',
|
||||
properties: n.properties || {},
|
||||
text: typeof n.text === 'string' ? n.text : n.text?.value,
|
||||
};
|
||||
}
|
||||
// 只更新 properties,不重新赋值整个 selectedElement,避免触发 watch 重置表单
|
||||
selectedElement.value.properties = {
|
||||
...currentProps,
|
||||
inputSource,
|
||||
};
|
||||
|
||||
syncDsl();
|
||||
ElMessage.success(`已添加上级参数:${paramName}`);
|
||||
@@ -1022,23 +1079,18 @@ const removeInputSource = (nodeId: string, paramName: string) => {
|
||||
inputSource.splice(nodeIndex, 1);
|
||||
}
|
||||
|
||||
const newInputSource = inputSource.length > 0 ? inputSource : null;
|
||||
|
||||
lf.setProperties(selectedElement.value.id, {
|
||||
...currentProps,
|
||||
inputSource: inputSource.length > 0 ? inputSource : null,
|
||||
inputSource: newInputSource,
|
||||
});
|
||||
|
||||
// 更新 selectedElement 以触发界面刷新
|
||||
const g = lf.getGraphData() as { nodes: Item[]; edges: Item[] };
|
||||
const n = g.nodes.find((x) => x.id === selectedElement.value?.id);
|
||||
if (n) {
|
||||
selectedElement.value = {
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
kind: 'node',
|
||||
properties: n.properties || {},
|
||||
text: typeof n.text === 'string' ? n.text : n.text?.value,
|
||||
};
|
||||
}
|
||||
// 只更新 properties,不重新赋值整个 selectedElement,避免触发 watch 重置表单
|
||||
selectedElement.value.properties = {
|
||||
...currentProps,
|
||||
inputSource: newInputSource,
|
||||
};
|
||||
|
||||
syncDsl();
|
||||
ElMessage.success(`已删除参数:${paramName}`);
|
||||
@@ -1067,18 +1119,11 @@ const updateQuoteOutput = (nodeId: string, enabled: boolean) => {
|
||||
inputSource,
|
||||
});
|
||||
|
||||
// 更新 selectedElement 以触发界面刷新
|
||||
const g = lf.getGraphData() as { nodes: Item[]; edges: Item[] };
|
||||
const n = g.nodes.find((x) => x.id === selectedElement.value?.id);
|
||||
if (n) {
|
||||
selectedElement.value = {
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
kind: 'node',
|
||||
properties: n.properties || {},
|
||||
text: typeof n.text === 'string' ? n.text : n.text?.value,
|
||||
};
|
||||
}
|
||||
// 只更新 properties,不重新赋值整个 selectedElement,避免触发 watch 重置表单
|
||||
selectedElement.value.properties = {
|
||||
...currentProps,
|
||||
inputSource,
|
||||
};
|
||||
|
||||
syncDsl();
|
||||
|
||||
@@ -1194,18 +1239,11 @@ const toggleNodeOutput = (nodeId: string, enabled: boolean) => {
|
||||
inputSource,
|
||||
});
|
||||
|
||||
// 更新 selectedElement 以触发界面刷新
|
||||
const g = lf.getGraphData() as { nodes: Item[]; edges: Item[] };
|
||||
const n = g.nodes.find((x) => x.id === selectedElement.value?.id);
|
||||
if (n) {
|
||||
selectedElement.value = {
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
kind: 'node',
|
||||
properties: n.properties || {},
|
||||
text: typeof n.text === 'string' ? n.text : n.text?.value,
|
||||
};
|
||||
}
|
||||
// 只更新 properties,不重新赋值整个 selectedElement,避免触发 watch 重置表单
|
||||
selectedElement.value.properties = {
|
||||
...currentProps,
|
||||
inputSource,
|
||||
};
|
||||
|
||||
syncDsl();
|
||||
|
||||
@@ -1259,14 +1297,20 @@ watch(
|
||||
formState.text = String(e?.text || '');
|
||||
formState.nodeCode = String(e?.properties?.nodeCode || '');
|
||||
formState.field = String(e?.properties?.field || '');
|
||||
Object.keys(dynamicFormValues).forEach((k) => delete dynamicFormValues[k]);
|
||||
|
||||
// 重置 dynamicFormValues(不删除键,保持响应式)
|
||||
for (const key in dynamicFormValues) {
|
||||
dynamicFormValues[key] = '';
|
||||
}
|
||||
|
||||
// 获取当前节点的基础表单字段(直接从 nodeSchemaMap 获取,避免响应式延迟)
|
||||
const currentNodeCode = formState.nodeCode;
|
||||
const baseFormFields = nodeSchemaMap.value[currentNodeCode] || [];
|
||||
const baseFieldNames = new Set(baseFormFields.map((f) => f.field));
|
||||
|
||||
// 加载自定义字段和基础字段(从 formConfig)
|
||||
customFields.value = [];
|
||||
if (e?.properties?.formConfig && Array.isArray(e.properties.formConfig)) {
|
||||
// 分离基础字段和自定义字段
|
||||
const baseFieldNames = new Set(currentNodeForm.value.map(f => f.field));
|
||||
|
||||
e.properties.formConfig.forEach((fieldConfig: any) => {
|
||||
if (baseFieldNames.has(fieldConfig.field)) {
|
||||
// 基础字段:加载到 dynamicFormValues
|
||||
@@ -1304,15 +1348,42 @@ watch(
|
||||
dynamicFormValues.modelApiKey = e?.properties?.modelApiKey || '';
|
||||
}
|
||||
|
||||
// 获取当前节点的模型配置
|
||||
let nodeModelConfigs: any[] = [];
|
||||
nodeLibraryGroups.value.forEach((group) => {
|
||||
(group.items || []).forEach((item) => {
|
||||
if (item.nodeCode === currentNodeCode) {
|
||||
nodeModelConfigs = item.modelConfig || [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 如果没有选择模型但有模型配置,选择第一个
|
||||
if (!selectedModel.value && currentNodeModelConfig.value.length > 0) {
|
||||
selectedModel.value = currentNodeModelConfig.value[0].modelName;
|
||||
if (!selectedModel.value && nodeModelConfigs.length > 0) {
|
||||
selectedModel.value = nodeModelConfigs[0].modelName;
|
||||
}
|
||||
|
||||
// 恢复技能信息(只根据 skillName)
|
||||
if (e?.properties?.skillName) {
|
||||
// 只保存技能名称用于显示,完整信息在选择时已经保存到节点属性
|
||||
selectedSkill.value = {
|
||||
id: 0,
|
||||
name: e.properties.skillName,
|
||||
description: '',
|
||||
category: '',
|
||||
fileName: '',
|
||||
fileUrl: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
};
|
||||
} else {
|
||||
selectedSkill.value = null;
|
||||
}
|
||||
|
||||
// 初始化所有表单字段(基础 + 模型)- 只设置还没有值的字段
|
||||
allFormFields.value.forEach((fieldItem) => {
|
||||
// 如果已经从 formConfig 或 modelConfig 加载过,跳过
|
||||
if (dynamicFormValues[fieldItem.field] !== undefined) {
|
||||
if (dynamicFormValues[fieldItem.field] !== undefined && dynamicFormValues[fieldItem.field] !== '') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1360,7 +1431,7 @@ const applySelected = () => {
|
||||
formState.field ? (p.field = formState.field) : delete p.field;
|
||||
} else {
|
||||
// 保留重要的配置字段,删除其他字段
|
||||
const keysToKeep = ['nodeCode', 'fieldMetadata', 'modelConfig', 'inputSource', 'formConfig', 'width', 'height'];
|
||||
const keysToKeep = ['nodeCode', 'fieldMetadata', 'modelConfig', 'inputSource', 'formConfig', 'skillName', 'width', 'height'];
|
||||
Object.keys(p).forEach((key) => {
|
||||
if (!keysToKeep.includes(key)) {
|
||||
delete p[key];
|
||||
@@ -1414,7 +1485,7 @@ const applySelected = () => {
|
||||
|
||||
// 保存 formConfig(包含基础字段 + 自定义字段,不包含模型字段)
|
||||
const formConfig: Array<any> = [];
|
||||
|
||||
|
||||
// 1. 添加基础表单字段(非模型字段)
|
||||
// 重用上面的 modelFieldNames
|
||||
currentNodeForm.value.forEach((fieldItem) => {
|
||||
@@ -1423,11 +1494,11 @@ const applySelected = () => {
|
||||
type: fieldItem.type,
|
||||
field: fieldItem.field,
|
||||
label: fieldItem.label,
|
||||
value: value !== undefined && value !== null ? value : (fieldItem.default || ''),
|
||||
value: value !== undefined && value !== null ? value : fieldItem.default || '',
|
||||
required: fieldItem.required || false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 2. 添加自定义字段
|
||||
customFields.value.forEach((field) => {
|
||||
formConfig.push({
|
||||
@@ -1438,7 +1509,7 @@ const applySelected = () => {
|
||||
required: field.required,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 保存 formConfig
|
||||
if (formConfig.length > 0) {
|
||||
p.formConfig = formConfig;
|
||||
@@ -1672,14 +1743,31 @@ const loadWorkflowFromDsl = (dsl: any) => {
|
||||
|
||||
try {
|
||||
// 转换后端 DSL 为 LogicFlow 格式
|
||||
const nodes = (dsl.nodes || []).map((n: any) => ({
|
||||
id: n.id,
|
||||
type: n.type || 'rect',
|
||||
x: n.config?.x || 220,
|
||||
y: n.config?.y || 140,
|
||||
text: n.name || '',
|
||||
properties: n.config || {},
|
||||
}));
|
||||
const nodes = (dsl.nodes || []).map((n: any) => {
|
||||
// 判断是否为判断节点
|
||||
const nodeCode = String(n.nodeCode || '').toLowerCase();
|
||||
const nodeName = String(n.name || '').toLowerCase();
|
||||
const isJudge = JUDGE_KEYWORDS.some((k) => nodeCode.includes(k) || nodeName.includes(k));
|
||||
const nodeType = isJudge ? 'diamond' : 'rect';
|
||||
|
||||
return {
|
||||
id: n.id,
|
||||
type: nodeType,
|
||||
x: n.config?.x || 220,
|
||||
y: n.config?.y || 140,
|
||||
text: n.name || '',
|
||||
properties: {
|
||||
// 保留 config 中的基础配置
|
||||
...(n.config || {}),
|
||||
// 添加节点级别的重要字段
|
||||
nodeCode: n.nodeCode,
|
||||
skillName: n.skillName || null,
|
||||
formConfig: n.formConfig || null,
|
||||
modelConfig: n.modelConfig || null,
|
||||
inputSource: n.inputSource || null,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const edges = (dsl.edges || []).map((e: any) => ({
|
||||
id: e.id,
|
||||
@@ -1695,7 +1783,7 @@ const loadWorkflowFromDsl = (dsl: any) => {
|
||||
syncDsl();
|
||||
ElMessage.success('工作流已加载');
|
||||
} catch (error) {
|
||||
// 后端错误会自动显示
|
||||
ElMessage.error('工作流加载失败');
|
||||
}
|
||||
};
|
||||
onMounted(async () => {
|
||||
@@ -2659,4 +2747,19 @@ onBeforeUnmount(() => {
|
||||
margin-top: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skill-selector-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.selected-skill-tag {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.selected-skill-tag .el-tag {
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user