首页工作流节点引用多选与保存回显完善: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:
2026-08-19 19:53:06 +08:00
co-authored by Claude
parent b6208b37e4
commit 68bdc63231
10 changed files with 298 additions and 80 deletions
@@ -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 }];
}
}
}