首页工作流:输出字段中文展示 + 引用面板滚动修复
- valueSource 契约统一为 { nodeId, field },替换 modelParamUtils 中 fieldName 旧用法
- 数组模板压缩时记录 __originIndex,叶子字段扁平清单按后端原始索引产出 enumValues
- 模型节点输出项保存时用 responseMapping 中文填充 value,引用展示显示中文
- http 节点输出字段识别 JsonEditor 包裹结构,修正真实数据路径并提取中文 label
- 引用上级节点输出面板超高时内部滚动,避免超出视口被截掉
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -124,7 +124,10 @@ import {
|
||||
isEqual,
|
||||
deepClone,
|
||||
removeArrayValueInstances,
|
||||
buildModelRequestParamsPath,
|
||||
enrichResponseBodyMapping,
|
||||
type ExposedField,
|
||||
type ModelRequestParamsPathItem,
|
||||
} from './component/modelParamUtils';
|
||||
import NodeLibraryPanel from './component/NodeLibraryPanel.vue';
|
||||
import WorkflowListPanel from './component/WorkflowListPanel.vue';
|
||||
@@ -164,6 +167,7 @@ interface NodeData {
|
||||
modelName?: string;
|
||||
modelType?: string | number;
|
||||
modelRequestParams?: any;
|
||||
modelRequestParamsPath?: ModelRequestParamsPathItem[] | null;
|
||||
modelFormFields?: ExposedField[] | null;
|
||||
modelResponseBodyMapping?: any; // 模型返回参数(数组或对象两种结构),供下游引用
|
||||
} | null;
|
||||
@@ -355,8 +359,9 @@ const handleModelConfirm = (model: any) => {
|
||||
// 深拷贝模型 requestBodyMapping 作为参数模板(重选模型时覆盖旧参数)
|
||||
// 剔除 isForm=false 的只读字段,使其不显示也不随工作流保存
|
||||
modelRequestParams: stripReadonlyFields(JSON.parse(JSON.stringify(model.requestBodyMapping ?? null))),
|
||||
// 保存模型返回参数(responseBodyMapping),作为该节点可被下游引用的输出项
|
||||
modelResponseBodyMapping: model.responseBodyMapping ?? null,
|
||||
// 保存模型返回参数(responseBodyMapping),作为该节点可被下游引用的输出项;
|
||||
// 用模型 responseMapping 的中文描述填充 value,引用展示显示中文而非英文 key
|
||||
modelResponseBodyMapping: enrichResponseBodyMapping(model.responseBodyMapping, model.responseMapping),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -724,27 +729,50 @@ const parseSchema = (value: any): any => {
|
||||
return null;
|
||||
};
|
||||
|
||||
// 递归收集 JSON schema 的所有叶子字段(数组取首元素对象作为结构样本)
|
||||
const collectLeafFieldsFromJson = (obj: any, prefix = ''): UpstreamField[] => {
|
||||
const result: UpstreamField[] = [];
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return result;
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (!key || key.startsWith('_temp_')) continue;
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
const val = obj[key];
|
||||
if (val && typeof val === 'object') {
|
||||
if (Array.isArray(val)) {
|
||||
const sample = val.find((it: any) => it && typeof it === 'object' && !Array.isArray(it));
|
||||
if (sample) result.push(...collectLeafFieldsFromJson(sample, path));
|
||||
else result.push({ field: path, label: path });
|
||||
} else {
|
||||
result.push(...collectLeafFieldsFromJson(val, path));
|
||||
// 递归收集 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[] => {
|
||||
// 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 (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));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
result.push({ field: path, label: path });
|
||||
if (jtype === 'array') {
|
||||
// 数组:取首元素样本作为结构(实际值为数组整体,field 不带索引)
|
||||
const arr = node.attrs;
|
||||
if (Array.isArray(arr) && arr.length > 0) return collectHttpLeafFields(arr[0], prefix);
|
||||
return [];
|
||||
}
|
||||
// 标量叶子:label 为字段配置的中文名,无则兜底路径
|
||||
return [{ field: prefix, label: (typeof node.label === 'string' && node.label) || prefix }];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
// 普通对象(根/未包裹):递归子字段;子字段为原始标量时以路径兜底产出
|
||||
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 });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// 数组根:取首元素结构样本
|
||||
if (Array.isArray(node)) return node.length > 0 ? collectHttpLeafFields(node[0], prefix) : [];
|
||||
return [];
|
||||
};
|
||||
|
||||
// 取节点在设计时的可引用输出字段(运行时这些字段会产出实际值)
|
||||
@@ -767,16 +795,26 @@ const getNodeOutputFields = (node: Node<NodeData, any, string>): UpstreamField[]
|
||||
? (responseTypeEntry?.expand || []).find((e: any) => e.field === 'response')
|
||||
: formConfig.find((f: any) => f.field === 'response');
|
||||
const schema = parseSchema(responseField?.value);
|
||||
if (schema) return collectLeafFieldsFromJson(schema);
|
||||
if (schema) return collectHttpLeafFields(schema);
|
||||
return [];
|
||||
}
|
||||
if (nodeCode === 'model') {
|
||||
// 模型节点:模型的返回参数(responseBodyMapping)即可被下游引用的输出
|
||||
// 模型节点:模型的返回参数(responseBodyMapping)即可被下游引用的输出;
|
||||
// value 为保存时从模型 responseMapping 填充的中文描述,展示优先用中文,无则兜底 key
|
||||
const resp = node.data?.modelConfig?.modelResponseBodyMapping;
|
||||
const keys = Array.isArray(resp) ? resp : resp && typeof resp === 'object' ? Object.keys(resp) : [];
|
||||
return keys
|
||||
.filter((k: any) => typeof k === 'string' && k.trim() !== '')
|
||||
.map((k: string) => ({ field: k, label: k }));
|
||||
// 数组结构:元素带 field/label
|
||||
if (Array.isArray(resp)) {
|
||||
return resp
|
||||
.filter((it: any) => it && typeof it === 'object' && (it.field || it.label || it.key))
|
||||
.map((it: any) => ({ field: it.field || it.label || it.key || '', label: it.label || it.field || it.key || '' }));
|
||||
}
|
||||
// 对象结构:{ key: 中文描述 }
|
||||
if (resp && typeof resp === 'object') {
|
||||
return Object.keys(resp)
|
||||
.filter((k: any) => typeof k === 'string' && k.trim() !== '')
|
||||
.map((k: string) => ({ field: k, label: (typeof resp[k] === 'string' && resp[k]) || k }));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (nodeCode === START_NODE_CODE) {
|
||||
// 开始节点:运行表单字段(被勾选的模型参数 + form 自定义字段)
|
||||
@@ -1051,7 +1089,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, fieldName: def.field } : undefined;
|
||||
const valueSource = def.isFormField === true ? { nodeId: startNodeId, field: def.field } : undefined;
|
||||
if (def.field === 'responseType') {
|
||||
const expand = entry?.expand || [];
|
||||
return {
|
||||
@@ -1080,7 +1118,7 @@ const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
|
||||
label: f.label || f.field || '',
|
||||
value: f.value ?? '',
|
||||
required: Boolean(f.required),
|
||||
...(fieldName ? { valueSource: { nodeId: startNodeId, fieldName } } : {}),
|
||||
...(fieldName ? { valueSource: { nodeId: startNodeId, field: fieldName } } : {}),
|
||||
...(f.type === 'uploadMultiple'
|
||||
? {
|
||||
fieldConstraint: {
|
||||
@@ -1118,7 +1156,7 @@ const buildOutputConfig = (node: Node<NodeData>, startNodeId = '') => {
|
||||
label: def.label || def.field,
|
||||
// isFormField 字段值在开始节点运行表单,不重复保存(回显从开始节点反查)
|
||||
...(isForm ? {} : { value: entry?.value ?? '' }),
|
||||
...(isForm ? { valueSource: { nodeId: startNodeId, fieldName: def.field } } : {}),
|
||||
...(isForm ? { valueSource: { nodeId: startNodeId, field: def.field } } : {}),
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1147,14 +1185,14 @@ const serializeSubFlowConfig = (config: SubFlowConfig | null | undefined, node?:
|
||||
fieldConstraint: f.fieldConstraint ?? null,
|
||||
options: Array.isArray(f.options) ? f.options : null,
|
||||
multiple: Boolean(f.multiple),
|
||||
// 值来源契约(后端统一 { nodeId, fieldName }):
|
||||
// 值来源契约(后端统一 { nodeId, field }):
|
||||
// - 勾选表单展示:值来自主工作流开始节点(首页表单),指向开始节点
|
||||
// - 引用上级输出:值来自主工作流上游节点,统一转 fieldName
|
||||
// - 引用上级输出:值来自主工作流上游节点,统一转 field
|
||||
// - 其余:无引用(静态默认值)
|
||||
valueSource: f.runtimeShow
|
||||
? { nodeId: startNodeId, fieldName: f.field }
|
||||
? { nodeId: startNodeId, field: f.field }
|
||||
: f.valueSource && typeof f.valueSource === 'object'
|
||||
? { nodeId: f.valueSource.nodeId, fieldName: f.valueSource.field }
|
||||
? { nodeId: f.valueSource.nodeId, field: f.valueSource.field }
|
||||
: null,
|
||||
isFormField: Boolean(f.runtimeShow),
|
||||
})),
|
||||
@@ -1183,7 +1221,7 @@ const buildSubFlowConfigFromDsl = (subConfig: any, startNodeId = '') => {
|
||||
const vs = f.valueSource;
|
||||
// 指向开始节点:值为表单展示(首页可填),编辑器不显示为引用(勾选由 isFormField 恢复)
|
||||
if (vs && typeof vs === 'object' && vs.nodeId && vs.nodeId === startNodeId) return null;
|
||||
// 引用上级:后端 fieldName 转回前端 field 结构
|
||||
// 引用上级:后端 field(兼容旧 DSL 的 fieldName)转回前端 field 结构
|
||||
if (vs && typeof vs === 'object') return { nodeId: vs.nodeId, field: vs.fieldName ?? vs.field };
|
||||
return null;
|
||||
})(),
|
||||
@@ -1205,6 +1243,8 @@ const buildModelConfigFromDsl = (n: any, startNodeId = '') => {
|
||||
modelName: n.modelConfig?.modelName || '',
|
||||
modelType: n.modelConfig?.modelType ?? undefined,
|
||||
modelRequestParams,
|
||||
// 全部叶子字段扁平清单随 DSL 透传,编辑过程不消费,仅保证重开保存不丢
|
||||
modelRequestParamsPath: n.modelConfig?.modelRequestParamsPath ?? null,
|
||||
modelFormFields,
|
||||
modelResponseBodyMapping: n.modelConfig?.modelResponseBodyMapping ?? null,
|
||||
};
|
||||
@@ -1362,7 +1402,7 @@ const confirmSaveWorkflow = async () => {
|
||||
// 数组字段的实例值(value)由前端从模板复制而来,与 enumValues 模板重复,
|
||||
// 保存时不提交;deepClone 避免污染节点运行时状态
|
||||
const savedModelRequestParams = rawModelParams ? removeArrayValueInstances(deepClone(rawModelParams)) : null;
|
||||
// 勾选「表单展示」的字段:补 valueSource={nodeId:开始节点id, fieldName:path}(后端契约,
|
||||
// 勾选「表单展示」的字段:补 valueSource={nodeId:开始节点id, field:path}(后端契约,
|
||||
// 标记字段值来自主工作流开始节点首页表单)
|
||||
if (savedModelRequestParams && startNode?.id) {
|
||||
attachFormFieldValueSources(savedModelRequestParams, savedModelFormFields, startNode.id);
|
||||
@@ -1390,6 +1430,8 @@ const confirmSaveWorkflow = async () => {
|
||||
? { modelType: n.data.modelConfig.modelType }
|
||||
: {}),
|
||||
modelRequestParams: savedModelRequestParams,
|
||||
// 全部叶子字段扁平清单(path + 值 + 值来源),后端按 path 重组请求参数;基于编辑器原始嵌套树生成,保留用户配置的引用 valueSource
|
||||
modelRequestParamsPath: rawModelParams ? buildModelRequestParamsPath(rawModelParams, startNode?.id || '') : null,
|
||||
// 勾选的「表单展示」字段(路径带实例索引),已在上方基于完整结构收集
|
||||
...(savedModelFormFields && savedModelFormFields.length > 0 ? { modelFormFields: savedModelFormFields } : {}),
|
||||
// 模型返回参数随工作流保存,保证重开后仍可被下游引用
|
||||
|
||||
Reference in New Issue
Block a user