// 模型参数表单共用工具(workflow 独立实现,不依赖内容创作) // 与项目一致的深拷贝方式 export function deepClone(val: T): T { return JSON.parse(JSON.stringify(val ?? null)) as T; } // 按类型生成叶子字段空值 export function defaultLeafValue(def: any): any { const t = def?.type || 'string'; const ft = def?.fieldType || ''; if (t === 'boolean' || ft === 'switch') return false; if (t === 'number' || ft === 'number') return 0; return ''; } // 归一化数组元素:元素为 { type:'object', attrs } 包装时,递归预填 attrs 子字段 export function normalizeArrayItem(item: any): any { if (item && typeof item === 'object') { if (item.type === 'object' && item.attrs && typeof item.attrs === 'object' && !Array.isArray(item.attrs)) { Object.values(item.attrs).forEach(normalizeFieldDef); } } return item; } // 归一化字段定义:用 defaultValue 预填 value;递归处理 object/array 子树 export function normalizeFieldDef(def: any): any { if (!def || typeof def !== 'object') return def; const t = def.type; if (t === 'object') { const attrs = def.attrs; if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) { Object.values(attrs).forEach(normalizeFieldDef); } if (def.value === undefined || def.value === null) { def.value = def.defaultValue !== undefined && def.defaultValue !== null ? def.defaultValue : {}; } } else if (t === 'array') { if (Array.isArray(def.value)) { def.value.forEach(normalizeArrayItem); } else if (Array.isArray(def.defaultValue)) { def.value = def.defaultValue.map((it: any) => normalizeArrayItem(deepClone(it))); } else { def.value = []; } } else { if (def.value === undefined || def.value === null) { def.value = def.defaultValue !== undefined && def.defaultValue !== null ? def.defaultValue : defaultLeafValue(def); } } return def; } // 归一化整个 requestBodyMapping 参数模板对象 export function normalizeModelParams(params: any): Record { if (!params || typeof params !== 'object') return {}; Object.values(params).forEach(normalizeFieldDef); return params; } // 剔除 isForm === false(只读)字段:不在节点表单中显示,且不随工作流保存。 // 级联规则:容器若内部无可编辑子级则无意义,一并剔除 // 1. 字段自身 isForm === false → 整体剔除 // 2. object:剔除子字段后 attrs 为空(无可编辑子字段)→ object 一并剔除 // 3. array:过滤「子字段全部只读」的元素模板;无可编辑模板 → array 一并剔除 // 4. array 已添加元素内部被清空 → 一并移除 export function stripReadonlyFields(params: any): any { if (!params || typeof params !== 'object' || Array.isArray(params)) return params; for (const key of Object.keys(params)) { const def = params[key]; if (!def || typeof def !== 'object') continue; // 自身标记为只读 → 整体删除 if (def.isForm === false) { delete params[key]; continue; } if (def.type === 'object') { const attrs = def.attrs; if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) { stripReadonlyFields(attrs); // 子字段全部只读 → object 无意义,一并剔除 if (Object.keys(attrs).length === 0) { delete params[key]; } } } else if (def.type === 'array') { let hasEditable = false; // 单一元素模板(attrs 为 object) if (def.attrs && typeof def.attrs === 'object' && !Array.isArray(def.attrs)) { stripReadonlyFields(def.attrs); if (Object.keys(def.attrs).length > 0) hasEditable = true; } // 多元素模板:优先 enumValues(与 ModelField.arrayTemplates 一致),其次 attrs 数组;过滤子字段全部只读的空模板 const isEnumValues = Array.isArray(def.enumValues) && def.enumValues.length > 0; const isAttrsArray = Array.isArray(def.attrs) && def.attrs.length > 0; const templates = isEnumValues ? def.enumValues : isAttrsArray ? def.attrs : []; const keptTemplates = templates.filter((t: any) => { if (t && typeof t === 'object' && t.attrs && typeof t.attrs === 'object' && !Array.isArray(t.attrs)) { stripReadonlyFields(t.attrs); return Object.keys(t.attrs).length > 0; } return true; }); if (isEnumValues) def.enumValues = keptTemplates; if (isAttrsArray) def.attrs = keptTemplates; if (keptTemplates.length > 0) hasEditable = true; // 已添加元素内部清理后为空的 → 一并移除 if (Array.isArray(def.value)) { def.value.forEach((it: any) => { if (it && typeof it === 'object' && it.attrs && typeof it.attrs === 'object' && !Array.isArray(it.attrs)) { stripReadonlyFields(it.attrs); } }); def.value = def.value.filter((it: any) => { if (it && typeof it === 'object' && it.attrs && typeof it.attrs === 'object' && !Array.isArray(it.attrs)) { return Object.keys(it.attrs).length > 0; } return true; }); } // 无可编辑内容 → array 无意义,一并剔除 if (!hasEditable) { delete params[key]; } } } return params; } // 剔除 array 类型字段的实例值(value):后端模型定义中 array 仅保存结构/模板 // (enumValues / attrs),前端渲染时从模板复制并填充的 value 实例是冗余数据, // 保存工作流时不提交。递归处理嵌套结构(array 字段可嵌套于 enumValues 模板中, // 如 messages.enumValues[0].attrs.content 仍是 array,同样需删除 value)。 export function removeArrayValueInstances(params: any): any { if (!params || typeof params !== 'object' || Array.isArray(params)) return params; // 单个字段定义(带 type):array 删实例值并递归模板;object 递归 attrs if (typeof params.type === 'string') { if (params.type === 'array') { delete params.value; if (params.attrs && typeof params.attrs === 'object') { if (Array.isArray(params.attrs)) params.attrs.forEach((it: any) => removeArrayValueInstances(it)); else removeArrayValueInstances(params.attrs); } if (Array.isArray(params.enumValues)) params.enumValues.forEach((tpl: any) => removeArrayValueInstances(tpl)); } else if (params.type === 'object' && params.attrs && typeof params.attrs === 'object' && !Array.isArray(params.attrs)) { removeArrayValueInstances(params.attrs); } return params; } // 字段定义容器 { key: fieldDef }:逐个字段递归 for (const key of Object.keys(params)) { removeArrayValueInstances(params[key]); } return params; } // ===== 暴露清单:在工作流表单中展示的勾选字段 ===== // array 的模板元素列表(与 ModelField.arrayTemplates 优先级一致): // enumValues 数组 → attrs 数组 → 单个 attrs 对象。勾选/值都跟随模板,故 path 用 // enumValues[i] 定位模板,collect 与 resolve 共用本函数保持文法一致。 function arrayTemplatesOf(def: any): any[] { if (!def || typeof def !== 'object') return []; if (Array.isArray(def.enumValues) && def.enumValues.length) return def.enumValues; if (Array.isArray(def.attrs) && def.attrs.length) return def.attrs; if (def.attrs && typeof def.attrs === 'object' && !Array.isArray(def.attrs)) return [{ type: 'object', attrs: def.attrs }]; return []; } // 单个暴露叶子字段(路径带模板索引,如 "messages.enumValues[0].attrs.content") export interface ExposedField { path: string; label: string; fieldType: string; type: string; required: boolean; options?: any[]; value?: any; defaultValue?: any; // 默认值:首页初始化/回显用 fieldConstraint?: any; // 字段约束:上传格式/大小/数量、数字 min/max 等 valueSource?: any; // 引用上游节点输出({ nodeId, field });有则首页只读展示 multiple?: boolean; // 多文件上传标记 refNodeId?: string; // 预留:引用其他节点功能(后续启用) } // 收集 runtimeShow === true 的叶子字段,生成暴露清单 // 路径文法(与 restoreRuntimeShow 共用): // object 子字段 → 父路径 + ".attrs." + 子key // array 模板元素 → 父路径 + ".enumValues[i]" // 原始值数组元素(非 object 包装)不可勾选,跳过 export function collectExposedFields(params: any, prefix = ''): ExposedField[] { if (!params || typeof params !== 'object') return []; const result: ExposedField[] = []; for (const key of Object.keys(params)) { const def = params[key]; if (!def || typeof def !== 'object') continue; const path = prefix ? `${prefix}.${key}` : key; const t = def.type; if (t === 'object') { const attrs = def.attrs; if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) { result.push(...collectExposedFields(attrs, `${path}.attrs`)); } } else if (t === 'array') { // 勾选跟随模板:渲染时 value 元素与模板共享引用,runtimeShow 落在模板上, // 故遍历模板(enumValues 优先)收集,path 用 enumValues[i] 定位模板 const templates = arrayTemplatesOf(def); templates.forEach((tpl: any, i: number) => { if (tpl && typeof tpl === 'object' && tpl.attrs && typeof tpl.attrs === 'object' && !Array.isArray(tpl.attrs)) { result.push(...collectExposedFields(tpl.attrs, `${path}.enumValues[${i}].attrs`)); } }); } else if (def.runtimeShow === true) { // 叶子且已勾选 result.push({ path, label: def.label || key, fieldType: def.fieldType || 'string', type: t || 'string', required: !!def.required, options: Array.isArray(def.options) ? def.options : undefined, 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, multiple: def.multiple || def.fieldType === 'uploadMultiple' || undefined, }); } } return result; } // 按暴露清单的 path 反解,把对应叶子 def 的 runtimeShow 置 true(加载时还原勾选) export function restoreRuntimeShow(params: any, fields: ExposedField[] | null | undefined): void { if (!params || typeof params !== 'object' || !Array.isArray(fields) || fields.length === 0) return; for (const f of fields) { if (!f || typeof f.path !== 'string') continue; const cur = resolvePath(params, f.path); if (cur && typeof cur === 'object' && !Array.isArray(cur)) { cur.runtimeShow = true; } } } // 按路径走回 params 树;解析失败(schema 变更导致字段缺失)返回 undefined function resolvePath(params: any, path: string): any { if (!path) return undefined; const segments = path.split('.'); let cur = params; for (const seg of segments) { if (cur === undefined || cur === null || typeof cur !== 'object' || Array.isArray(cur)) return undefined; if (seg === 'attrs') { cur = cur.attrs; if (!cur || typeof cur !== 'object') return undefined; } else { const mv = seg.match(/^value\[(\d+)\]$/); if (mv) { const idx = Number(mv[1]); if (!Array.isArray(cur.value) || idx >= cur.value.length) return undefined; cur = cur.value[idx]; continue; } const me = seg.match(/^enumValues\[(\d+)\]$/); if (me) { const idx = Number(me[1]); const templates = arrayTemplatesOf(cur); if (idx >= templates.length) return undefined; cur = templates[idx]; continue; } cur = cur[seg]; } } return cur; } // 深度比较(JSON 序列化方式),供 index.vue 的"仅变更才写"守卫使用 export function isEqual(a: any, b: any): boolean { return JSON.stringify(a ?? null) === JSON.stringify(b ?? null); }