首页工作流:输出字段中文展示 + 引用面板滚动修复

- valueSource 契约统一为 { nodeId, field },替换 modelParamUtils 中 fieldName 旧用法
- 数组模板压缩时记录 __originIndex,叶子字段扁平清单按后端原始索引产出 enumValues
- 模型节点输出项保存时用 responseMapping 中文填充 value,引用展示显示中文
- http 节点输出字段识别 JsonEditor 包裹结构,修正真实数据路径并提取中文 label
- 引用上级节点输出面板超高时内部滚动,避免超出视口被截掉

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-19 17:07:21 +08:00
co-authored by Claude
parent c8f457dada
commit f7b3531392
5 changed files with 235 additions and 42 deletions
@@ -102,6 +102,15 @@ export function stripReadonlyFields(params: any): any {
}
return true;
});
// 有模板被剔除(子字段全只读)→ 数组索引被压缩;给保留的模板补记原始位置 __originIndex
// 供 buildModelRequestParamsPath 生成后端契约的 enumValues 原始索引(幂等:已带则不覆盖)
if (keptTemplates.length !== templates.length) {
templates.forEach((t: any, originIdx: number) => {
if (t && typeof t === 'object' && keptTemplates.includes(t) && t.__originIndex === undefined) {
t.__originIndex = originIdx;
}
});
}
if (isEnumValues) def.enumValues = keptTemplates;
if (isAttrsArray) def.attrs = keptTemplates;
if (keptTemplates.length > 0) hasEditable = true;
@@ -231,6 +240,73 @@ export function collectExposedFields(params: any, prefix = ''): ExposedField[] {
return result;
}
// ===== 模型请求参数扁平清单(modelRequestParamsPath=====
// 后端契约:保存时与 modelRequestParams 平级新增 modelRequestParamsPath
// 为嵌套模型参数树的全部叶子字段扁平清单(path 文法与 collectExposedFields 一致),
// 后端按 path 重组请求参数。value 保留当前值;valueSource 规则:
// - 已有引用配置(用户在设计器配置「引用上游」)→ 原样保留 { nodeId, field }
// - 无引用但勾选「表单展示」(runtimeShow=true)→ 补 { nodeId: 开始节点id, field: path }
// - 其余 → 不带 valueSource
export interface ModelRequestParamsPathItem {
path: string;
type: string;
required: boolean;
value: any;
valueSource?: any;
}
// 收集嵌套树的全部叶子字段(含数组模板元素,不过滤 runtimeShow),返回 { path, def }
function collectAllLeafFields(params: any, prefix = ''): { path: string; def: any }[] {
if (!params || typeof params !== 'object' || Array.isArray(params)) return [];
const result: { path: string; def: any }[] = [];
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(...collectAllLeafFields(attrs, `${path}.attrs`));
}
} else if (t === 'array') {
// 数组字段遍历模板(enumValues 优先,其次 attrs 数组),path 用 enumValues[i] 定位模板,
// 与 ModelField 渲染 / collectExposedFields 文法一致(值/勾选均落在模板上,不依赖 value 实例)。
// 模板带 __originIndexstrip 时记录的原始位置)时用原始索引,保证与后端模型完整定义的 enumValues 编号一致
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)) {
const originIdx = typeof tpl.__originIndex === 'number' ? tpl.__originIndex : i;
result.push(...collectAllLeafFields(tpl.attrs, `${path}.enumValues[${originIdx}].attrs`));
}
});
} else {
result.push({ path, def });
}
}
return result;
}
// 生成 modelRequestParamsPath 扁平清单
export function buildModelRequestParamsPath(params: any, startNodeId = ''): ModelRequestParamsPathItem[] {
const fields = collectAllLeafFields(params);
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;
return {
path,
type: def.type || 'string',
required: Boolean(def.required),
value: def.value ?? '',
...(valueSource ? { valueSource } : {}),
};
});
}
// 按暴露清单的 path 反解,把对应叶子 def 的 runtimeShow 置 true(加载时还原勾选)
// startNodeId:勾选「表单展示」的字段保存时带 valueSource 指向开始节点(后端契约);
// 回显时该 valueSource 无编辑器引用语义,清除避免 ModelField 误判引用/误删
@@ -248,7 +324,7 @@ export function restoreRuntimeShow(params: any, fields: ExposedField[] | null |
}
}
// 勾选「表单展示」的字段:保存前按后端契约补 valueSource={nodeId:开始节点id, fieldName:path}
// 勾选「表单展示」的字段:保存前按后端契约补 valueSource={nodeId:开始节点id, field:path}
// 标记字段值来自主工作流开始节点(首页表单)。startNodeId 为空(无开始节点)时跳过。
export function attachFormFieldValueSources(params: any, fields: ExposedField[] | null | undefined, startNodeId = ''): void {
if (!params || typeof params !== 'object' || !Array.isArray(fields) || fields.length === 0 || !startNodeId) return;
@@ -256,7 +332,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, fieldName: f.path };
cur.valueSource = { nodeId: startNodeId, field: f.path };
}
}
}
@@ -297,3 +373,53 @@ function resolvePath(params: any, path: string): any {
export function isEqual(a: any, b: any): boolean {
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
}
// ===== 模型返回参数中文 label(引用展示用)=====
// 模型 responseMapping(响应映射:路径→中文)支持两种结构:普通对象 {路径: 中文} 与
// { type, attrs/value, label } 包裹格式。独立实现平铺(不依赖 settings/modelConfigV2),
// 供保存 modelResponseBodyMapping 时把中文描述填入 value,引用上级节点输出展示中文而非英文 key。
function flattenResponseLabelMap(mapping: any, basePath = '', out: Record<string, string>): void {
if (!mapping || typeof mapping !== 'object') return;
// { type, attrs/value } 包裹格式:叶子节点带 label
if (typeof mapping.type === 'string' && ['string', 'number', 'boolean', 'null', 'object', 'array'].includes(mapping.type)) {
const jtype = mapping.type;
const dataKey = jtype === 'object' || jtype === 'array' ? 'attrs' : 'value';
if (jtype === 'object' && dataKey in mapping && mapping[dataKey] && typeof mapping[dataKey] === 'object' && !Array.isArray(mapping[dataKey])) {
for (const [key, val] of Object.entries(mapping[dataKey])) {
flattenResponseLabelMap(val, basePath ? `${basePath}.attrs.${key}` : key, out);
}
} else if (jtype === 'array' && dataKey in mapping && Array.isArray(mapping[dataKey])) {
mapping[dataKey].forEach((item: any, idx: number) => {
flattenResponseLabelMap(item, `${basePath}.attrs[${idx}]`, out);
});
} else if (['string', 'number', 'boolean', 'null'].includes(jtype)) {
if (mapping.label) out[basePath] = String(mapping.label);
}
return;
}
// 普通对象(非包裹格式)
if (!Array.isArray(mapping)) {
for (const [key, val] of Object.entries(mapping)) {
const childPath = basePath ? `${basePath}.${key}` : key;
if (val && typeof val === 'object' && !Array.isArray(val)) {
flattenResponseLabelMap(val, childPath, out);
} else if (typeof val === 'string' && val) {
out[childPath] = val;
}
}
}
}
// 用模型 responseMapping 的中文描述填充 modelResponseBodyMapping 的 value
// { key: '' } → { key: '中文' }。结构不变(对象 key→string),无对应中文时保留原 value(兜底 key)。
export function enrichResponseBodyMapping(body: any, mapping: any): any {
if (!body || typeof body !== 'object' || Array.isArray(body)) return body ?? null;
const labelMap: Record<string, string> = {};
flattenResponseLabelMap(mapping, '', labelMap);
const out: Record<string, string> = {};
for (const k of Object.keys(body)) {
const raw = typeof body[k] === 'string' ? body[k] : '';
out[k] = labelMap[k] || raw || k;
}
return out;
}