首页工作流节点引用多选与保存回显完善:isMultiParameter 全链路透传 + valueSource 数组化/label + 旧 DSL 兼容
- isMultiParameter 节点级开关从节点库透传至引用下拉,支持多选勾选/去重/逐条删除/清空 - valueSource 契约统一为数组并补 label(节点名.字段中文名),覆盖用户引用、表单展示标记与旧 DSL 单对象 - ModelField 递归透传 multi,object/array 嵌套子字段同样支持多选 - 保存/回显兼容旧 DSL 单对象 valueSource,重开自动升级为数组 - modelRequestParamsPath 每项新增 label 字段,后端按 path 重组请求参数 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,8 +25,11 @@ export interface NodeLibraryItem {
|
||||
sort: number;
|
||||
desc?: string;
|
||||
batchExecOption: boolean;
|
||||
preToolOption: boolean;
|
||||
postToolOption: boolean;
|
||||
// 是否允许该节点内「引用上级节点输出」多选(true 时所有引用下拉可多选;节点库返回,仅 model 节点 true)
|
||||
isMultiParameter: boolean;
|
||||
// 前置/后置方法配置定义(结构同 presetOption,值为数组;仅 model 节点提供)
|
||||
preToolOption: NodeLibraryPresetOption[] | null;
|
||||
postToolOption: NodeLibraryPresetOption[] | null;
|
||||
skillOption: boolean;
|
||||
promptOption: boolean;
|
||||
negativePromptOption: boolean;
|
||||
|
||||
@@ -229,14 +229,20 @@ const getVisibleFields = (node: any): any[] => {
|
||||
return collectHomeFormFields(node);
|
||||
};
|
||||
|
||||
// 引用来源展示:valueSource.nodeId 定位上游节点名 + 字段
|
||||
// 引用来源展示:valueSource 统一数组 [{ nodeId, field }](兼容旧 DSL 单对象),逐项定位上游节点名 + 字段
|
||||
const getSourceDisplay = (field: any): string => {
|
||||
const vs = field?.valueSource;
|
||||
if (!vs || typeof vs !== 'object') return '';
|
||||
const arr = Array.isArray(vs) ? vs : [vs];
|
||||
const nodes = props.detail?.nodeInputParams || [];
|
||||
const srcNode = nodes.find((n: any) => String(n?.id) === String(vs.nodeId));
|
||||
const nodeName = srcNode?.name || srcNode?.nodeName || (vs.nodeId ? `节点 ${vs.nodeId}` : '上游节点');
|
||||
return vs.field ? `${nodeName} · ${vs.field}` : nodeName;
|
||||
const parts = arr
|
||||
.filter((r: any) => r && typeof r === 'object')
|
||||
.map((r: any) => {
|
||||
const srcNode = nodes.find((n: any) => String(n?.id) === String(r.nodeId));
|
||||
const nodeName = srcNode?.name || srcNode?.nodeName || (r.nodeId ? `节点 ${r.nodeId}` : '上游节点');
|
||||
return r.field ? `${nodeName} · ${r.field}` : nodeName;
|
||||
});
|
||||
return parts.join(',');
|
||||
};
|
||||
|
||||
const isFileField = (field: any): boolean => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- 引用上级节点输出:按钮 + 候选面板(叶子字段 + 对象/数组整体) -->
|
||||
<!-- 引用上级节点输出:按钮 + 候选面板(叶子字段 + 对象/数组整体);multi 时支持多选 -->
|
||||
<el-popover
|
||||
v-if="canValueSource"
|
||||
placement="bottom"
|
||||
@@ -32,8 +32,12 @@
|
||||
v-for="of in node.outputFields"
|
||||
:key="`${of.field}-${of.fieldType || 'scalar'}`"
|
||||
class="mf-ref-field"
|
||||
@click="setValueSource(node, of)"
|
||||
:class="{ 'is-selected': isSelected(node, of) }"
|
||||
@click="toggleValueSource(node, of)"
|
||||
>
|
||||
<span class="mf-ref-check">
|
||||
<el-icon v-if="isSelected(node, of)"><Check /></el-icon>
|
||||
</span>
|
||||
<span v-if="of.fieldType === 'object'" class="mf-ref-type" title="引用对象整体">{} 对象</span>
|
||||
<span v-else-if="of.fieldType === 'array'" class="mf-ref-type" title="引用数组整体">[] 数组</span>
|
||||
<span class="mf-ref-field-text">{{ of.label || of.field }}</span>
|
||||
@@ -46,7 +50,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Link } from '@element-plus/icons-vue';
|
||||
import { Check, Link } from '@element-plus/icons-vue';
|
||||
|
||||
defineOptions({ name: 'MfReferenceButton' });
|
||||
|
||||
@@ -63,7 +67,7 @@ interface UpstreamNodeInfo {
|
||||
outputFields: UpstreamField[];
|
||||
}
|
||||
|
||||
const props = defineProps<{ fieldDef: any; upstreamNodes?: UpstreamNodeInfo[] }>();
|
||||
const props = defineProps<{ fieldDef: any; upstreamNodes?: UpstreamNodeInfo[]; multi?: boolean }>();
|
||||
|
||||
// 可引用:非开关、非固定选项下拉(开关/下拉值来自模型);叶子与对象/数组整体均可引用
|
||||
const canValueSource = computed(() => {
|
||||
@@ -73,10 +77,35 @@ const canValueSource = computed(() => {
|
||||
});
|
||||
const hasUpstream = computed(() => (props.upstreamNodes?.length ?? 0) > 0);
|
||||
|
||||
// 选中前驱节点输出字段:就地写 fieldDef.valueSource,复用父组件深 watch 上传链路
|
||||
const setValueSource = (node: UpstreamNodeInfo, of: UpstreamField) => {
|
||||
// 当前引用集合(契约统一数组 [{ nodeId, field }];兼容旧 DSL 单对象)
|
||||
const valueSource = computed<any[]>(() => {
|
||||
const vs = props.fieldDef?.valueSource;
|
||||
if (Array.isArray(vs)) return vs;
|
||||
return vs && typeof vs === 'object' && !Array.isArray(vs) ? [vs] : [];
|
||||
});
|
||||
|
||||
const isSelected = (node: UpstreamNodeInfo, of: UpstreamField): boolean =>
|
||||
valueSource.value.some((r) => r && r.nodeId === node.id && r.field === of.field);
|
||||
|
||||
// 点选切换:就地写 fieldDef.valueSource 数组,复用父组件深 watch 上传链路
|
||||
const toggleValueSource = (node: UpstreamNodeInfo, of: UpstreamField) => {
|
||||
if (!props.fieldDef) return;
|
||||
props.fieldDef.valueSource = { nodeId: node.id, field: of.field };
|
||||
const cur = valueSource.value;
|
||||
if (props.multi) {
|
||||
// 多选:勾选/取消切换,同字段去重
|
||||
if (isSelected(node, of)) {
|
||||
props.fieldDef.valueSource = cur.filter((r) => !(r && r.nodeId === node.id && r.field === of.field));
|
||||
} else {
|
||||
props.fieldDef.valueSource = [...cur, { nodeId: node.id, field: of.field }];
|
||||
}
|
||||
} else {
|
||||
// 单选:点已选字段取消,否则替换为单元素数组
|
||||
if (cur.length === 1 && isSelected(node, of)) {
|
||||
props.fieldDef.valueSource = [];
|
||||
} else {
|
||||
props.fieldDef.valueSource = [{ nodeId: node.id, field: of.field }];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -141,11 +170,25 @@ const setValueSource = (node: UpstreamNodeInfo, of: UpstreamField) => {
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.mf-ref-check {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
font-size: 12px;
|
||||
color: #3b82f6;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.mf-ref-type {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
:field-def="childDef"
|
||||
:path="path ? `${path}.attrs.${childKey}` : childKey"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
:multi="multi"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,6 +47,7 @@
|
||||
:field-def="subDef"
|
||||
:path="path ? `${path}.enumValues[${idx}].attrs.${subKey}` : String(subKey)"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
:multi="multi"
|
||||
/>
|
||||
</template>
|
||||
<el-input v-else :model-value="getTemplatePrimitive(item)" @input="(v: any) => setTemplatePrimitive(item, v)" size="small" />
|
||||
@@ -71,12 +73,22 @@
|
||||
class="mf-leaf-expose"
|
||||
>表单展示</el-checkbox>
|
||||
</el-tooltip>
|
||||
<!-- 引用上级输出与「表单展示」勾选同级:所有叶子参数(含数组元素内字段)都可引用 -->
|
||||
<MfReferenceButton :field-def="def" :upstream-nodes="upstreamNodes" />
|
||||
<!-- 引用上级输出与「表单展示」勾选同级:所有叶子参数(含数组元素内字段)都可引用;multi 时支持多选 -->
|
||||
<MfReferenceButton :field-def="def" :upstream-nodes="upstreamNodes" :multi="multi" />
|
||||
</div>
|
||||
<div v-if="hasValueSource" class="mf-ref-bound" :class="{ 'is-invalid': valueSourceInvalid }">
|
||||
<span class="mf-ref-bound-text" :title="valueSourceLabel">已引用:{{ valueSourceLabel }}</span>
|
||||
<el-button size="small" text type="danger" @click="clearValueSource">清除</el-button>
|
||||
<div class="mf-ref-tags">
|
||||
<el-tag
|
||||
v-for="(ref, i) in valueSourceList"
|
||||
:key="`${ref.nodeId}-${ref.field}-${i}`"
|
||||
size="small"
|
||||
class="mf-ref-tag"
|
||||
:type="ref.invalid ? 'danger' : ''"
|
||||
closable
|
||||
@close="removeValueSource(ref)"
|
||||
>{{ ref.label }}</el-tag>
|
||||
</div>
|
||||
<el-button size="small" text type="danger" @click="clearValueSource">清空</el-button>
|
||||
</div>
|
||||
<div v-if="!hasValueSource && !isUpload" class="mf-leaf-ctrl">
|
||||
<el-input-number
|
||||
@@ -135,7 +147,7 @@ interface UpstreamNodeInfo {
|
||||
outputFields: { field: string; label: string; fieldType?: 'scalar' | 'object' | 'array' }[];
|
||||
}
|
||||
|
||||
const props = defineProps<{ fieldDef: any; path?: string; upstreamNodes?: UpstreamNodeInfo[] }>();
|
||||
const props = defineProps<{ fieldDef: any; path?: string; upstreamNodes?: UpstreamNodeInfo[]; multi?: boolean }>();
|
||||
|
||||
// 用 computed 实时取当前 props,避免 internalSync 替换对象树后仍持有旧引用
|
||||
const def = computed(() => props.fieldDef);
|
||||
@@ -167,37 +179,56 @@ const isUpload = computed(() => isLeaf.value && fieldType.value === 'upload');
|
||||
|
||||
// ===== 引用上级节点输出 =====
|
||||
// 引用按钮与候选面板在 MfReferenceButton 中统一渲染(叶子与对象/数组整体均可引用)
|
||||
const hasValueSource = computed(() => !!def.value?.valueSource);
|
||||
// 已引用时的展示文案:{节点label}.{字段label}
|
||||
const valueSourceLabel = computed(() => {
|
||||
const ref = def.value?.valueSource;
|
||||
if (!ref) return '';
|
||||
const node = props.upstreamNodes?.find((n) => n.id === ref.nodeId);
|
||||
const nodeName = node?.label || ref.nodeId;
|
||||
const fieldLabel = node?.outputFields.find((f) => f.field === ref.field)?.label || ref.field;
|
||||
return `${nodeName}.${fieldLabel}`;
|
||||
// valueSource 契约统一数组 [{ nodeId, field }];multi(节点 isMultiParameter)时支持多选
|
||||
const valueSourceList = computed(() => {
|
||||
const vs = def.value?.valueSource;
|
||||
// 兼容旧 DSL 单对象 { nodeId, field }:统一按数组读取
|
||||
if (!vs || typeof vs !== 'object') return [];
|
||||
const arr = Array.isArray(vs) ? vs : [vs];
|
||||
return arr.map((r: any) => {
|
||||
const node = props.upstreamNodes?.find((n) => n.id === r?.nodeId);
|
||||
const nodeName = node?.label || r?.nodeId || '';
|
||||
const fieldLabel = node?.outputFields.find((f) => f.field === r?.field)?.label || r?.field || '';
|
||||
const invalid = !node || !node.outputFields.some((f) => f.field === r?.field);
|
||||
return { nodeId: r?.nodeId, field: r?.field, label: `${nodeName}.${fieldLabel}`, invalid };
|
||||
});
|
||||
});
|
||||
const hasValueSource = computed(() => valueSourceList.value.length > 0);
|
||||
// 任一引用失效即整体标红(失效项逐条展示,由失效清理 watch 自动移除)
|
||||
const valueSourceInvalid = computed(() => valueSourceList.value.some((r) => r.invalid));
|
||||
|
||||
// 引用是否已失效:上游节点被删除,或其输出字段已不存在(换模型/换配置导致 schema 变化)
|
||||
const valueSourceInvalid = computed(() => {
|
||||
const ref = def.value?.valueSource;
|
||||
if (!ref) return false;
|
||||
const node = props.upstreamNodes?.find((n) => n.id === ref.nodeId);
|
||||
if (!node) return true;
|
||||
return !node.outputFields.some((f) => f.field === ref.field);
|
||||
});
|
||||
|
||||
// 失效引用自动清理,避免保存脏数据(就地删除 valueSource,复用深 watch 上传链路)
|
||||
// 失效引用自动清理,避免保存脏数据(过滤失效项;全部失效则删除整个 valueSource,复用深 watch 上传链路)
|
||||
watch(
|
||||
valueSourceInvalid,
|
||||
(invalid) => {
|
||||
if (invalid && def.value) {
|
||||
delete def.value.valueSource;
|
||||
}
|
||||
() => valueSourceList.value,
|
||||
(list) => {
|
||||
if (!def.value) return;
|
||||
const vs = def.value.valueSource;
|
||||
if (!vs || typeof vs !== 'object') return;
|
||||
// 兼容旧 DSL 单对象:按数组处理,清理后统一写回数组
|
||||
const arr = Array.isArray(vs) ? vs : [vs];
|
||||
if (!list.some((r) => r.invalid)) return;
|
||||
const keep = arr.filter((r: any) => {
|
||||
const node = props.upstreamNodes?.find((n) => n.id === r?.nodeId);
|
||||
return node && node.outputFields.some((f) => f.field === r?.field);
|
||||
});
|
||||
if (keep.length === 0) delete def.value.valueSource;
|
||||
else def.value.valueSource = keep;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 删除单个引用(标签 closable)
|
||||
const removeValueSource = (ref: any) => {
|
||||
if (!def.value) return;
|
||||
const vs = def.value.valueSource;
|
||||
if (!vs || typeof vs !== 'object') return;
|
||||
const arr = Array.isArray(vs) ? vs : [vs];
|
||||
const keep = arr.filter((r: any) => !(r?.nodeId === ref.nodeId && r?.field === ref.field));
|
||||
if (keep.length === 0) delete def.value.valueSource;
|
||||
else def.value.valueSource = keep;
|
||||
};
|
||||
|
||||
// 清空全部引用
|
||||
const clearValueSource = () => {
|
||||
if (!def.value) return;
|
||||
delete def.value.valueSource;
|
||||
@@ -446,20 +477,26 @@ const variantLabel = (idx: number): string => templateLabel(def.value?.enumValue
|
||||
|
||||
.mf-ref-bound {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
padding: 6px 8px;
|
||||
|
||||
.mf-ref-bound-text {
|
||||
.mf-ref-tags {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
|
||||
.mf-ref-tag {
|
||||
:deep(.el-tag__content) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-invalid {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
:field-def="def"
|
||||
:path="key"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
:multi="multi"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +34,7 @@ interface UpstreamNodeInfo {
|
||||
outputFields: { field: string; label: string }[];
|
||||
}
|
||||
|
||||
const props = defineProps<{ modelRequestParams: Record<string, any> | null; upstreamNodes?: UpstreamNodeInfo[] }>();
|
||||
const props = defineProps<{ modelRequestParams: Record<string, any> | null; upstreamNodes?: UpstreamNodeInfo[]; multi?: boolean }>();
|
||||
const emit = defineEmits<{ 'update:modelRequestParams': [Record<string, any> | null] }>();
|
||||
|
||||
const localParams = ref<Record<string, any>>({});
|
||||
|
||||
@@ -13,8 +13,39 @@
|
||||
@update:model-value="updateNodeDesc"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="节点类型">
|
||||
<el-tag>{{ selectedNode.data?.nodeCode }}</el-tag>
|
||||
|
||||
<!-- 前置方法 / 后置方法(节点库 preToolOption / postToolOption 配置定义,值保存到节点顶层 preTool / postTool,与 outputConfig 平级) -->
|
||||
<el-form-item v-if="preToolDef" :label="preToolDef.label || '前置方法'">
|
||||
<el-select
|
||||
:model-value="selectedNode.data?.preTool ?? ''"
|
||||
@update:model-value="updatePreTool"
|
||||
clearable
|
||||
class="w100"
|
||||
:placeholder="preToolDef.required ? '必选' : '选填'"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in preToolDef.options || []"
|
||||
:key="opt.key || opt.value"
|
||||
:label="opt.value ?? opt.key"
|
||||
:value="opt.key ?? opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="postToolDef" :label="postToolDef.label || '后置方法'">
|
||||
<el-select
|
||||
:model-value="selectedNode.data?.postTool ?? ''"
|
||||
@update:model-value="updatePostTool"
|
||||
clearable
|
||||
class="w100"
|
||||
:placeholder="postToolDef.required ? '必选' : '选填'"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in postToolDef.options || []"
|
||||
:key="opt.key || opt.value"
|
||||
:label="opt.value ?? opt.key"
|
||||
:value="opt.key ?? opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 开始节点:运行表单字段摘要(来自各模型节点勾选的「表单展示」字段) -->
|
||||
@@ -219,6 +250,7 @@
|
||||
<SubFlowParams
|
||||
:fields="subFlowConfig.fields"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
:multi="!!nodeConfig?.isMultiParameter"
|
||||
@update:fields="updateSubFlowFields"
|
||||
/>
|
||||
</template>
|
||||
@@ -245,6 +277,7 @@
|
||||
<ModelParamsForm
|
||||
:model-request-params="selectedNode.data.modelConfig.modelRequestParams"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
:multi="!!nodeConfig?.isMultiParameter"
|
||||
@update:model-request-params="updateModelRequestParams"
|
||||
/>
|
||||
</template>
|
||||
@@ -291,6 +324,8 @@ interface NodeData {
|
||||
negativePrompt?: string;
|
||||
patchLayout?: boolean;
|
||||
isSaveFile?: boolean;
|
||||
preTool?: string | null;
|
||||
postTool?: string | null;
|
||||
runFormFields?: any[];
|
||||
// 子流程节点(sub_flow):引入的工作流配置(workflowId + 引入参数)
|
||||
subFlowConfig?: any;
|
||||
@@ -298,6 +333,9 @@ interface NodeData {
|
||||
|
||||
interface NodeConfig {
|
||||
formConfig: any[];
|
||||
preToolOption: any[];
|
||||
postToolOption: any[];
|
||||
isMultiParameter: boolean;
|
||||
modelConfigOption: boolean;
|
||||
formConfigOption: boolean;
|
||||
skillOption: boolean;
|
||||
@@ -465,6 +503,42 @@ const updateModelRequestParams = (params: Record<string, any> | null) => {
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
|
||||
// 前置/后置方法配置定义(节点库 preToolOption / postToolOption,仅 model 节点提供;取首个配置项渲染)
|
||||
const preToolDef = computed(() => {
|
||||
const defs = props.nodeConfig?.preToolOption;
|
||||
return Array.isArray(defs) && defs.length > 0 ? defs[0] : null;
|
||||
});
|
||||
const postToolDef = computed(() => {
|
||||
const defs = props.nodeConfig?.postToolOption;
|
||||
return Array.isArray(defs) && defs.length > 0 ? defs[0] : null;
|
||||
});
|
||||
|
||||
// 前置方法值写节点顶层 preTool(与 outputConfig 平级)
|
||||
const updatePreTool = (value: string) => {
|
||||
if (!props.selectedNode?.data) return;
|
||||
const updatedNode = {
|
||||
...props.selectedNode,
|
||||
data: {
|
||||
...props.selectedNode.data,
|
||||
preTool: value,
|
||||
},
|
||||
};
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
|
||||
// 后置方法值写节点顶层 postTool(与 outputConfig 平级)
|
||||
const updatePostTool = (value: string) => {
|
||||
if (!props.selectedNode?.data) return;
|
||||
const updatedNode = {
|
||||
...props.selectedNode,
|
||||
data: {
|
||||
...props.selectedNode.data,
|
||||
postTool: value,
|
||||
},
|
||||
};
|
||||
emit('update:selectedNode', updatedNode);
|
||||
};
|
||||
|
||||
// 子流程引入参数就地变更(ModelField 已就地写共享引用):
|
||||
// 仅刷新节点引用同步 VueFlow / 父组件状态;fields 为同一数组引用,避免与 SubFlowParams 深 watch 形成循环
|
||||
const updateSubFlowFields = (fields: SubFlowField[]) => {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:field-def="f"
|
||||
:path="f.field"
|
||||
:upstream-nodes="upstreamNodes"
|
||||
:multi="multi"
|
||||
/>
|
||||
</template>
|
||||
<el-empty v-else description="该工作流无可引入的开始参数" :image-size="60" />
|
||||
@@ -26,6 +27,7 @@ defineOptions({ name: 'SubFlowParams' });
|
||||
interface Props {
|
||||
fields: SubFlowField[];
|
||||
upstreamNodes?: any[];
|
||||
multi?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
|
||||
@@ -188,7 +188,7 @@ export interface ExposedField {
|
||||
value?: any;
|
||||
defaultValue?: any; // 默认值:首页初始化/回显用
|
||||
fieldConstraint?: any; // 字段约束:上传格式/大小/数量、数字 min/max 等
|
||||
valueSource?: any; // 引用上游节点输出({ nodeId, field });有则首页只读展示
|
||||
valueSource?: any; // 引用上游节点输出([{ nodeId, field }] 数组,支持多选);有则首页只读展示
|
||||
multiple?: boolean; // 多文件上传标记
|
||||
refNodeId?: string; // 预留:引用其他节点功能(后续启用)
|
||||
}
|
||||
@@ -232,7 +232,7 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] {
|
||||
value: def.value,
|
||||
defaultValue: def.defaultValue,
|
||||
fieldConstraint: def.fieldConstraint && typeof def.fieldConstraint === 'object' ? def.fieldConstraint : undefined,
|
||||
valueSource: def.valueSource && typeof def.valueSource === 'object' ? def.valueSource : undefined,
|
||||
valueSource: Array.isArray(def.valueSource) ? def.valueSource : undefined,
|
||||
multiple: def.multiple || def.fieldType === 'uploadMultiple' || undefined,
|
||||
});
|
||||
}
|
||||
@@ -243,12 +243,14 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] {
|
||||
// ===== 模型请求参数扁平清单(modelRequestParamsPath)=====
|
||||
// 后端契约:保存时与 modelRequestParams 平级新增 modelRequestParamsPath,
|
||||
// 为嵌套模型参数树的全部叶子字段扁平清单(path 文法与 collectExposedFields 一致),
|
||||
// 后端按 path 重组请求参数。value 保留当前值;valueSource 规则:
|
||||
// - 已有引用配置(用户在设计器配置「引用上游」)→ 原样保留 { nodeId, field }
|
||||
// - 无引用但勾选「表单展示」(runtimeShow=true)→ 补 { nodeId: 开始节点id, field: path }
|
||||
// 后端按 path 重组请求参数。value 保留当前值;valueSource 规则(统一数组 [{ nodeId, field }]):
|
||||
// - 已有引用配置(用户在设计器配置「引用上游」)→ 原样保留数组
|
||||
// - 无引用但勾选「表单展示」(runtimeShow=true)→ 补 [{ nodeId: 开始节点id, field: path }]
|
||||
// - 其余 → 不带 valueSource
|
||||
export interface ModelRequestParamsPathItem {
|
||||
path: string;
|
||||
// 叶子字段中文名(回显/展示用;无 label 时兜底 path 最后一段字段名)
|
||||
label: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
value: any;
|
||||
@@ -290,17 +292,44 @@ function collectAllLeafFields(params: any, prefix = ''): { path: string; def: an
|
||||
// 生成 modelRequestParamsPath 扁平清单
|
||||
// 所有叶子字段(含对象/数组内部)都支持「引用上游」,valueSource 原样保留;
|
||||
// 与编辑器「引用上级输出与表单展示同级」语义一致
|
||||
export function buildModelRequestParamsPath(params: any, startNodeId = ''): ModelRequestParamsPathItem[] {
|
||||
export function buildModelRequestParamsPath(
|
||||
params: any,
|
||||
startNodeId = '',
|
||||
// 节点输出索引(id → 节点名 + 字段 label),用于补全 valueSource 的引用来源 label
|
||||
outputIndex?: Map<string, { name: string; fields: Map<string, string> }>
|
||||
): ModelRequestParamsPathItem[] {
|
||||
const fields = collectAllLeafFields(params);
|
||||
// 引用来源 label:节点名.字段label(无索引/未命中时兜底 field)
|
||||
const resolveRefLabel = (nodeId: string, field: string): string => {
|
||||
const info = outputIndex?.get(nodeId);
|
||||
if (!info) return field;
|
||||
const fieldLabel = info.fields.get(field) || field;
|
||||
return info.name ? `${info.name}.${fieldLabel}` : fieldLabel;
|
||||
};
|
||||
return fields.map(({ path, def }) => {
|
||||
const valueSource =
|
||||
def.valueSource && typeof def.valueSource === 'object'
|
||||
? def.valueSource // 引用上游:前端编辑态 { nodeId, field } 原样保留
|
||||
: def.runtimeShow === true && startNodeId
|
||||
? { nodeId: startNodeId, field: path } // 勾选「表单展示」:补后端契约 { nodeId, field }
|
||||
: undefined;
|
||||
let valueSource: any;
|
||||
if (Array.isArray(def.valueSource)) {
|
||||
// 引用上游:前端编辑态 [{ nodeId, field }] 数组,逐项补 label
|
||||
valueSource = def.valueSource.map((r: any) =>
|
||||
r && r.nodeId && r.field
|
||||
? { nodeId: r.nodeId, field: r.field, label: resolveRefLabel(r.nodeId, r.field) }
|
||||
: r
|
||||
);
|
||||
} else if (def.valueSource && typeof def.valueSource === 'object') {
|
||||
// 兼容旧 DSL 单对象 { nodeId, field }
|
||||
const r = def.valueSource as any;
|
||||
valueSource = [
|
||||
r && r.nodeId && r.field
|
||||
? { nodeId: r.nodeId, field: r.field, label: resolveRefLabel(r.nodeId, r.field) }
|
||||
: r,
|
||||
];
|
||||
} else if (def.runtimeShow === true && startNodeId) {
|
||||
// 勾选「表单展示」:补后端契约数组(指向开始节点,label 取开始节点字段名)
|
||||
valueSource = [{ nodeId: startNodeId, field: path, label: resolveRefLabel(startNodeId, path) }];
|
||||
}
|
||||
return {
|
||||
path,
|
||||
label: def.label || path.split('.').pop() || '',
|
||||
type: def.type || 'string',
|
||||
required: Boolean(def.required),
|
||||
value: def.value ?? '',
|
||||
@@ -319,8 +348,10 @@ export function restoreRuntimeShow(params: any, fields: ExposedField[] | null |
|
||||
const cur = resolvePath(params, f.path);
|
||||
if (cur && typeof cur === 'object' && !Array.isArray(cur)) {
|
||||
cur.runtimeShow = true;
|
||||
if (startNodeId && cur.valueSource && typeof cur.valueSource === 'object' && cur.valueSource.nodeId === startNodeId) {
|
||||
delete cur.valueSource;
|
||||
if (startNodeId && Array.isArray(cur.valueSource)) {
|
||||
// 移除指向开始节点(表单展示标记)的引用;全部移除后删除整个 valueSource
|
||||
cur.valueSource = cur.valueSource.filter((r: any) => !(r && r.nodeId === startNodeId));
|
||||
if (cur.valueSource.length === 0) delete cur.valueSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,7 +365,7 @@ export function attachFormFieldValueSources(params: any, fields: ExposedField[]
|
||||
if (!f || typeof f.path !== 'string') continue;
|
||||
const cur = resolvePath(params, f.path);
|
||||
if (cur && typeof cur === 'object' && !Array.isArray(cur)) {
|
||||
cur.valueSource = { nodeId: startNodeId, field: f.path };
|
||||
cur.valueSource = [{ nodeId: startNodeId, field: f.path }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface SubFlowField {
|
||||
// 多文件上传标记
|
||||
multiple?: boolean;
|
||||
// 引用上级节点输出(主工作流上游);有值则编辑器内只读引用展示
|
||||
valueSource?: { nodeId: string; field: string } | null;
|
||||
valueSource?: { nodeId: string; field: string }[] | null;
|
||||
// 编辑器内「表单展示」勾选(ModelField 用 runtimeShow)
|
||||
runtimeShow?: boolean;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export interface SubFlowDslField {
|
||||
fieldConstraint?: any;
|
||||
options?: any[] | null;
|
||||
multiple?: boolean;
|
||||
valueSource?: { nodeId: string; field: string } | null;
|
||||
valueSource?: { nodeId: string; field: string }[] | null;
|
||||
// 勾选表单展示 → 聚合进主工作流开始节点(首页表单可填)
|
||||
isFormField?: boolean;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ interface NodeData {
|
||||
patchLayout?: boolean;
|
||||
isSaveFile?: boolean;
|
||||
preTool?: string | null;
|
||||
postTool?: string | null;
|
||||
runFormFields?: RunFormField[]; // 仅开始节点使用
|
||||
// 子流程节点(sub_flow):引入的工作流配置(workflowId + 引入参数)
|
||||
subFlowConfig?: SubFlowConfig | null;
|
||||
@@ -205,6 +206,9 @@ const nodeConfigMap = computed(() => {
|
||||
string,
|
||||
{
|
||||
formConfig: any[];
|
||||
preToolOption: any[];
|
||||
postToolOption: any[];
|
||||
isMultiParameter: boolean;
|
||||
modelConfigOption: boolean;
|
||||
formConfigOption: boolean;
|
||||
skillOption: boolean;
|
||||
@@ -217,6 +221,10 @@ const nodeConfigMap = computed(() => {
|
||||
group.nodes.forEach((item) => {
|
||||
map.set(item.key, {
|
||||
formConfig: item.presetOption || [],
|
||||
// 前置/后置方法配置定义(后端新增,仅 model 节点提供;值保存到节点顶层 preTool/postTool 字段)
|
||||
preToolOption: item.preToolOption || [],
|
||||
postToolOption: item.postToolOption || [],
|
||||
isMultiParameter: item.isMultiParameter || false,
|
||||
modelConfigOption: item.modelConfigOption || false,
|
||||
formConfigOption: item.formConfigOption || false,
|
||||
skillOption: item.skillOption || false,
|
||||
@@ -656,7 +664,7 @@ const syncRunFormFields = () => {
|
||||
value: f.value ?? f.defaultValue ?? '',
|
||||
defaultValue: f.defaultValue,
|
||||
fieldConstraint: f.fieldConstraint && typeof f.fieldConstraint === 'object' ? f.fieldConstraint : undefined,
|
||||
valueSource: f.valueSource && typeof f.valueSource === 'object' ? f.valueSource : undefined,
|
||||
valueSource: Array.isArray(f.valueSource) ? f.valueSource : undefined,
|
||||
options: Array.isArray(f.options) ? f.options : undefined,
|
||||
multiple: isUploadMultiple || f.multiple || undefined,
|
||||
nodeId: n.id,
|
||||
@@ -1102,7 +1110,7 @@ const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
|
||||
const entry = formConfig.find((f: any) => f.field === def.field);
|
||||
const value = entry?.value ?? '';
|
||||
// 勾选「表单展示」的字段:值来自开始节点表单,补 valueSource 标记(后端契约)
|
||||
const valueSource = def.isFormField === true ? { nodeId: startNodeId, field: def.field } : undefined;
|
||||
const valueSource = def.isFormField === true ? [{ nodeId: startNodeId, field: def.field }] : undefined;
|
||||
if (def.field === 'responseType') {
|
||||
const expand = entry?.expand || [];
|
||||
return {
|
||||
@@ -1131,7 +1139,7 @@ const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
|
||||
label: f.label || f.field || '',
|
||||
value: f.value ?? '',
|
||||
required: Boolean(f.required),
|
||||
...(fieldName ? { valueSource: { nodeId: startNodeId, field: fieldName } } : {}),
|
||||
...(fieldName ? { valueSource: [{ nodeId: startNodeId, field: fieldName }] } : {}),
|
||||
...(f.type === 'uploadMultiple'
|
||||
? {
|
||||
fieldConstraint: {
|
||||
@@ -1169,7 +1177,7 @@ const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
|
||||
label: def.label || def.field,
|
||||
// isFormField 字段值在开始节点运行表单,不重复保存(回显从开始节点反查)
|
||||
...(isForm ? {} : { value: entry?.value ?? '' }),
|
||||
...(isForm ? { valueSource: { nodeId: startNodeId, field: def.field } } : {}),
|
||||
...(isForm ? { valueSource: [{ nodeId: startNodeId, field: def.field }] } : {}),
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1203,9 +1211,9 @@ const serializeSubFlowConfig = (config: SubFlowConfig | null | undefined, node?:
|
||||
// - 引用上级输出:值来自主工作流上游节点,统一转 field
|
||||
// - 其余:无引用(静态默认值)
|
||||
valueSource: f.runtimeShow
|
||||
? { nodeId: startNodeId, field: f.field }
|
||||
: f.valueSource && typeof f.valueSource === 'object'
|
||||
? { nodeId: f.valueSource.nodeId, field: f.valueSource.field }
|
||||
? [{ nodeId: startNodeId, field: f.field }]
|
||||
: Array.isArray(f.valueSource)
|
||||
? f.valueSource.map((vs: any) => ({ nodeId: vs.nodeId, field: vs.field }))
|
||||
: null,
|
||||
isFormField: Boolean(f.runtimeShow),
|
||||
})),
|
||||
@@ -1232,11 +1240,12 @@ const buildSubFlowConfigFromDsl = (subConfig: any, startNodeId = '') => {
|
||||
multiple: Boolean(f.multiple) || f.fieldType === 'uploadMultiple',
|
||||
valueSource: (() => {
|
||||
const vs = f.valueSource;
|
||||
// 统一数组:兼容旧 DSL 单对象
|
||||
const arr = Array.isArray(vs) ? vs : vs && typeof vs === 'object' ? [vs] : [];
|
||||
// 指向开始节点:值为表单展示(首页可填),编辑器不显示为引用(勾选由 isFormField 恢复)
|
||||
if (vs && typeof vs === 'object' && vs.nodeId && vs.nodeId === startNodeId) return null;
|
||||
const refs = arr.filter((r: any) => !(r && r.nodeId && r.nodeId === startNodeId));
|
||||
// 引用上级:后端 field(兼容旧 DSL 的 fieldName)转回前端 field 结构
|
||||
if (vs && typeof vs === 'object') return { nodeId: vs.nodeId, field: vs.fieldName ?? vs.field };
|
||||
return null;
|
||||
return refs.length ? refs.map((r: any) => ({ nodeId: r.nodeId, field: r.fieldName ?? r.field })) : null;
|
||||
})(),
|
||||
runtimeShow: Boolean(f.isFormField),
|
||||
})),
|
||||
@@ -1317,6 +1326,7 @@ const loadWorkflowFromDsl = (dsl: any) => {
|
||||
patchLayout: n.patchLayout || false,
|
||||
isSaveFile: Boolean(n.isSaveFile),
|
||||
preTool: n.preTool ?? null,
|
||||
postTool: n.postTool ?? null,
|
||||
// 子流程节点:恢复引入的工作流配置(无 subConfig 时返回 null,兼容旧 DSL)
|
||||
...(n.nodeCode === 'sub_flow' ? { subFlowConfig: buildSubFlowConfigFromDsl(n.subConfig, startNodeId) } : {}),
|
||||
// 开始节点运行表单字段:新格式存 outputConfig;旧 DSL 顶层 runFormFields 兜底兼容
|
||||
@@ -1403,6 +1413,16 @@ const confirmSaveWorkflow = async () => {
|
||||
ElMessage.warning(`存在重名运行字段:${dupNames.join('、')},首页表单可能出现冲突`);
|
||||
}
|
||||
|
||||
// 节点 id → 名称与输出字段(field → label)索引,供 modelRequestParamsPath 的 valueSource label 补全
|
||||
const outputFieldsIndex = new Map<string, { name: string; fields: Map<string, string> }>();
|
||||
nodes.value.forEach((n) => {
|
||||
const fields = getNodeOutputFields(n);
|
||||
outputFieldsIndex.set(n.id, {
|
||||
name: n.data?.label || n.id,
|
||||
fields: new Map(fields.map((f: any) => [f.field, f.label || f.field])),
|
||||
});
|
||||
});
|
||||
|
||||
const workflowDsl = {
|
||||
version: '1.0.0',
|
||||
startNodeId: startNode?.id || '',
|
||||
@@ -1432,6 +1452,7 @@ const confirmSaveWorkflow = async () => {
|
||||
height: gNode.dimensions?.height || gNode.height || 80,
|
||||
},
|
||||
...(n.data?.preTool ? { preTool: n.data.preTool } : {}),
|
||||
...(n.data?.postTool ? { postTool: n.data.postTool } : {}),
|
||||
isSaveFile: Boolean(n.data?.isSaveFile),
|
||||
// 子流程节点:序列化引入的工作流配置(含 workflowId 与 isFormField 标记);其余节点保持 null
|
||||
subConfig: nodeCode === 'sub_flow' ? serializeSubFlowConfig(n.data?.subFlowConfig, n, startNode?.id || '') : null,
|
||||
@@ -1444,7 +1465,7 @@ const confirmSaveWorkflow = async () => {
|
||||
: {}),
|
||||
modelRequestParams: savedModelRequestParams,
|
||||
// 全部叶子字段扁平清单(path + 值 + 值来源),后端按 path 重组请求参数;基于编辑器原始嵌套树生成,保留用户配置的引用 valueSource
|
||||
modelRequestParamsPath: rawModelParams ? buildModelRequestParamsPath(rawModelParams, startNode?.id || '') : null,
|
||||
modelRequestParamsPath: rawModelParams ? buildModelRequestParamsPath(rawModelParams, startNode?.id || '', outputFieldsIndex) : null,
|
||||
// 勾选的「表单展示」字段(路径带实例索引),已在上方基于完整结构收集
|
||||
...(savedModelFormFields && savedModelFormFields.length > 0 ? { modelFormFields: savedModelFormFields } : {}),
|
||||
// 模型返回参数随工作流保存,保证重开后仍可被下游引用
|
||||
|
||||
Reference in New Issue
Block a user