工作流管理相关调整模型列表接口调整
This commit is contained in:
@@ -49,14 +49,14 @@ export interface WorkflowModelItem {
|
||||
modelName: string;
|
||||
modelType: number | string;
|
||||
baseUrl?: string;
|
||||
enabled?: number;
|
||||
enabled?: number | boolean;
|
||||
isOwner?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function getWorkflowModelList(params?: { pageNum: number; pageSize: number; modelName?: string }) {
|
||||
return request({
|
||||
url: '/model-gateway/model/listModel',
|
||||
url: '/model-gateway/model/manage/listModelManage',
|
||||
method: 'get',
|
||||
params,
|
||||
}) as Promise<{ code: number; message: string; data: { list: WorkflowModelItem[]; total: number } }>;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="form-fields-editor">
|
||||
<div v-for="(field, index) in fields" :key="index" class="custom-field-config">
|
||||
<el-input v-model="field.label" placeholder="字段名" class="custom-field-input" @input="sync" />
|
||||
<div class="custom-field-row">
|
||||
<el-select v-model="field.type" placeholder="类型" class="custom-field-type" @change="sync">
|
||||
<el-option label="文本" value="input" />
|
||||
<el-option label="数字" value="number" />
|
||||
<el-option label="多行文本" value="textarea" />
|
||||
<el-option label="开关" value="switch" />
|
||||
<el-option label="文件上传" value="uploadMultiple" />
|
||||
</el-select>
|
||||
<el-checkbox v-model="field.required" class="custom-field-required" @change="sync">必填</el-checkbox>
|
||||
<el-button type="danger" link @click="removeField(index)">删除</el-button>
|
||||
</div>
|
||||
<!-- 文件上传类型:只配置规则,不在此处上传 -->
|
||||
<div v-if="field.type === 'uploadMultiple'" class="upload-rules">
|
||||
<el-input v-model="field.fileTypes" placeholder="文件格式(如 .mp4,jpg,png)" class="rule-file-types" @input="sync" />
|
||||
<el-input-number v-model="field.maxFileSize" :min="1" placeholder="最大大小(MB)" class="rule-file-size" @change="sync" />
|
||||
<el-input-number v-model="field.maxFileCount" :min="1" placeholder="最大数量" class="rule-file-size" @change="sync" />
|
||||
</div>
|
||||
<!-- 非文件上传类型显示默认值输入框 -->
|
||||
<el-input v-else-if="field.type === 'textarea'" v-model="field.value" placeholder="默认值" type="textarea" :rows="2" class="custom-field-value-full" @input="sync" />
|
||||
<el-input-number v-else-if="field.type === 'number'" v-model="field.value" placeholder="默认值" class="custom-field-value-full" @change="sync" />
|
||||
<el-switch v-else-if="field.type === 'switch'" v-model="field.value" class="custom-field-value-full" @change="sync" />
|
||||
<el-input v-else v-model="field.value" placeholder="默认值" class="custom-field-value-full" @input="sync" />
|
||||
</div>
|
||||
<el-button type="primary" link class="w100" @click="addField">+ 添加自定义字段</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
|
||||
export interface FormField {
|
||||
label: string;
|
||||
type: string; // input | number | textarea | switch | uploadMultiple
|
||||
value: any; // 默认值(uploadMultiple 时存文件 URL 数组或空)
|
||||
required: boolean;
|
||||
fileTypes?: string;
|
||||
maxFileSize?: number;
|
||||
maxFileCount?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{ modelValue: FormField[] }>();
|
||||
const emit = defineEmits<{ 'update:modelValue': [FormField[]] }>();
|
||||
|
||||
const fields = ref<FormField[]>([]);
|
||||
let internalSync = false; // 由 props 同步触发,避免 emit 循环
|
||||
|
||||
const clone = (arr: FormField[]): FormField[] => JSON.parse(JSON.stringify(arr || [])) as FormField[];
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
internalSync = true;
|
||||
fields.value = clone(val);
|
||||
nextTick(() => {
|
||||
internalSync = false;
|
||||
});
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const sync = () => {
|
||||
if (internalSync) return;
|
||||
emit('update:modelValue', clone(fields.value));
|
||||
};
|
||||
|
||||
const addField = () => {
|
||||
fields.value.push({ label: '', type: 'input', value: '', required: false });
|
||||
sync();
|
||||
};
|
||||
|
||||
const removeField = (index: number) => {
|
||||
fields.value.splice(index, 1);
|
||||
sync();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-fields-editor {
|
||||
width: 100%;
|
||||
|
||||
.custom-field-config {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
background: #f8fafc;
|
||||
|
||||
.custom-field-input {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.custom-field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.custom-field-type {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.custom-field-required {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-rules {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.rule-file-types,
|
||||
.rule-file-size {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-field-value-full {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -31,8 +31,8 @@
|
||||
<h3 class="model-name">{{ model.modelName }}</h3>
|
||||
<p class="model-url">{{ model.baseUrl }}</p>
|
||||
<div class="model-status">
|
||||
<el-tag :type="model.enabled === 1 ? 'success' : 'info'" size="small">
|
||||
{{ model.enabled === 1 ? '已启用' : '已禁用' }}
|
||||
<el-tag :type="isModelEnabled(model) ? 'success' : 'info'" size="small">
|
||||
{{ isModelEnabled(model) ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +133,9 @@ const getModelTypeName = (type: number | string) => {
|
||||
return typeMap[Number(type)] || '未知类型';
|
||||
};
|
||||
|
||||
// 兼容 enabled 数字(1/0) 与布尔(true/false) 两种返回
|
||||
const isModelEnabled = (model: WorkflowModelItem) => model.enabled === 1 || model.enabled === true;
|
||||
|
||||
const fetchModelList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
|
||||
@@ -45,63 +45,143 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 是否保存文件开关 -->
|
||||
<el-form-item v-if="nodeConfig?.isSaveFileOption" label="是否保存文件">
|
||||
<el-switch
|
||||
:model-value="selectedNode.data?.isSaveFile ?? false"
|
||||
active-text="开启"
|
||||
inactive-text="关闭"
|
||||
@update:model-value="updateIsSaveFile"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 动态表单字段 -->
|
||||
<template v-if="nodeConfig?.formConfig && nodeConfig.formConfig.length > 0">
|
||||
<el-divider content-position="left">节点参数</el-divider>
|
||||
<el-form-item v-for="field in nodeConfig.formConfig" :key="field.field" :label="field.label" :required="field.required">
|
||||
<el-input
|
||||
v-if="field.type === 'input' || field.type === 'string'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
:placeholder="field.required ? '必填' : '选填'"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="field.type === 'number' || field.type === 'inputNumber'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
class="w100"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="field.type === 'textarea'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="field.required ? '必填' : '选填'"
|
||||
/>
|
||||
<el-switch
|
||||
v-else-if="field.type === 'switch'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select' && field.options"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
:placeholder="field.required ? '必填' : '选填'"
|
||||
class="w100"
|
||||
>
|
||||
<el-option v-for="opt in field.options" :key="opt.key || opt.value" :label="opt.value" :value="opt.value" />
|
||||
</el-select>
|
||||
<KeyValueEditor
|
||||
v-else-if="field.type === 'keyValue'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="(v: any) => updateFieldValue(field.field, v)"
|
||||
/>
|
||||
<el-button
|
||||
v-else-if="field.type === 'schemaJson'"
|
||||
@click="openSchemaEditor(field.field)"
|
||||
style="width: 100%"
|
||||
>配置</el-button>
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
:placeholder="field.required ? '必填' : '选填'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<template v-for="field in nodeConfig.formConfig" :key="field.field">
|
||||
<el-form-item :label="field.label" :required="field.required">
|
||||
<el-input
|
||||
v-if="field.type === 'input' || field.type === 'string'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
:placeholder="field.required ? '必填' : '选填'"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="field.type === 'number' || field.type === 'inputNumber'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
class="w100"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="field.type === 'textarea'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="field.required ? '必填' : '选填'"
|
||||
/>
|
||||
<el-switch
|
||||
v-else-if="field.type === 'switch'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select' && field.options"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateSelectFieldValue(field, $event)"
|
||||
:placeholder="field.required ? '必填' : '选填'"
|
||||
class="w100"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in field.options"
|
||||
:key="opt.key || opt.value"
|
||||
:label="opt.key || opt.value"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<KeyValueEditor
|
||||
v-else-if="field.type === 'keyValue'"
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="(v: any) => updateFieldValue(field.field, v)"
|
||||
/>
|
||||
<el-button
|
||||
v-else-if="field.type === 'schemaJson'"
|
||||
@click="openSchemaEditor(field.field)"
|
||||
style="width: 100%"
|
||||
>配置</el-button>
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="getFieldValue(field.field)"
|
||||
@update:model-value="updateFieldValue(field.field, $event)"
|
||||
:placeholder="field.required ? '必填' : '选填'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 下拉选中选项的嵌套配置(responseType 级联) -->
|
||||
<template v-if="getSelectedOptionConfig(field).length > 0">
|
||||
<el-form-item
|
||||
v-for="sub in getSelectedOptionConfig(field)"
|
||||
:key="`${field.field}-${sub.field}`"
|
||||
:label="sub.label"
|
||||
:required="sub.required"
|
||||
>
|
||||
<el-input
|
||||
v-if="sub.type === 'input' || sub.type === 'string'"
|
||||
:model-value="getExpandValue(field.field, sub.field)"
|
||||
@update:model-value="(v: any) => updateExpandValue(field.field, sub.field, v)"
|
||||
:placeholder="sub.required ? '必填' : '选填'"
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="sub.type === 'number' || sub.type === 'inputNumber'"
|
||||
:model-value="getExpandValue(field.field, sub.field)"
|
||||
@update:model-value="(v: any) => updateExpandValue(field.field, sub.field, v)"
|
||||
class="w100"
|
||||
/>
|
||||
<el-switch
|
||||
v-else-if="sub.type === 'switch'"
|
||||
:model-value="getExpandValue(field.field, sub.field)"
|
||||
@update:model-value="(v: any) => updateExpandValue(field.field, sub.field, v)"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="sub.type === 'select' && sub.options"
|
||||
:model-value="getExpandValue(field.field, sub.field)"
|
||||
@update:model-value="(v: any) => updateExpandValue(field.field, sub.field, v)"
|
||||
:placeholder="sub.required ? '必填' : '选填'"
|
||||
class="w100"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in sub.options"
|
||||
:key="opt.key || opt.value"
|
||||
:label="opt.key || opt.value"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<KeyValueEditor
|
||||
v-else-if="sub.type === 'keyValue'"
|
||||
:model-value="getExpandValue(field.field, sub.field)"
|
||||
@update:model-value="(v: any) => updateExpandValue(field.field, sub.field, v)"
|
||||
/>
|
||||
<el-button
|
||||
v-else-if="sub.type === 'schemaJson'"
|
||||
@click="openSchemaEditor(field.field, sub.field)"
|
||||
style="width: 100%"
|
||||
>配置</el-button>
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="getExpandValue(field.field, sub.field)"
|
||||
@update:model-value="(v: any) => updateExpandValue(field.field, sub.field, v)"
|
||||
:placeholder="sub.required ? '必填' : '选填'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</el-form>
|
||||
<!-- 自定义表单字段(formConfigOption,如 form 节点) -->
|
||||
<template v-if="nodeConfig?.formConfigOption">
|
||||
<el-divider content-position="left">自定义字段</el-divider>
|
||||
<FormFieldsEditor :model-value="customFormFields" @update:model-value="updateCustomFields" />
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-empty v-else description="请选择一个节点" :image-size="100" />
|
||||
|
||||
@@ -122,6 +202,7 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import type { Node } from '@vue-flow/core';
|
||||
import KeyValueEditor from './KeyValueEditor.vue';
|
||||
import FormFieldsEditor, { type FormField } from './FormFieldsEditor.vue';
|
||||
import { JsonEditor } from '/@/components/json-schema-editor';
|
||||
|
||||
interface NodeData {
|
||||
@@ -132,6 +213,7 @@ interface NodeData {
|
||||
modelConfig?: any;
|
||||
skillName?: string;
|
||||
patchLayout?: boolean;
|
||||
isSaveFile?: boolean;
|
||||
}
|
||||
|
||||
interface NodeConfig {
|
||||
@@ -156,6 +238,9 @@ const isVideoModelSelected = computed(() => {
|
||||
// 开始节点不展示节点描述
|
||||
const isStartNode = computed(() => props.selectedNode?.data?.nodeCode === '__start__');
|
||||
|
||||
// form 节点自定义字段(formConfigOption)
|
||||
const customFormFields = computed<FormField[]>(() => (props.selectedNode?.data?.formConfig as FormField[]) || []);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selectedNode', node: Node<NodeData, any, string>): void;
|
||||
(e: 'openModelSelector'): void;
|
||||
@@ -168,17 +253,23 @@ const emit = defineEmits<{
|
||||
// Schema 编辑器弹窗
|
||||
const schemaEditorVisible = ref(false);
|
||||
const schemaEditorField = ref('');
|
||||
const schemaEditorExpandField = ref(''); // 空表示顶层字段,非空表示 responseType 的嵌套 expand 字段
|
||||
const schemaEditorData = ref<any>({});
|
||||
|
||||
function openSchemaEditor(fieldName: string) {
|
||||
function openSchemaEditor(fieldName: string, expandField = '') {
|
||||
schemaEditorField.value = fieldName;
|
||||
const val = getFieldValue(fieldName);
|
||||
schemaEditorExpandField.value = expandField;
|
||||
const val = expandField ? getExpandValue(fieldName, expandField) : getFieldValue(fieldName);
|
||||
schemaEditorData.value = val && typeof val === 'object' ? { ...val } : {};
|
||||
schemaEditorVisible.value = true;
|
||||
}
|
||||
|
||||
function confirmSchemaEditor() {
|
||||
updateFieldValue(schemaEditorField.value, schemaEditorData.value);
|
||||
if (schemaEditorExpandField.value) {
|
||||
updateExpandValue(schemaEditorField.value, schemaEditorExpandField.value, schemaEditorData.value);
|
||||
} else {
|
||||
updateFieldValue(schemaEditorField.value, schemaEditorData.value);
|
||||
}
|
||||
schemaEditorVisible.value = false;
|
||||
}
|
||||
|
||||
@@ -206,6 +297,31 @@ const updateNodeDesc = (newDesc: string) => {
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
|
||||
const updateIsSaveFile = (value: boolean) => {
|
||||
if (!props.selectedNode?.data) return;
|
||||
const updatedNode = {
|
||||
...props.selectedNode,
|
||||
data: {
|
||||
...props.selectedNode.data,
|
||||
isSaveFile: value,
|
||||
},
|
||||
};
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
|
||||
// 自定义表单字段(formConfigOption)变更:写回 node.data.formConfig
|
||||
const updateCustomFields = (fields: FormField[]) => {
|
||||
if (!props.selectedNode?.data) return;
|
||||
const updatedNode = {
|
||||
...props.selectedNode,
|
||||
data: {
|
||||
...props.selectedNode.data,
|
||||
formConfig: fields,
|
||||
},
|
||||
};
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
|
||||
const getFieldValue = (fieldName: string) => {
|
||||
if (!props.selectedNode?.data?.formConfig) return '';
|
||||
const field = props.selectedNode.data.formConfig.find((f: any) => f.field === fieldName);
|
||||
@@ -240,6 +356,82 @@ const updateFieldValue = (fieldName: string, value: any) => {
|
||||
};
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
|
||||
// 取 formConfig 中的某个字段条目
|
||||
const getFormConfigEntry = (fieldName: string) => {
|
||||
if (!props.selectedNode?.data?.formConfig) return undefined;
|
||||
return props.selectedNode.data.formConfig.find((f: any) => f.field === fieldName);
|
||||
};
|
||||
|
||||
// 当前选中下拉选项的嵌套 config 定义(来自节点库 presetOption)
|
||||
const getSelectedOptionConfig = (fieldDef: any) => {
|
||||
if (fieldDef?.type !== 'select' || !Array.isArray(fieldDef.options)) return [];
|
||||
const value = getFieldValue(fieldDef.field);
|
||||
const opt = fieldDef.options.find((o: any) => o.value === value);
|
||||
return opt?.config || [];
|
||||
};
|
||||
|
||||
// 读嵌套 expand 字段值
|
||||
const getExpandValue = (hostField: string, subField: string) => {
|
||||
const entry = getFormConfigEntry(hostField);
|
||||
const sub = entry?.expand?.find((e: any) => e.field === subField);
|
||||
return sub?.value ?? '';
|
||||
};
|
||||
|
||||
// 写嵌套 expand 字段值(单次 emit)
|
||||
const updateExpandValue = (hostField: string, subField: string, value: any) => {
|
||||
if (!props.selectedNode?.data) return;
|
||||
const formConfig = props.selectedNode.data.formConfig || [];
|
||||
const entryIndex = formConfig.findIndex((f: any) => f.field === hostField);
|
||||
if (entryIndex < 0) return;
|
||||
|
||||
const entry = formConfig[entryIndex];
|
||||
const expand = entry.expand || [];
|
||||
const subIndex = expand.findIndex((e: any) => e.field === subField);
|
||||
const newExpand = [...expand];
|
||||
if (subIndex >= 0) {
|
||||
newExpand[subIndex] = { ...newExpand[subIndex], value };
|
||||
} else {
|
||||
newExpand.push({ field: subField, value });
|
||||
}
|
||||
|
||||
const updatedFormConfig = [...formConfig];
|
||||
updatedFormConfig[entryIndex] = { ...entry, expand: newExpand };
|
||||
|
||||
const updatedNode = {
|
||||
...props.selectedNode,
|
||||
data: { ...props.selectedNode.data, formConfig: updatedFormConfig },
|
||||
};
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
|
||||
// select 变更统一入口:更新 value 并按选中选项的 config 重建 expand(保留已有字段值)
|
||||
const updateSelectFieldValue = (fieldDef: any, value: any) => {
|
||||
if (!props.selectedNode?.data) return;
|
||||
const formConfig = props.selectedNode.data.formConfig || [];
|
||||
const entryIndex = formConfig.findIndex((f: any) => f.field === fieldDef.field);
|
||||
const opt = (fieldDef.options || []).find((o: any) => o.value === value);
|
||||
const configDefs = opt?.config || [];
|
||||
const oldExpand = entryIndex >= 0 ? formConfig[entryIndex]?.expand || [] : [];
|
||||
const newExpand = configDefs.map((cd: any) => {
|
||||
const old = oldExpand.find((e: any) => e.field === cd.field);
|
||||
return { ...cd, value: old?.value !== undefined ? old.value : (cd.default ?? '') };
|
||||
});
|
||||
|
||||
let updatedFormConfig;
|
||||
if (entryIndex >= 0) {
|
||||
updatedFormConfig = [...formConfig];
|
||||
updatedFormConfig[entryIndex] = { ...updatedFormConfig[entryIndex], value, expand: newExpand };
|
||||
} else {
|
||||
updatedFormConfig = [...formConfig, { ...fieldDef, value, expand: newExpand }];
|
||||
}
|
||||
|
||||
const updatedNode = {
|
||||
...props.selectedNode,
|
||||
data: { ...props.selectedNode.data, formConfig: updatedFormConfig },
|
||||
};
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -110,9 +110,11 @@ interface NodeData {
|
||||
nodeCode?: string;
|
||||
desc?: string;
|
||||
formConfig?: any[];
|
||||
modelConfig?: any;
|
||||
modelConfig?: { modelId?: string; modelName?: string; modelType?: string | number; modelRequestParams?: any } | null;
|
||||
skillName?: string;
|
||||
patchLayout?: boolean;
|
||||
isSaveFile?: boolean;
|
||||
preTool?: string | null;
|
||||
}
|
||||
|
||||
const { addNodes, addEdges, findNode, removeNodes, getNodes, updateNode } = useVueFlow();
|
||||
@@ -240,9 +242,9 @@ const onConnect = (connection: Connection) => {
|
||||
|
||||
const onNodeClick = (event: { node: Node<NodeData, any, string> }) => {
|
||||
selectedNode.value = event.node;
|
||||
// 同步选择器预选项,避免展示上一个节点的模型/技能
|
||||
// 同步选择器预选项,避免展示上一个节点的模型/技能;回读后无 modelName 时仍按 modelId 高亮
|
||||
const mc = event.node.data?.modelConfig;
|
||||
selectedModelData.value = mc?.modelName ? { modelName: mc.modelName, modelType: mc.modelType } : null;
|
||||
selectedModelData.value = mc?.modelId ? { id: mc.modelId, modelName: mc.modelName || '', modelType: mc.modelType } : null;
|
||||
const sk = event.node.data?.skillName;
|
||||
selectedSkillData.value = sk ? { name: sk } : null;
|
||||
};
|
||||
@@ -267,11 +269,10 @@ const handleModelConfirm = (model: any) => {
|
||||
data: {
|
||||
...selectedNode.value.data,
|
||||
modelConfig: {
|
||||
modelId: model.id || '',
|
||||
modelName: model.modelName,
|
||||
modelType: model.modelType,
|
||||
modelApiKey: '',
|
||||
modelForm: model.modelForm || [],
|
||||
modelResponse: model.responseBody || {},
|
||||
modelRequestParams: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -527,7 +528,8 @@ const editWorkflow = async (workflow: WorkflowItem) => {
|
||||
const res = await getWorkflowDetail(workflow.id);
|
||||
if (res.data?.flowContent) {
|
||||
loadWorkflowFromDsl(res.data.flowContent);
|
||||
currentEditingWorkflowId.value = res.data.id;
|
||||
const detail = res.data as any; // 后端详情可能返回 flowId 而非 id
|
||||
currentEditingWorkflowId.value = detail.id || detail.flowId || null;
|
||||
saveForm.value.flowName = res.data.flowName || res.data.flowTemplateName || '';
|
||||
saveForm.value.description = res.data.description || '';
|
||||
ElMessage.success('工作流已加载');
|
||||
@@ -537,26 +539,144 @@ const editWorkflow = async (workflow: WorkflowItem) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 从 DSL 节点重建 formConfig(新格式:outputConfig;旧格式:formConfig 兜底)
|
||||
const buildNodeFormConfigFromDsl = (n: any) => {
|
||||
// form 节点:自定义字段(完整结构 {type,field,label,value,required},兼容命名对象 [{key:value}])
|
||||
if (n.nodeCode === 'form' && Array.isArray(n.outputConfig)) {
|
||||
return n.outputConfig.map((o: any) => {
|
||||
if (o && typeof o === 'object' && o.field !== undefined) {
|
||||
const constraint = o.fieldConstraint || {};
|
||||
return {
|
||||
label: o.label || o.field,
|
||||
field: o.field,
|
||||
type: o.type || 'input',
|
||||
value: o.value ?? '',
|
||||
required: Boolean(o.required),
|
||||
...(o.type === 'uploadMultiple'
|
||||
? {
|
||||
fileTypes: constraint.fileTypes || '',
|
||||
maxFileSize: constraint.maxFileSize,
|
||||
maxFileCount: constraint.maxFileCount,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
// 命名对象兼容:{key: value}
|
||||
const key = Object.keys(o || {})[0] || '';
|
||||
const value = o?.[key];
|
||||
return {
|
||||
label: key,
|
||||
field: key,
|
||||
type: typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'switch' : 'input',
|
||||
value,
|
||||
required: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
// 新格式:HTTP 节点 outputConfig 为 {type,field,label,value} 数组
|
||||
if (Array.isArray(n.outputConfig) && n.outputConfig.length > 0) {
|
||||
const defs = nodeConfigMap.value.get(n.nodeCode)?.formConfig || [];
|
||||
// 无 presetOption 定义(如 form 节点为命名值数组)时直接透传
|
||||
if (defs.length === 0) return n.outputConfig;
|
||||
|
||||
return defs.map((def: any) => {
|
||||
const out = n.outputConfig.find((o: any) => o.field === def.field);
|
||||
if (def.field === 'responseType') {
|
||||
// 恢复嵌套配置:定义取自 presetOption 选中选项的 config,值取自 options[0].config
|
||||
const savedConfig = out?.options?.[0]?.config || [];
|
||||
const selectedOpt = (def.options || []).find((o: any) => o.value === out?.value);
|
||||
const expandDefs = selectedOpt?.config || [];
|
||||
const expand = expandDefs.map((cd: any) => {
|
||||
const saved = savedConfig.find((c: any) => c.field === cd.field);
|
||||
return { ...cd, value: saved?.value ?? '' };
|
||||
});
|
||||
return { ...def, value: out?.value ?? '', expand };
|
||||
}
|
||||
return { ...def, value: out?.value ?? '' };
|
||||
});
|
||||
}
|
||||
// 旧格式兜底
|
||||
if (Array.isArray(n.formConfig)) return n.formConfig;
|
||||
return null;
|
||||
};
|
||||
|
||||
// 构建保存用的 outputConfig(HTTP/form 节点),其余返回 null
|
||||
const buildOutputConfig = (node: Node<NodeData>) => {
|
||||
const nodeCode = node.data?.nodeCode || '';
|
||||
if (nodeCode === 'http') {
|
||||
const defs = nodeConfigMap.value.get(nodeCode)?.formConfig || [];
|
||||
const formConfig = node.data?.formConfig || [];
|
||||
return defs.map((def: any) => {
|
||||
const entry = formConfig.find((f: any) => f.field === def.field);
|
||||
const value = entry?.value ?? '';
|
||||
if (def.field === 'responseType') {
|
||||
const expand = entry?.expand || [];
|
||||
return {
|
||||
type: 'select',
|
||||
field: 'responseType',
|
||||
label: def.label,
|
||||
value,
|
||||
options: expand.length
|
||||
? [{ config: expand.map((e: any) => ({ type: e.type, field: e.field, label: e.label, value: e.value })) }]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
return { type: def.type, field: def.field, label: def.label, value };
|
||||
});
|
||||
}
|
||||
if (nodeCode === 'form') {
|
||||
// form 节点 outputConfig 为自定义字段完整结构(含 type/required,便于完整回显)
|
||||
const fields = Array.isArray(node.data?.formConfig) ? node.data.formConfig : [];
|
||||
return fields.map((f: any) => ({
|
||||
type: f.type || 'input',
|
||||
field: f.label || f.field || '',
|
||||
label: f.label || f.field || '',
|
||||
value: f.value ?? '',
|
||||
required: Boolean(f.required),
|
||||
...(f.type === 'uploadMultiple'
|
||||
? {
|
||||
fieldConstraint: {
|
||||
...(f.fileTypes ? { fileTypes: f.fileTypes } : {}),
|
||||
...(f.maxFileSize !== undefined && f.maxFileSize !== null ? { maxFileSize: f.maxFileSize } : {}),
|
||||
...(f.maxFileCount !== undefined && f.maxFileCount !== null ? { maxFileCount: f.maxFileCount } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 从 DSL 加载工作流
|
||||
const loadWorkflowFromDsl = (dsl: any) => {
|
||||
if (!dsl) return;
|
||||
|
||||
try {
|
||||
const loadedNodes = (dsl.nodes || []).map((n: any) => {
|
||||
const isStart = n.nodeCode === '__start__';
|
||||
return {
|
||||
id: n.id,
|
||||
type: 'default',
|
||||
type: isStart ? 'input' : 'default',
|
||||
position: { x: n.config?.x || 220, y: n.config?.y || 140 },
|
||||
data: {
|
||||
label: n.name || '',
|
||||
nodeCode: n.nodeCode,
|
||||
desc: n.desc || '',
|
||||
formConfig: n.formConfig || null,
|
||||
modelConfig: n.modelConfig || null,
|
||||
formConfig: buildNodeFormConfigFromDsl(n),
|
||||
modelConfig: {
|
||||
modelId: n.modelConfig?.modelId || '',
|
||||
modelName: '',
|
||||
modelType: undefined,
|
||||
modelRequestParams: n.modelConfig?.modelRequestParams ?? null,
|
||||
},
|
||||
skillName: n.skillName || null,
|
||||
patchLayout: n.patchLayout || false,
|
||||
isSaveFile: Boolean(n.isSaveFile),
|
||||
preTool: n.preTool ?? null,
|
||||
},
|
||||
style: { background: '#fff', border: '2px solid #3b82f6', borderRadius: '8px', padding: '10px 20px' },
|
||||
style: isStart
|
||||
? { background: '#10b981', color: '#fff', border: '2px solid #059669', borderRadius: '8px', padding: '10px 20px' }
|
||||
: { background: '#fff', border: '2px solid #3b82f6', borderRadius: '8px', padding: '10px 20px' },
|
||||
};
|
||||
});
|
||||
|
||||
@@ -613,29 +733,37 @@ const confirmSaveWorkflow = async () => {
|
||||
const workflowDsl = {
|
||||
version: '1.0.0',
|
||||
startNodeId: startNode?.id || '',
|
||||
nodes: nodes.value.map((n) => ({
|
||||
id: n.id,
|
||||
nodeCode: n.data?.nodeCode || 'unknown',
|
||||
name: n.data?.label || '',
|
||||
type: n.type || 'default',
|
||||
desc: n.data?.desc || null,
|
||||
skillName: n.data?.skillName || null,
|
||||
patchLayout: n.data?.patchLayout || false,
|
||||
config: {
|
||||
nodeCode: n.data?.nodeCode || 'unknown',
|
||||
x: n.position?.x || 0,
|
||||
y: n.position?.y || 0,
|
||||
},
|
||||
formConfig: n.data?.formConfig || null,
|
||||
modelConfig: n.data?.modelConfig || null,
|
||||
outputResult: null,
|
||||
})),
|
||||
nodes: nodes.value.map((n) => {
|
||||
const gNode = n as any; // VueFlow 运行时会注入 dimensions(渲染尺寸)
|
||||
const nodeCode = n.data?.nodeCode || 'unknown';
|
||||
return {
|
||||
id: n.id,
|
||||
nodeCode,
|
||||
name: n.data?.label || '',
|
||||
config: {
|
||||
nodeCode,
|
||||
x: n.position?.x || 0,
|
||||
y: n.position?.y || 0,
|
||||
width: gNode.dimensions?.width || gNode.width || 100,
|
||||
height: gNode.dimensions?.height || gNode.height || 80,
|
||||
},
|
||||
...(n.data?.preTool ? { preTool: n.data.preTool } : {}),
|
||||
isSaveFile: Boolean(n.data?.isSaveFile),
|
||||
subConfig: null,
|
||||
modelConfig: {
|
||||
modelId: n.data?.modelConfig?.modelId || '',
|
||||
modelRequestParams: n.data?.modelConfig?.modelRequestParams ?? null,
|
||||
},
|
||||
outputConfig: buildOutputConfig(n),
|
||||
...(n.data?.skillName ? { skillName: n.data.skillName } : {}),
|
||||
...(n.data?.patchLayout ? { patchLayout: n.data.patchLayout } : {}),
|
||||
outputResult: null,
|
||||
};
|
||||
}),
|
||||
edges: edges.value.map((e) => ({
|
||||
id: e.id,
|
||||
from: e.source,
|
||||
to: e.target,
|
||||
type: e.type || 'smoothstep',
|
||||
mapping: {},
|
||||
})),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user