diff --git a/src/components/json-schema-editor/FieldConfigDialog.vue b/src/components/json-schema-editor/FieldConfigDialog.vue index 3b81a66..982c0b3 100644 --- a/src/components/json-schema-editor/FieldConfigDialog.vue +++ b/src/components/json-schema-editor/FieldConfigDialog.vue @@ -65,7 +65,9 @@
该节点无输出字段
- {{ of.label || of.field }} + {} 对象 + [] 数组 + {{ of.label || of.field }}
@@ -206,7 +208,9 @@
该节点无输出字段
- {{ of.label || of.field }} + {} 对象 + [] 数组 + {{ of.label || of.field }}
@@ -305,7 +309,7 @@ interface UpstreamNodeInfo { id: string; label: string; nodeCode: string; - outputFields: { field: string; label: string }[]; + outputFields: { field: string; label: string; fieldType?: 'scalar' | 'object' | 'array' }[]; } interface FieldFormData { @@ -584,6 +588,9 @@ function handleClosed() { resetForm(); } padding: 2px 0 2px 8px; } .fcd-ref-field { + display: flex; + align-items: center; + gap: 4px; font-size: 12px; color: #475569; padding: 4px 8px; @@ -593,6 +600,24 @@ function handleClosed() { resetForm(); } background: #eff6ff; color: #3b82f6; } + + .fcd-ref-type { + flex-shrink: 0; + font-size: 11px; + line-height: 1; + color: #64748b; + background: #f1f5f9; + border-radius: 3px; + padding: 2px 4px; + } + + .fcd-ref-field-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } } } diff --git a/src/views/settings/workflow/component/MfReferenceButton.vue b/src/views/settings/workflow/component/MfReferenceButton.vue new file mode 100644 index 0000000..3f21868 --- /dev/null +++ b/src/views/settings/workflow/component/MfReferenceButton.vue @@ -0,0 +1,170 @@ + + + + + + + + diff --git a/src/views/settings/workflow/component/ModelField.vue b/src/views/settings/workflow/component/ModelField.vue index 5cfcc0a..3abfa83 100644 --- a/src/views/settings/workflow/component/ModelField.vue +++ b/src/views/settings/workflow/component/ModelField.vue @@ -71,52 +71,14 @@ class="mf-leaf-expose" >表单展示 - - -
-
无可引用的上级节点
- -
-
+ +
已引用:{{ valueSourceLabel }} 清除
-
+
import { computed, watch } from 'vue'; -import { FolderOpened, List, QuestionFilled, Link } from '@element-plus/icons-vue'; +import { FolderOpened, List, QuestionFilled } from '@element-plus/icons-vue'; +import MfReferenceButton from './MfReferenceButton.vue'; defineOptions({ name: 'ModelField' }); @@ -169,7 +132,7 @@ interface UpstreamNodeInfo { id: string; label: string; nodeCode: string; - outputFields: { field: string; label: string }[]; + outputFields: { field: string; label: string; fieldType?: 'scalar' | 'object' | 'array' }[]; } const props = defineProps<{ fieldDef: any; path?: string; upstreamNodes?: UpstreamNodeInfo[] }>(); @@ -203,9 +166,7 @@ const isTextarea = computed(() => isLeaf.value && fieldType.value === 'textarea' const isUpload = computed(() => isLeaf.value && fieldType.value === 'upload'); // ===== 引用上级节点输出 ===== -// 可引用:叶子 + 非开关 + 非固定选项下拉(开关/下拉值来自模型;上传类型同样支持引用上级输出) -const canValueSource = computed(() => isLeaf.value && !isSwitch.value && !isSelect.value); -const hasUpstream = computed(() => (props.upstreamNodes?.length ?? 0) > 0); +// 引用按钮与候选面板在 MfReferenceButton 中统一渲染(叶子与对象/数组整体均可引用) const hasValueSource = computed(() => !!def.value?.valueSource); // 已引用时的展示文案:{节点label}.{字段label} const valueSourceLabel = computed(() => { @@ -237,12 +198,6 @@ watch( { immediate: true } ); -// 选中前驱节点输出字段:就地写 def.valueSource,复用深 watch 上传链路 -const setValueSource = (node: UpstreamNodeInfo, of: { field: string; label: string }) => { - if (!def.value) return; - def.value.valueSource = { nodeId: node.id, field: of.field }; -}; - const clearValueSource = () => { if (!def.value) return; delete def.value.valueSource; diff --git a/src/views/settings/workflow/component/modelParamUtils.ts b/src/views/settings/workflow/component/modelParamUtils.ts index cf5df60..abebb6e 100644 --- a/src/views/settings/workflow/component/modelParamUtils.ts +++ b/src/views/settings/workflow/component/modelParamUtils.ts @@ -288,6 +288,8 @@ function collectAllLeafFields(params: any, prefix = ''): { path: string; def: an } // 生成 modelRequestParamsPath 扁平清单 +// 所有叶子字段(含对象/数组内部)都支持「引用上游」,valueSource 原样保留; +// 与编辑器「引用上级输出与表单展示同级」语义一致 export function buildModelRequestParamsPath(params: any, startNodeId = ''): ModelRequestParamsPathItem[] { const fields = collectAllLeafFields(params); return fields.map(({ path, def }) => { diff --git a/src/views/settings/workflow/index.vue b/src/views/settings/workflow/index.vue index 6c70bae..d511ef0 100644 --- a/src/views/settings/workflow/index.vue +++ b/src/views/settings/workflow/index.vue @@ -149,6 +149,8 @@ interface RunFormField extends Omit { interface UpstreamField { field: string; label: string; + // 输出字段类型:http 节点容器会额外产出 'object'/'array' 整体候选,标量或未标记视为叶子 + fieldType?: 'scalar' | 'object' | 'array'; } interface UpstreamNodeInfo { id: string; @@ -729,50 +731,61 @@ const parseSchema = (value: any): any => { return null; }; -// 递归收集 http 节点 response schema 的叶子输出字段(识别 JsonEditor 的 { type, attrs/value, label } 包裹结构): +// 递归收集 http 节点 response schema 的输出字段(识别 JsonEditor 的 { type, attrs/value, label } 包裹结构): +// 产出叶子字段 + 对象/数组容器整体候选(下游可引用整个结构); // field 用实际返回数据路径(不含 attrs 元数据,如 data.url 而非 data.attrs.url.value), // label 优先用字段配置的中文名(label),无则兜底路径 key -const collectHttpLeafFields = (node: any, prefix = ''): UpstreamField[] => { +const collectHttpOutputFields = (node: any, prefix = '', isRoot = true, suppressContainer = false): UpstreamField[] => { + const out: UpstreamField[] = []; // JsonEditor 包裹结构:{ type: 'object'|'array'|'string'|..., attrs/value, label } if (node && typeof node === 'object' && !Array.isArray(node) && typeof node.type === 'string') { const jtype = node.type; if (['object', 'array', 'string', 'number', 'boolean', 'null'].includes(jtype)) { + // 对象/数组容器:非根且非数组样本时,产出「整体」候选(引用整体 = 拿整个结构);标量不产整体 + if (!isRoot && !suppressContainer && prefix && (jtype === 'object' || jtype === 'array')) { + out.push({ field: prefix, label: (typeof node.label === 'string' && node.label) || prefix, fieldType: jtype }); + } if (jtype === 'object') { // 对象容器:子字段在 attrs 里,field 路径直接拼子字段名,不拼 attrs const attrs = node.attrs; if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) { - const out: UpstreamField[] = []; for (const k of Object.keys(attrs)) { - out.push(...collectHttpLeafFields(attrs[k], prefix ? `${prefix}.${k}` : k)); + out.push(...collectHttpOutputFields(attrs[k], prefix ? `${prefix}.${k}` : k, false, false)); } - return out; } - return []; + return out; } if (jtype === 'array') { - // 数组:取首元素样本作为结构(实际值为数组整体,field 不带索引) + // 数组:取首元素样本作为结构(实际值为数组整体,field 不带索引); + // 首元素为标量时不产叶子(数组整体已代表该元素),元素为对象时不产对象整体避免与数组整体 field 重复 const arr = node.attrs; - if (Array.isArray(arr) && arr.length > 0) return collectHttpLeafFields(arr[0], prefix); - return []; + if (Array.isArray(arr) && arr.length > 0) { + const el = arr[0]; + if (el && typeof el === 'object' && !Array.isArray(el) && typeof el.type === 'string' && ['string', 'number', 'boolean', 'null'].includes(el.type)) { + return out; + } + return out.concat(collectHttpOutputFields(el, prefix, false, true)); + } + return out; } // 标量叶子:label 为字段配置的中文名,无则兜底路径 - return [{ field: prefix, label: (typeof node.label === 'string' && node.label) || prefix }]; + out.push({ field: prefix, label: (typeof node.label === 'string' && node.label) || prefix, fieldType: 'scalar' }); + return out; } } // 普通对象(根/未包裹):递归子字段;子字段为原始标量时以路径兜底产出 if (node && typeof node === 'object' && !Array.isArray(node)) { - const out: UpstreamField[] = []; for (const k of Object.keys(node)) { const child = node[k]; const path = prefix ? `${prefix}.${k}` : k; - if (child && typeof child === 'object') out.push(...collectHttpLeafFields(child, path)); - else out.push({ field: path, label: path }); + if (child && typeof child === 'object') out.push(...collectHttpOutputFields(child, path, false, false)); + else out.push({ field: path, label: path, fieldType: 'scalar' }); } return out; } // 数组根:取首元素结构样本 - if (Array.isArray(node)) return node.length > 0 ? collectHttpLeafFields(node[0], prefix) : []; - return []; + if (Array.isArray(node)) return node.length > 0 ? collectHttpOutputFields(node[0], prefix, isRoot, suppressContainer) : []; + return out; }; // 取节点在设计时的可引用输出字段(运行时这些字段会产出实际值) @@ -786,7 +799,7 @@ const getNodeOutputFields = (node: Node): UpstreamField[] })); } if (nodeCode === 'http') { - // http 节点:输出 = 结果返回结构(response schema)的所有叶子字段。 + // http 节点:输出 = 结果返回结构(response schema)的叶子字段 + 对象/数组容器整体。 // 结果返回方式为主动拉取(responseType === 'pull')时,取主动拉取分支下配置的结果返回结构。 const formConfig = node.data?.formConfig || []; const responseTypeEntry = formConfig.find((f: any) => f.field === 'responseType'); @@ -795,7 +808,7 @@ const getNodeOutputFields = (node: Node): UpstreamField[] ? (responseTypeEntry?.expand || []).find((e: any) => e.field === 'response') : formConfig.find((f: any) => f.field === 'response'); const schema = parseSchema(responseField?.value); - if (schema) return collectHttpLeafFields(schema); + if (schema) return collectHttpOutputFields(schema); return []; } if (nodeCode === 'model') {