lan-low-code-cli 0.0.0-stage → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GLOBAL_ACTION_EVENTS = exports.ACTION_GLOBAL_TARGET = void 0;
4
+ exports.resolveLocales = resolveLocales;
5
+ exports.completeOptionsI18n = completeOptionsI18n;
6
+ exports.buildViewModel = buildViewModel;
7
+ exports.applyControlActions = applyControlActions;
8
+ exports.buildFdConfig = buildFdConfig;
9
+ exports.createDataField = createDataField;
10
+ exports.parseFieldSpec = parseFieldSpec;
11
+ exports.buildFormDefinePayload = buildFormDefinePayload;
12
+ const constants_1 = require("../constants");
13
+ const locale_1 = require("../utils/locale");
14
+ const random_1 = require("../utils/random");
15
+ const widgets_1 = require("../widgets");
16
+ /** `$global`:动作目标为表单全局(onLoad / onBeforeSubmit / onAfterSubmit) */
17
+ exports.ACTION_GLOBAL_TARGET = '$global';
18
+ /** 全局动作支持的(前端注册的)事件类型 */
19
+ exports.GLOBAL_ACTION_EVENTS = ['onLoad', 'onBeforeSubmit', 'onAfterSubmit'];
20
+ /** 字段级多语言的 prop 集合(沿用旧视图时这些条目由生成器重建,不从旧 lang 携带) */
21
+ const FIELD_LANG_PROPS = new Set(['label', 'placeholder', 'option']);
22
+ /**
23
+ * 校验并规范化已有 viewModel。
24
+ *
25
+ * 合法性:视图 fdConfig 可解析、含 `view.render.desktop` 渲染树与 `view.controlMappings`,
26
+ * 且无 type 的节点是「纯 key 引用」形态(老版本 CLI 生成的整节点拷贝会重建而不是沿用)。
27
+ */
28
+ function normalizePrevViewModel(prevViewModel) {
29
+ if (!Array.isArray(prevViewModel) || prevViewModel.length === 0)
30
+ return undefined;
31
+ const viewConfig = (0, random_1.safeJsonParse)(prevViewModel[0]?.fdConfig, {});
32
+ const desktop = viewConfig?.view?.render?.desktop;
33
+ if (!Array.isArray(desktop) || !viewConfig?.view?.controlMappings)
34
+ return undefined;
35
+ const referencedKeys = new Set();
36
+ let valid = true;
37
+ const walk = (nodes) => {
38
+ for (const node of nodes) {
39
+ if (!node || typeof node !== 'object')
40
+ continue;
41
+ if (node.type === undefined) {
42
+ // 引用节点必须是 { key, children? } 纯引用
43
+ if (typeof node.key !== 'string' || node.controlProps !== undefined || node.kind !== undefined) {
44
+ valid = false;
45
+ return;
46
+ }
47
+ referencedKeys.add(node.key);
48
+ }
49
+ if (Array.isArray(node.children))
50
+ walk(node.children);
51
+ }
52
+ };
53
+ walk(desktop);
54
+ if (!valid || referencedKeys.size === 0)
55
+ return undefined;
56
+ return { viewModel: prevViewModel, referencedKeys };
57
+ }
58
+ /** 找出未包含在已有布局中的字段(新增字段) */
59
+ function collectUnreferencedFields(referencedKeys, built, details) {
60
+ const missing = [];
61
+ for (const item of built) {
62
+ if (!referencedKeys.has(item.field.uuid))
63
+ missing.push(item.field.fdName);
64
+ }
65
+ for (const detail of details) {
66
+ if (!referencedKeys.has(detail.model.fdTableName))
67
+ missing.push(detail.model.fdTableName);
68
+ }
69
+ return missing;
70
+ }
71
+ /** 控件配置面板默认值中,不应写入 controlProps 的键(属于样式/布局,存放在 controlStyle) */
72
+ const DEFAULT_EXCLUDE_KEYS = new Set([
73
+ 'layout',
74
+ 'labelStyle',
75
+ 'controlValueStyle',
76
+ 'desktopTitleBgColor',
77
+ 'desktopPaddingMode',
78
+ 'titleStyle',
79
+ 'color',
80
+ 'paddingControl',
81
+ 'controlTitleStyle',
82
+ 'controlText',
83
+ ]);
84
+ function controlMapOf(options) {
85
+ return { ...constants_1.DEFAULT_CONTROL_MAP, ...(options.controlMap ?? {}) };
86
+ }
87
+ /**
88
+ * 归一化多语言列表:拆逗号、去重、大小写归一到前端 `getLocale` 的产物(`Cn`/`Us`/`Tw`),
89
+ * 并保证至少有一个主语言(列表首项)。
90
+ */
91
+ function resolveLocales(locale) {
92
+ const raw = Array.isArray(locale) ? locale : [String(locale ?? '')];
93
+ const list = raw
94
+ .flatMap((item) => String(item).split(','))
95
+ .map((item) => item.trim())
96
+ .filter(Boolean)
97
+ .map(locale_1.normalizeLocale);
98
+ if (list.length === 0)
99
+ return [constants_1.DEFAULT_LOCALE];
100
+ const result = [];
101
+ for (const item of list) {
102
+ if (!result.includes(item))
103
+ result.push(item);
104
+ }
105
+ return result;
106
+ }
107
+ /**
108
+ * 组装 `lang[messageKey].content`。
109
+ *
110
+ * 主语言取真实文案;其余语言的取值优先级:
111
+ * 1. `--lang-file` 用户翻译(按标题、再按字段物理名匹配)
112
+ * 2. 控件自带译文(`elementLangs`,如英文 placeholder「Please enter」)
113
+ * 3. 与主语言写同一份文案
114
+ *
115
+ * 第 3 条是兜底:前端解析不到当前语言时会回落 `Cn`,此处保证不漏项,
116
+ * 与前端 `ide-model/business/input/inputLocale.tsx` 的 placeholder 兜底行为一致。
117
+ */
118
+ function buildLangContent(locales, text, alias, messages, elementLangs) {
119
+ const primary = locales[0];
120
+ const content = { [primary]: text };
121
+ for (const locale of locales) {
122
+ if (locale === primary)
123
+ continue;
124
+ const dict = messages?.[locale];
125
+ content[locale] =
126
+ dict?.[text] ?? (alias ? dict?.[alias] : undefined) ?? elementLangs?.[locale] ?? text;
127
+ }
128
+ return content;
129
+ }
130
+ /** 选项是否为「平台静态多语言」(数据字典等),由静态资源解析,不归 lang 管理 */
131
+ function isStaticOption(option) {
132
+ const messageKey = option?.messageKey;
133
+ return typeof messageKey === 'string' && messageKey.length > 0 && !messageKey.startsWith('!{');
134
+ }
135
+ /** 取选项中已有的自定义多语言 key(`!{...}`) */
136
+ function findOptionKey(...options) {
137
+ for (const option of options) {
138
+ const item = option;
139
+ for (const value of [item?.messageKey, item?.label]) {
140
+ if (typeof value === 'string' && value.startsWith('!{'))
141
+ return value;
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+ /** 取选项的真实文案(排除多语言 key 与静态标记) */
147
+ function findOptionText(...options) {
148
+ for (const option of options) {
149
+ const label = option?.label;
150
+ if (typeof label === 'string' && label && !label.startsWith('!{') && !label.startsWith('${')) {
151
+ return label;
152
+ }
153
+ if (label && typeof label === 'object' && typeof label.default === 'string') {
154
+ return label.default;
155
+ }
156
+ }
157
+ return undefined;
158
+ }
159
+ /** 从已有 lang 中取某个 key 的主语言文案 */
160
+ function resolveLangText(key, lang, primary) {
161
+ if (!key)
162
+ return undefined;
163
+ const content = lang[key]?.content;
164
+ if (!content)
165
+ return undefined;
166
+ return content[primary] ?? content[constants_1.DEFAULT_LOCALE] ?? Object.values(content)[0];
167
+ }
168
+ /**
169
+ * 完善下拉 / 单选 / 多选控件的**选项多语言**(`EI18nProp.option`)。
170
+ *
171
+ * 对应前端 `genFormByDataModel/lang/index.ts#initOptionsI18n` + `dataModel#setOptionsToDataField`,
172
+ * 落库结构(已用真实环境数据核对):
173
+ *
174
+ * ```
175
+ * lang["!{xxx}"] = { prop:'option', name:<fdName>, type:'input', content:{ Cn:'选项1' } }
176
+ * fdAttribute...controlProps.options[] = { label:'!{xxx}', value:'1' }
177
+ * fdFontExtendData.options[] = { messageKey:'!{xxx}', label:'选项1', value:'1', ... }
178
+ * ```
179
+ *
180
+ * ⚠️ 数据字典选项(`messageKey` 形如 `${sys-xform:enum.X}` / `sys-xform:enum.X`)走平台静态多语言,
181
+ * **跳过不改**,否则会污染 lang。
182
+ *
183
+ * @returns 实际完善的选项数量
184
+ */
185
+ function completeOptionsI18n(field, lang, locales, messages) {
186
+ const attr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
187
+ const controlProps = attr?.config
188
+ ?.controlProps;
189
+ const ctrlOptions = Array.isArray(controlProps?.options)
190
+ ? controlProps.options
191
+ : undefined;
192
+ const fontExtend = field.fdFontExtendData
193
+ ? (0, random_1.safeJsonParse)(field.fdFontExtendData, {})
194
+ : {};
195
+ const fontOptions = Array.isArray(fontExtend.options) ? fontExtend.options : undefined;
196
+ if (!ctrlOptions && !fontOptions)
197
+ return 0;
198
+ // 以 fdFontExtendData.options 为准(它保存了真实文案),controlProps 仅同步 key
199
+ const length = Math.max(ctrlOptions?.length ?? 0, fontOptions?.length ?? 0);
200
+ const primary = locales[0];
201
+ let count = 0;
202
+ for (let index = 0; index < length; index++) {
203
+ const fontOption = fontOptions?.[index];
204
+ const ctrlOption = ctrlOptions?.[index];
205
+ // 数据字典 / 静态多语言选项:不改
206
+ if (isStaticOption(fontOption) || isStaticOption(ctrlOption))
207
+ continue;
208
+ const existingKey = findOptionKey(fontOption, ctrlOption);
209
+ const value = fontOption?.value ?? ctrlOption?.value;
210
+ const text = findOptionText(fontOption, ctrlOption) ??
211
+ resolveLangText(existingKey, lang, primary) ??
212
+ (value === undefined ? undefined : String(value));
213
+ if (!text)
214
+ continue;
215
+ const key = (0, random_1.isMessageKey)(existingKey) ? existingKey : (0, random_1.generateMessageKey)();
216
+ // 选项文案允许重复,额外支持 `字段名#选项值` 精确匹配
217
+ const alias = value === undefined ? undefined : `${field.fdName}#${value}`;
218
+ lang[key] = {
219
+ prop: 'option',
220
+ name: field.fdName,
221
+ type: 'input',
222
+ content: buildLangContent(locales, text, alias, messages),
223
+ };
224
+ if (ctrlOption)
225
+ ctrlOption.label = key;
226
+ if (fontOption) {
227
+ fontOption.messageKey = key;
228
+ fontOption.label = text;
229
+ }
230
+ count++;
231
+ }
232
+ if (ctrlOptions && count)
233
+ field.fdAttribute = JSON.stringify(attr);
234
+ if (fontOptions && count)
235
+ field.fdFontExtendData = JSON.stringify(fontExtend);
236
+ return count;
237
+ }
238
+ /** 依据数据项类型/逻辑类型解析控件包与元数据 */
239
+ async function resolveControl(field, options) {
240
+ const map = controlMapOf(options);
241
+ // 优先按控件逻辑类型(xform.type)匹配,避免把「金额/评分/下拉」等
242
+ // 错误降级成同数据项类型的默认控件(如 number -> 数字)
243
+ let entry = constants_1.CONTROL_TYPE_MAP[field.fdType] ?? map[field.fdType];
244
+ if (!entry)
245
+ entry = map[field.fdDataType];
246
+ if (!entry)
247
+ entry = Object.values(map).find((item) => item.logicalType === field.fdType);
248
+ entry = entry ?? constants_1.FALLBACK_CONTROL;
249
+ const registry = options.registry;
250
+ const [meta, metaMobile] = registry
251
+ ? await Promise.all([registry.get(entry.desktop), registry.get(entry.mobile)])
252
+ : [null, null];
253
+ return {
254
+ desktopPkg: entry.desktop,
255
+ mobilePkg: entry.mobile,
256
+ logicalType: meta?.xform?.type || entry.logicalType,
257
+ meta,
258
+ metaMobile,
259
+ };
260
+ }
261
+ /** 从 fdFontExtendData 提取需要注入 controlProps 的前端扩展属性 */
262
+ function pickFontExtend(data) {
263
+ const picked = {};
264
+ ['passValue', 'trace', 'encrypt', 'defaultValueType', 'relationControl'].forEach((key) => {
265
+ if (data[key] !== undefined)
266
+ picked[key] = data[key];
267
+ });
268
+ return picked;
269
+ }
270
+ /** 解析字段的 fdFontExtendData */
271
+ function fontExtendOf(field) {
272
+ return pickFontExtend((0, random_1.safeJsonParse)(field.fdFontExtendData, {}));
273
+ }
274
+ /**
275
+ * 生成数据项的 fdAttribute。
276
+ *
277
+ * 对应前端 `configJson.ts#completeFdAttribute` + `generateConfigByDataField`:
278
+ * { uuid, env:['xform'], config: { type, kind, key, label, controlProps, labelProps } }
279
+ *
280
+ * 该结构是前端加载表单时的**唯一控件来源**(`cacheConfig[uuid] = config`),
281
+ * 缺失或结构不符会导致控件无法渲染。
282
+ */
283
+ function buildFieldAttribute(params) {
284
+ const { fieldUuid, langKey, fieldName, tableName, tableType, fdLength, span, resolved } = params;
285
+ const key = (0, random_1.generateCmptKey)(resolved.desktopPkg);
286
+ const logicalType = resolved.logicalType;
287
+ const xform = resolved.meta?.xform ?? {};
288
+ // 控件 config.json 中声明的默认值
289
+ const defaults = (0, widgets_1.collectWidgetDefaults)(resolved.meta);
290
+ const controlDefaults = {};
291
+ Object.keys(defaults).forEach((k) => {
292
+ if (!DEFAULT_EXCLUDE_KEYS.has(k))
293
+ controlDefaults[k] = defaults[k];
294
+ });
295
+ const showText = defaults.showText;
296
+ const controlProps = {
297
+ ...controlDefaults,
298
+ // 必填属性兜底默认值(config.json 声明 required 但无 default 的项)
299
+ ...constants_1.REQUIRED_PROP_DEFAULTS[resolved.desktopPkg],
300
+ ...params.fontExtend,
301
+ // 保留已有选项:本函数会重建 controlProps,不显式带回会丢失用户配置的 options
302
+ ...(params.options && params.options.length ? { options: params.options } : {}),
303
+ ...(params.placeholderKey ? { placeholder: params.placeholderKey } : {}),
304
+ id: key,
305
+ desktop: { type: resolved.desktopPkg },
306
+ mobile: { type: resolved.mobilePkg },
307
+ name: fieldName,
308
+ uuid: fieldUuid,
309
+ title: langKey,
310
+ span: xform.span || span,
311
+ $$tableType: tableType,
312
+ $$tableName: tableName,
313
+ };
314
+ if (typeof fdLength === 'number' && fdLength > 0) {
315
+ controlProps.maxLength = fdLength;
316
+ }
317
+ const labelProps = {
318
+ desktop: {},
319
+ mobile: {},
320
+ title: langKey,
321
+ // 控件布局:左右布局(前端 LayoutType.horizontal)
322
+ layout: 'horizontal',
323
+ // 左右布局下的标题对齐(前端 LabelTextAlign.left)
324
+ labelTextAlign: 'left',
325
+ ...(showText !== undefined ? { showText } : {}),
326
+ ...(params.fontExtend.trace !== undefined ? { trace: params.fontExtend.trace } : {}),
327
+ };
328
+ const config = {
329
+ key,
330
+ type: logicalType,
331
+ kind: 'control',
332
+ label: langKey,
333
+ controlProps,
334
+ labelProps,
335
+ };
336
+ return {
337
+ attribute: JSON.stringify({
338
+ uuid: fieldUuid,
339
+ env: resolved.meta?.env ?? ['xform'],
340
+ config,
341
+ }),
342
+ config,
343
+ };
344
+ }
345
+ /** 补全数据项属性(对齐前端 completeFieldAttribute / completeFdAttribute) */
346
+ function enrichField(field, model, resolved) {
347
+ const xform = resolved.meta?.xform ?? {};
348
+ field.uuid = field.fdName;
349
+ field.fdType = resolved.logicalType;
350
+ if (xform.dataType)
351
+ field.fdDataType = xform.dataType;
352
+ if (xform.dictType)
353
+ field.fdDictType = xform.dictType;
354
+ if (typeof field.fdLength !== 'number' || Number.isNaN(field.fdLength)) {
355
+ field.fdLength = xform.defaultLength ?? 0;
356
+ }
357
+ field.fdTableName = model.fdTableName;
358
+ field.fdTableType = model.fdType;
359
+ field.fdDataModel = { fdId: model.fdId, fdName: model.fdName };
360
+ field.fdIsIndex = field.fdIsIndex ?? false;
361
+ field.fdDisplay = field.fdDisplay ?? true;
362
+ field.fdIsStored = field.fdIsStored ?? true;
363
+ field.fdState = field.fdState ?? 'effective';
364
+ if (field.fdIsSystem) {
365
+ field.$$fdLockMode = 'part';
366
+ field.fdLockMode = 'part';
367
+ }
368
+ else {
369
+ field.$$fdLockMode = 'none';
370
+ field.fdLockMode = 'none';
371
+ field.fdFontExtendData =
372
+ field.fdFontExtendData ??
373
+ JSON.stringify({
374
+ passValue: false,
375
+ trace: false,
376
+ encrypt: false,
377
+ // 初始值模式:config.json 默认 'null'(无初始值)。不能写 'formula'——
378
+ // 公式模式下 defaultValueFormulaVO 必填,设计器提交会被拦截
379
+ defaultValueType: 'null',
380
+ encryptDefinition: {},
381
+ });
382
+ }
383
+ if (!field.fdIsSystem && (field.fdOrder === undefined || field.fdOrder === null)) {
384
+ field.fdOrder = 1;
385
+ }
386
+ }
387
+ /** 依据数据项生成(或补全)控件配置,并回写 fdAttribute / fdLabelLangKey */
388
+ async function buildField(field, model, options, lang, locales) {
389
+ const resolved = await resolveControl(field, options);
390
+ const span = options.span ?? constants_1.DEFAULT_SPAN;
391
+ enrichField(field, model, resolved);
392
+ // 仅复用「前端可识别」的 key(!{仅含 [\w.]});其余(含历史 RFC4122 UUID)一律重建
393
+ const langKey = (0, random_1.isMessageKey)(field.fdLabelLangKey)
394
+ ? field.fdLabelLangKey
395
+ : (0, random_1.generateMessageKey)();
396
+ field.fdLabelLangKey = langKey;
397
+ lang[langKey] = {
398
+ prop: 'label',
399
+ name: field.fdName,
400
+ type: 'input',
401
+ content: buildLangContent(locales, field.fdLabel || field.fdName, field.fdName, options.messages),
402
+ };
403
+ // 控件占位提示多语言(约定 key:{pkg}###langs###placeholder)
404
+ let placeholderKey;
405
+ const registry = options.registry;
406
+ if (registry && (0, widgets_1.hasConfigItem)(resolved.meta, 'placeholder')) {
407
+ const langs = await registry.getElementLangs((0, widgets_1.elementLangKey)(resolved.desktopPkg, 'placeholder'));
408
+ // 控件自带的多语言(zh-cn/en-us/...),用于填充非主语言的真实译文
409
+ const elementLangs = langs ? (0, locale_1.normalizeLangMap)(langs) : undefined;
410
+ const value = elementLangs?.[locales[0]] ??
411
+ elementLangs?.[constants_1.DEFAULT_LOCALE] ??
412
+ (elementLangs ? Object.values(elementLangs)[0] : undefined);
413
+ if (value) {
414
+ placeholderKey = (0, random_1.generateMessageKey)();
415
+ lang[placeholderKey] = {
416
+ prop: 'placeholder',
417
+ name: field.fdName,
418
+ type: 'input',
419
+ content: buildLangContent(locales, value, undefined, options.messages, elementLangs),
420
+ };
421
+ }
422
+ }
423
+ // 新建选项(--option):写入 fdFontExtendData.options,作为选项的唯一来源
424
+ const specified = options.options?.[field.fdName];
425
+ if (specified?.length) {
426
+ const fontExtend = field.fdFontExtendData
427
+ ? (0, random_1.safeJsonParse)(field.fdFontExtendData, {})
428
+ : {};
429
+ // 结构对齐真实表单(拆分线等价物 colorSwitch / type 保留默认值)
430
+ fontExtend.options = specified.map((item, index) => ({
431
+ messageKey: '',
432
+ colorSwitch: false,
433
+ label: item.label,
434
+ type: '',
435
+ value: item.value ?? String(index + 1),
436
+ }));
437
+ field.fdFontExtendData = JSON.stringify(fontExtend);
438
+ }
439
+ // 保留已有的控件选项(数据项/设计器中配置),否则重建 attribute 会丢
440
+ const prevAttr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
441
+ const attrOptions = prevAttr?.config?.controlProps?.options;
442
+ const fontOptions = field.fdFontExtendData
443
+ ? ((0, random_1.safeJsonParse)(field.fdFontExtendData, {}).options ?? [])
444
+ : [];
445
+ // 已有 controlProps.options 原样保留(数据字典选项带有 messageKey,不能丢字段);
446
+ // 新建选项时归一为 { label, value },与真实表单的 controlProps 结构一致
447
+ const prevOptions = Array.isArray(attrOptions) && !specified?.length
448
+ ? attrOptions
449
+ : fontOptions.map((item) => ({
450
+ label: item?.label,
451
+ value: item?.value,
452
+ }));
453
+ const { attribute, config } = buildFieldAttribute({
454
+ fieldUuid: field.uuid,
455
+ langKey,
456
+ fieldName: field.fdName,
457
+ tableName: model.fdTableName,
458
+ tableType: model.fdType,
459
+ fdLength: field.fdLength,
460
+ span,
461
+ resolved,
462
+ placeholderKey,
463
+ fontExtend: fontExtendOf(field),
464
+ options: prevOptions.length ? prevOptions : undefined,
465
+ });
466
+ field.fdAttribute = attribute;
467
+ // 选项多语言(下拉/单选/多选)
468
+ const optionCount = completeOptionsI18n(field, lang, locales, options.messages);
469
+ // 控件必填属性校验(对齐设计器发布前校验)
470
+ const propErrors = (0, widgets_1.validateRequiredProps)(resolved.meta, config);
471
+ return { field, model, resolved, langKey, optionCount, propErrors };
472
+ }
473
+ /** 构建虚拟行:控件节点在持久化格式中**只保留 key 引用** */
474
+ function buildRows(built, options) {
475
+ const perRow = Math.max(1, options.perRow ?? 2);
476
+ const span = options.span ?? constants_1.DEFAULT_SPAN;
477
+ const rows = [];
478
+ for (let i = 0; i < built.length; i += perRow) {
479
+ const group = built.slice(i, i + perRow);
480
+ const rowKey = (0, random_1.generateCmptKey)(constants_1.ROW);
481
+ rows.push({
482
+ key: rowKey,
483
+ type: constants_1.ROW,
484
+ kind: 'container',
485
+ controlProps: { id: rowKey, spans: group.map(() => span) },
486
+ children: group.map((item) => ({ key: item.field.uuid })),
487
+ });
488
+ }
489
+ return rows;
490
+ }
491
+ /**
492
+ * 栅格布局:`layout` → `@elem/layout-grid` → 若干 `@elem/layout-grid.GridItem`。
493
+ *
494
+ * 结构依据真实环境表单(`layout~xxxx` / `controlProps:{id}`):
495
+ * ```
496
+ * layout { controlProps:{ id } }
497
+ * └ @elem/layout-grid { controlProps:{ id, columns, rows } }
498
+ * └ GridItem { controlProps:{ id, column, row, rowSpan, columnSpan, style:{backgroundColor:''} }, children:[{key:<fieldUuid>}] }
499
+ * ```
500
+ * 注意:栅格布局控件**不进** `controlMappings`(已核对真实表单)。
501
+ */
502
+ function buildGrid(built, columns) {
503
+ const cols = Math.max(1, columns);
504
+ const rows = Math.max(1, Math.ceil(built.length / cols));
505
+ const layoutKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT);
506
+ const gridKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID);
507
+ const items = built.map((item, index) => {
508
+ const itemKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID_ITEM);
509
+ return {
510
+ key: itemKey,
511
+ type: constants_1.LAYOUT_GRID_ITEM,
512
+ kind: 'container',
513
+ controlProps: {
514
+ column: (index % cols) + 1,
515
+ row: Math.floor(index / cols) + 1,
516
+ id: itemKey,
517
+ rowSpan: 1,
518
+ columnSpan: 1,
519
+ style: { backgroundColor: '' },
520
+ },
521
+ children: [{ key: item.field.uuid }],
522
+ };
523
+ });
524
+ return [
525
+ {
526
+ key: layoutKey,
527
+ type: constants_1.LAYOUT,
528
+ kind: 'container',
529
+ controlProps: { id: layoutKey },
530
+ children: [
531
+ {
532
+ key: gridKey,
533
+ type: constants_1.LAYOUT_GRID,
534
+ kind: 'container',
535
+ controlProps: { columns: cols, rows, id: gridKey },
536
+ children: items,
537
+ },
538
+ ],
539
+ },
540
+ ];
541
+ }
542
+ /**
543
+ * 解析页签并**一次性**写入页签标题多语言。
544
+ *
545
+ * 必须在 desktop / mobile 两棵树之前调用一次,
546
+ * 否则两棵树会各自生成一套随机 key,导致 `lang` 里出现重复条目。
547
+ */
548
+ function resolveTabs(built, options, lang, locales) {
549
+ const byName = new Map(built.map((item) => [item.field.fdName, item]));
550
+ const used = new Set();
551
+ const panes = [];
552
+ for (const spec of options.tabs ?? []) {
553
+ const items = [];
554
+ for (const name of spec.fields ?? []) {
555
+ const item = byName.get(name);
556
+ if (item && !used.has(name)) {
557
+ used.add(name);
558
+ items.push(item);
559
+ }
560
+ }
561
+ panes.push({ title: spec.title, messageKey: (0, random_1.generateMessageKey)(), items });
562
+ }
563
+ // 未列入任何页签的字段 → 归入「其它」
564
+ const rest = built.filter((item) => !used.has(item.field.fdName));
565
+ if (rest.length || panes.length === 0) {
566
+ panes.push({ title: '其它', messageKey: (0, random_1.generateMessageKey)(), items: rest });
567
+ }
568
+ panes.forEach((pane) => {
569
+ const anchor = pane.items[0]?.field.fdName ?? built[0]?.field.fdName ?? '';
570
+ lang[pane.messageKey] = {
571
+ prop: constants_1.I18N_PROP_TABS,
572
+ name: anchor,
573
+ type: 'input',
574
+ // 真实数据中页签多语言额外带 default
575
+ content: {
576
+ ...buildLangContent(locales, pane.title, undefined, options.messages),
577
+ default: pane.title,
578
+ },
579
+ };
580
+ });
581
+ return panes;
582
+ }
583
+ /**
584
+ * 多页签布局:`@elem/xform-tabs` + 若干 `@elem/xform-tabpane`。
585
+ *
586
+ * 结构依据真实环境表单:
587
+ * - tabs 的 key 为 `<外观key>-tabItem`,外观 key 形如 `@elem/xform-tab-appearance~xxxxxx`
588
+ * - 渲染树中**没有**独立的 `@elem/xform-tab-appearance` 节点,仅用其 key 前缀
589
+ * - pane 的 key 为 `<外观key>-tabN`,`controlProps.parent` 指回外观
590
+ * - 页签控件**需要**写入 `controlMappings`(与栅格布局不同)
591
+ */
592
+ function buildTabsNode(panes, mainTableName, options) {
593
+ const appearanceKey = (0, random_1.generateCmptKey)(constants_1.TAB_APPEARANCE);
594
+ // 页签内层布局:rows(默认)或 grid
595
+ const buildPaneChildren = (items) => options.tabLayout === 'grid'
596
+ ? buildGrid(items, options.gridColumns ?? options.perRow ?? 2)
597
+ : buildRows(items, options);
598
+ const children = panes.map((pane, index) => {
599
+ const paneKey = `${appearanceKey}-tab${index + 1}`;
600
+ return {
601
+ key: paneKey,
602
+ type: constants_1.TAB_PANE,
603
+ kind: 'container',
604
+ controlProps: {
605
+ forceRender: true,
606
+ title: pane.title,
607
+ tab: pane.title,
608
+ tabMessageKey: pane.messageKey,
609
+ key: paneKey,
610
+ $$id: paneKey,
611
+ parent: { name: constants_1.TAB_APPEARANCE, type: 'appearance' },
612
+ children: null,
613
+ },
614
+ children: buildPaneChildren(pane.items),
615
+ };
616
+ });
617
+ return [
618
+ {
619
+ key: `${appearanceKey}-tabItem`,
620
+ type: constants_1.TABS,
621
+ kind: 'container',
622
+ controlProps: {
623
+ $$tableName: mainTableName,
624
+ name: panes[0]?.items[0]?.field.fdName,
625
+ tabBarStyle: {},
626
+ },
627
+ children,
628
+ },
629
+ ];
630
+ }
631
+ /** 明细表在渲染树中的节点:`{ key:<表名>, children:[字段引用] }`(无 type) */
632
+ function detailNodes(details) {
633
+ return details.map((detail) => ({
634
+ key: detail.model.fdTableName,
635
+ children: detail.fields.map((item) => ({ key: item.field.uuid })),
636
+ }));
637
+ }
638
+ /** 桌面端:`@elem/xform-appearance` → `main` → 布局容器(+ 明细表行) */
639
+ function buildDesktopTree(built, mainTableName, options, panes, details) {
640
+ let containers;
641
+ switch (options.layout ?? 'rows') {
642
+ case 'grid':
643
+ containers = buildGrid(built, options.gridColumns ?? options.perRow ?? 2);
644
+ break;
645
+ case 'tabs':
646
+ containers = buildTabsNode(panes, mainTableName, options);
647
+ break;
648
+ default:
649
+ containers = buildRows(built, options);
650
+ }
651
+ // 明细表独立成行,追加在布局容器之后
652
+ return [wrapAppearance(mainTableName, [...containers, ...buildDetailRows(details)])];
653
+ }
654
+ /**
655
+ * 移动端:`@elem/xform-appearance` → `main` → **平铺的字段引用**。
656
+ *
657
+ * 实测真实表单的移动端渲染树不含任何布局容器(无 row / grid / tabs),
658
+ * 字段直接作为 `main.children` 的 `{ key }` 节点;明细表节点同理追加在末尾。
659
+ */
660
+ function buildMobileTree(built, mainTableName, details) {
661
+ return [
662
+ wrapAppearance(mainTableName, [
663
+ ...built.map((item) => ({ key: item.field.uuid })),
664
+ ...detailNodes(details),
665
+ ]),
666
+ ];
667
+ }
668
+ function wrapAppearance(tableName, children) {
669
+ return {
670
+ key: constants_1.APPEARANCE,
671
+ type: constants_1.APPEARANCE,
672
+ kind: 'container',
673
+ controlProps: { $$tableName: tableName },
674
+ children: [
675
+ {
676
+ key: constants_1.MAIN,
677
+ type: constants_1.MAIN,
678
+ kind: 'container',
679
+ children,
680
+ },
681
+ ],
682
+ };
683
+ }
684
+ /**
685
+ * 构建控件映射(controlMappings):平台 -> 用到的控件元数据。
686
+ *
687
+ * 注意:栅格布局(`layout` / `@elem/layout-grid` / `GridItem`)**不写入**此处,
688
+ * 而多页签(`tab-appearance` / `tabs` / `tabpane`)**需要写入** —— 均为真实表单核对结论。
689
+ */
690
+ function buildControlMappings(built, options = {}) {
691
+ /** 布局控件的兜底元数据(控件包无 xform 时用,与真实表单的 xform.type 一致) */
692
+ const layoutDefaults = {
693
+ [constants_1.TAB_APPEARANCE]: 'tabAppearance',
694
+ [constants_1.TABS]: 'xformTabs',
695
+ [constants_1.TAB_PANE]: 'xformTabpane',
696
+ };
697
+ const build = (platform) => {
698
+ const seen = new Set();
699
+ const list = [];
700
+ built.forEach((item) => {
701
+ const pkg = platform === 'desktop' ? item.resolved.desktopPkg : item.resolved.mobilePkg;
702
+ const meta = platform === 'desktop' ? item.resolved.meta : item.resolved.metaMobile;
703
+ if (!pkg || seen.has(pkg))
704
+ return;
705
+ seen.add(pkg);
706
+ const xform = { ...(meta?.xform ?? {}), type: item.resolved.logicalType };
707
+ // 前端 handleFindXFormWidgets 会把包级 env 同步进 xform.env
708
+ if (meta?.env && !xform.env) {
709
+ ;
710
+ xform.env = meta.env;
711
+ }
712
+ list.push({ name: pkg, platform, xform });
713
+ });
714
+ if (options.layout === 'tabs') {
715
+ const pkgs = platform === 'desktop'
716
+ ? [constants_1.TAB_APPEARANCE, constants_1.TABS, constants_1.TAB_PANE]
717
+ : [constants_1.TAB_APPEARANCE_MOBILE, constants_1.TABS_MOBILE, constants_1.TAB_PANE_MOBILE];
718
+ const defaults = platform === 'desktop'
719
+ ? layoutDefaults
720
+ : {
721
+ [constants_1.TAB_APPEARANCE_MOBILE]: 'tabAppearance',
722
+ [constants_1.TABS_MOBILE]: 'xformTabs',
723
+ [constants_1.TAB_PANE_MOBILE]: 'xformTabpane',
724
+ };
725
+ pkgs.forEach((pkg) => {
726
+ if (seen.has(pkg))
727
+ return;
728
+ seen.add(pkg);
729
+ list.push({ name: pkg, platform, xform: { type: defaults[pkg] } });
730
+ });
731
+ }
732
+ return list;
733
+ };
734
+ return { desktop: build('desktop'), mobile: build('mobile') };
735
+ }
736
+ /** 构建 viewModel(场景) */
737
+ function buildViewModel(built, mainTableName, options = {}, lang = {}, locales = [constants_1.DEFAULT_LOCALE], details = [], detailMappings) {
738
+ const controlStyle = {};
739
+ built.forEach((item) => {
740
+ controlStyle[item.field.uuid] = { desktop: { ...constants_1.DEFAULT_CONTROL_STYLE } };
741
+ });
742
+ // 明细表自身需要 controlStyle 条目(明细字段不需要)——真实表单核对结论
743
+ details.forEach((detail) => {
744
+ controlStyle[detail.model.fdTableName] = { ...constants_1.DEFAULT_DETAIL_CONTROL_STYLE };
745
+ });
746
+ // 页签多语言只生成一次,供桌面端渲染树复用
747
+ const panes = options.layout === 'tabs' ? resolveTabs(built, options, lang, locales) : [];
748
+ const mappings = buildControlMappings(built, options);
749
+ if (detailMappings?.desktop.length)
750
+ mappings.desktop.push(...detailMappings.desktop);
751
+ if (detailMappings?.mobile.length)
752
+ mappings.mobile.push(...detailMappings.mobile);
753
+ const viewConfig = {
754
+ view: {
755
+ render: {
756
+ desktop: buildDesktopTree(built, mainTableName, options, panes, details),
757
+ mobile: buildMobileTree(built, mainTableName, details),
758
+ },
759
+ controlMappings: mappings,
760
+ },
761
+ theme: {
762
+ desktop: {
763
+ name: 'default',
764
+ status: 'done',
765
+ viewAttr: { logo: { isUpload: false }, ...constants_1.DEFAULT_DESKTOP_THEME },
766
+ },
767
+ mobile: { name: 'default', status: 'done', viewAttr: { ...constants_1.DEFAULT_MOBILE_THEME } },
768
+ },
769
+ controlStyle,
770
+ };
771
+ return [
772
+ {
773
+ fdCode: constants_1.DEFAULT_SCENE_CODE,
774
+ fdName: constants_1.DEFAULT_SCENE_NAME,
775
+ fdStatus: 'draft',
776
+ fdLockMode: 'none',
777
+ fdOrder: 0,
778
+ dynamicProps: {},
779
+ needDelete: false,
780
+ ...(options.formId ? { fdXForm: { fdId: options.formId, fdName: options.formName } } : {}),
781
+ // 持久化时内层 fdConfig 同样为 JSON 字符串
782
+ fdConfig: JSON.stringify(viewConfig),
783
+ },
784
+ ];
785
+ }
786
+ /**
787
+ * 构建权限(auth)。
788
+ *
789
+ * 对应前端控件属性面板的「字段权限」(各控件 config.json 中的
790
+ * `controlGroup > fieldPermission > { required, editable, visible }`),
791
+ * 落库位置是 `auth[0].{add|edit|view}[表名].fields[字段名]`:
792
+ * ```
793
+ * { visible: true, editable: true, required: false, hide: false }
794
+ * ```
795
+ */
796
+ function buildAuth(models, options = {}) {
797
+ const add = {};
798
+ const edit = {};
799
+ const view = {};
800
+ const perm = options.permissions ?? {};
801
+ models.forEach((model) => {
802
+ // 明细表:字段 + 行操作(canAddRow/canDeleteRow/canImport/canExport)
803
+ if (model.fdType === 'detail') {
804
+ const bizFields = (model.fdFields ?? []).filter((f) => f.fdIsSystem !== true);
805
+ const addFields = {};
806
+ const editFields = {};
807
+ const viewFields = {};
808
+ bizFields.forEach((field) => {
809
+ addFields[field.fdName] = {
810
+ visible: true,
811
+ editable: true,
812
+ required: false,
813
+ hide: false,
814
+ ...perm.add?.[field.fdName],
815
+ };
816
+ editFields[field.fdName] = {
817
+ visible: true,
818
+ editable: true,
819
+ required: false,
820
+ hide: false,
821
+ ...perm.edit?.[field.fdName],
822
+ };
823
+ viewFields[field.fdName] = { visible: true, ...perm.view?.[field.fdName] };
824
+ });
825
+ add[model.fdTableName] = {
826
+ ...constants_1.DETAIL_ROW_FLAGS.add,
827
+ fields: addFields,
828
+ operations: constants_1.DETAIL_OPERATIONS.add,
829
+ };
830
+ edit[model.fdTableName] = {
831
+ ...constants_1.DETAIL_ROW_FLAGS.add,
832
+ fields: editFields,
833
+ operations: constants_1.DETAIL_OPERATIONS.add,
834
+ };
835
+ view[model.fdTableName] = {
836
+ canExport: true,
837
+ fields: viewFields,
838
+ operations: constants_1.DETAIL_OPERATIONS.view,
839
+ };
840
+ return;
841
+ }
842
+ const addFields = {};
843
+ const editFields = {};
844
+ const viewFields = {};
845
+ (model.fdFields ?? []).forEach((field) => {
846
+ addFields[field.fdName] = {
847
+ visible: true,
848
+ editable: true,
849
+ required: false,
850
+ hide: false,
851
+ ...perm.add?.[field.fdName],
852
+ };
853
+ editFields[field.fdName] = {
854
+ visible: true,
855
+ editable: true,
856
+ required: false,
857
+ hide: false,
858
+ ...perm.edit?.[field.fdName],
859
+ };
860
+ viewFields[field.fdName] = { visible: true, ...perm.view?.[field.fdName] };
861
+ });
862
+ add[model.fdTableName] = { fields: addFields };
863
+ edit[model.fdTableName] = { fields: editFields };
864
+ view[model.fdTableName] = { fields: viewFields };
865
+ });
866
+ return [
867
+ {
868
+ fdName: ':publicLang.sysFormAuth',
869
+ authOrg: [],
870
+ fdIsAvailable: true,
871
+ add,
872
+ edit,
873
+ view,
874
+ },
875
+ ];
876
+ }
877
+ /** 主题字段优先取文档标题,其次取第一个业务字段 */
878
+ function pickSubjectField(main) {
879
+ const fields = main.fdFields ?? [];
880
+ return (fields.find((f) => f.fdName === 'fd_doc_subject') ??
881
+ fields.find((f) => !f.fdIsSystem) ??
882
+ fields[0]);
883
+ }
884
+ /**
885
+ * 构建「全局规则」(fdConfig.attribute)。
886
+ *
887
+ * @param prevFormAttr 已有表单的 `attribute.formAttr` JSON 串(autofill 时传入),
888
+ * 其中已配置的 `controlAction`(以及 formRule / dataUnique 等前端态数据)会被保留,
889
+ * 否则 autofill 每次都会把设计器里配置的动作清空。
890
+ */
891
+ function buildAttribute(models, main, prevFormAttr) {
892
+ const subjectField = pickSubjectField(main);
893
+ const previous = (0, random_1.safeJsonParse)(prevFormAttr, {});
894
+ // controlAction 在已解析的 formAttr 中通常是对象,但也兼容嵌套 JSON 串的存法
895
+ const prevControlAction = typeof previous.controlAction === 'string'
896
+ ? (0, random_1.safeJsonParse)(previous.controlAction, {})
897
+ : (previous.controlAction ?? {});
898
+ const formAttr = {
899
+ subjectRule: subjectField
900
+ ? {
901
+ script: `\${data.biz.${subjectField.fdName}}`,
902
+ type: 'Eval',
903
+ vo: {
904
+ // 注意:content 使用数据模型名称,而非表名
905
+ content: `$${main.fdName}.${subjectField.fdLabel || subjectField.fdName}$`,
906
+ mode: 'formula',
907
+ },
908
+ }
909
+ : previous.subjectRule ?? {},
910
+ // 保留已有的表单规则 / 数据唯一性(设计器配置,重新生成会丢)
911
+ formRule: previous.formRule ?? { pattern: {} },
912
+ dataUnique: previous.dataUnique ?? {},
913
+ controlAction: prevControlAction,
914
+ currentTableName: main.fdTableName,
915
+ };
916
+ return formAttr;
917
+ }
918
+ /** 从函数源码中解析函数名(`function xxx(` / `async function xxx(`) */
919
+ function extractFunctionName(source) {
920
+ const matched = /(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/.exec(source);
921
+ return matched?.[1];
922
+ }
923
+ /** 深度收集 controlAction 中已占用的函数名,避免重名 */
924
+ function collectActionNames(controlAction) {
925
+ const names = new Set();
926
+ for (const group of [controlAction.control, controlAction.global]) {
927
+ if (!group)
928
+ continue;
929
+ for (const events of Object.values(group)) {
930
+ if (!events)
931
+ continue;
932
+ for (const items of Object.values(events)) {
933
+ if (!Array.isArray(items))
934
+ continue;
935
+ for (const item of items) {
936
+ if (item?.name)
937
+ names.add(item.name);
938
+ }
939
+ }
940
+ }
941
+ }
942
+ return names;
943
+ }
944
+ /**
945
+ * 将控件动作写入 `formAttr.controlAction`。
946
+ *
947
+ * 控件级事件的 key 为 **`表名.控件uuid`**(与运行时
948
+ * `runtime/business/action.ts#addActionToControlProps` 的查找规则一致:
949
+ * `controlAction.control[key] || controlAction.control[\`${$$tableName}.${uuid}\`]`);
950
+ * 全局动作(`onLoad` / `onBeforeSubmit` / `onAfterSubmit`)写入 `controlAction.global`。
951
+ *
952
+ * @returns 实际写入的动作数量与告警(目标字段不存在、函数重名等)
953
+ */
954
+ function applyControlActions(formAttr, targets, actions) {
955
+ var _a, _b;
956
+ if (!actions.length)
957
+ return { completed: 0, warnings: [] };
958
+ const controlAction = formAttr.controlAction ?? {};
959
+ formAttr.controlAction = controlAction;
960
+ controlAction.control = controlAction.control ?? {};
961
+ controlAction.global = controlAction.global ?? {};
962
+ const usedNames = collectActionNames(controlAction);
963
+ const controlMap = controlAction.control;
964
+ const globalMap = controlAction.global;
965
+ const sources = [];
966
+ const warnings = [];
967
+ let completed = 0;
968
+ for (const action of actions) {
969
+ // 函数名:显式指定 > 源码解析 > 自动生成;重名时按前端规则追加后缀
970
+ const base = action.name ??
971
+ extractFunctionName(action.source) ??
972
+ `${action.event}_${action.target.replace(/[^\w]/g, '_')}`;
973
+ const name = usedNames.has(base) ? `${base}_${(0, random_1.uuid)()}` : base;
974
+ if (name !== base) {
975
+ warnings.push(`动作函数重名:${base} 已重命名为 ${name}`);
976
+ }
977
+ usedNames.add(name);
978
+ const item = { name, id: (0, random_1.uuid)(), function: action.source };
979
+ if (action.target === exports.ACTION_GLOBAL_TARGET) {
980
+ ;
981
+ (globalMap[_a = action.event] ?? (globalMap[_a] = [])).push(item);
982
+ }
983
+ else {
984
+ const key = targets.get(action.target);
985
+ if (!key) {
986
+ warnings.push(`动作目标字段不存在,已跳过:${action.target}(事件 ${action.event})`);
987
+ continue;
988
+ }
989
+ const events = (controlMap[key] ?? (controlMap[key] = {}));
990
+ (events[_b = action.event] ?? (events[_b] = [])).push(item);
991
+ }
992
+ sources.push(action.source);
993
+ completed++;
994
+ }
995
+ // javascript 为所有函数源码拼接(设计器 JS 面板按此展示/编辑)
996
+ if (sources.length) {
997
+ const prev = typeof controlAction.javascript === 'string' ? controlAction.javascript : '';
998
+ controlAction.javascript = [prev.trim(), ...sources].filter(Boolean).join('\n\n');
999
+ }
1000
+ return { completed, warnings };
1001
+ }
1002
+ /**
1003
+ * 补齐明细表的系统字段属性。
1004
+ *
1005
+ * 实测真实明细表的系统字段(`fd_id`/`fd_main_id`/`fd_order`):
1006
+ * - `fdAttribute = {"uuid":"<表名>.<字段名>"}`(如 `kjcx_ptdj_dt1.fd_id`)
1007
+ * - `fdOrder = 9999`、`fdState = 'effective'`、`fdDataModel = {fdId, fdName}`
1008
+ */
1009
+ function enrichSystemField(field, model) {
1010
+ const table = model.fdTableName;
1011
+ const current = (0, random_1.safeJsonParse)(field.fdAttribute, {});
1012
+ if (!current.uuid) {
1013
+ field.fdAttribute = JSON.stringify({ uuid: `${table}.${field.fdName}` });
1014
+ }
1015
+ field.fdTableName = table;
1016
+ field.fdTableType = model.fdType;
1017
+ field.fdDataModel = { fdId: model.fdId, fdName: model.fdName };
1018
+ field.fdState = field.fdState === 'notEffective' ? 'effective' : (field.fdState ?? 'effective');
1019
+ field.fdOrder = field.fdOrder ?? 9999;
1020
+ }
1021
+ /**
1022
+ * 构建明细表控件(`@elem/xform-detail-table`)的 fdAttribute。
1023
+ *
1024
+ * 该配置挂在**明细模型自身**上(不是主表字段),结构依据真实表单:
1025
+ * ```
1026
+ * { uuid:<表名>, config:{ key, type:'detail', kind:'container',
1027
+ * controlProps:{ …config.json 默认值, id, desktop:{hiddenLabel:true,type}, mobile:{type},
1028
+ * name:<表名>, uuid:<表名>, title:<langKey>, $$tableType:'detail', $$tableName:<表名>, type },
1029
+ * label:<langKey>, labelProps:{desktop:{}, mobile:{}} } }
1030
+ * ```
1031
+ */
1032
+ function buildDetailAttribute(model, langKey, meta) {
1033
+ const table = model.fdTableName;
1034
+ const key = (0, random_1.generateCmptKey)(constants_1.DETAIL_CONTROL.desktop);
1035
+ const defaults = (0, widgets_1.collectWidgetDefaults)(meta);
1036
+ const controlDefaults = {};
1037
+ Object.keys(defaults).forEach((k) => {
1038
+ if (!DEFAULT_EXCLUDE_KEYS.has(k))
1039
+ controlDefaults[k] = defaults[k];
1040
+ });
1041
+ const controlProps = {
1042
+ ...constants_1.DETAIL_CONTROL_DEFAULTS,
1043
+ ...controlDefaults,
1044
+ id: key,
1045
+ desktop: { hiddenLabel: true, type: constants_1.DETAIL_CONTROL.desktop },
1046
+ mobile: { type: constants_1.DETAIL_CONTROL.mobile },
1047
+ name: table,
1048
+ uuid: table,
1049
+ title: langKey,
1050
+ $$tableType: 'detail',
1051
+ $$tableName: table,
1052
+ type: constants_1.DETAIL_CONTROL.desktop,
1053
+ };
1054
+ return JSON.stringify({
1055
+ uuid: table,
1056
+ config: {
1057
+ key,
1058
+ type: 'detail',
1059
+ controlProps,
1060
+ kind: 'container',
1061
+ label: langKey,
1062
+ labelProps: { desktop: {}, mobile: {} },
1063
+ },
1064
+ });
1065
+ }
1066
+ /** 明细表渲染行:独立成行(span 24),节点形如 `{ key:<表名>, children:[字段引用] }` */
1067
+ function buildDetailRows(details) {
1068
+ return details.map((detail) => {
1069
+ const rowKey = (0, random_1.generateCmptKey)(constants_1.ROW);
1070
+ return {
1071
+ key: rowKey,
1072
+ type: constants_1.ROW,
1073
+ kind: 'container',
1074
+ controlProps: { id: rowKey, spans: [24] },
1075
+ children: [
1076
+ {
1077
+ key: detail.model.fdTableName,
1078
+ children: detail.fields.map((item) => ({ key: item.field.uuid })),
1079
+ },
1080
+ ],
1081
+ };
1082
+ });
1083
+ }
1084
+ /** 构建完整表单配置 IXFormConfig(持久化格式) */
1085
+ async function buildFdConfig(models, options = {}, stats) {
1086
+ if (!models || models.length === 0) {
1087
+ throw new Error('无法生成表单配置:数据模型为空');
1088
+ }
1089
+ const main = models.find((m) => m.fdType === 'main') ?? models[0];
1090
+ const mainTableName = main.fdTableName;
1091
+ // 默认只把业务字段渲染成表单控件,系统字段保留在数据模型中
1092
+ const renderFields = (main.fdFields ?? []).filter((f) => (options.includeSystem === true || f.fdIsSystem !== true) && f.fdDisplay !== false);
1093
+ const locales = resolveLocales(options.locale);
1094
+ const lang = {};
1095
+ const built = [];
1096
+ /** 动作目标索引:fdName -> 控件 key(`表名.控件uuid`,与运行时查找规则一致) */
1097
+ const actionTargets = new Map();
1098
+ /** 必填属性校验失败汇总(对齐设计器发布前校验) */
1099
+ const validationErrors = [];
1100
+ for (const field of renderFields) {
1101
+ const builtField = await buildField(field, main, options, lang, locales);
1102
+ built.push(builtField);
1103
+ if (stats)
1104
+ stats.optionsCompleted += builtField.optionCount;
1105
+ actionTargets.set(builtField.field.fdName, `${mainTableName}.${builtField.field.uuid}`);
1106
+ for (const error of builtField.propErrors) {
1107
+ validationErrors.push(`字段 ${field.fdName}(${builtField.resolved.desktopPkg}):请输入${error.label}(${error.key})`);
1108
+ }
1109
+ }
1110
+ // ---------- 明细表 ----------
1111
+ const detailModels = models.filter((m) => m.fdType === 'detail');
1112
+ const details = [];
1113
+ let detailMappings;
1114
+ if (detailModels.length) {
1115
+ const registry = options.registry;
1116
+ const [detailMeta, detailMetaMobile] = registry
1117
+ ? await Promise.all([registry.get(constants_1.DETAIL_CONTROL.desktop), registry.get(constants_1.DETAIL_CONTROL.mobile)])
1118
+ : [null, null];
1119
+ for (const model of detailModels) {
1120
+ model.fdFields.forEach((f) => {
1121
+ if (f.fdIsSystem)
1122
+ enrichSystemField(f, model);
1123
+ });
1124
+ const fields = [];
1125
+ for (const field of (model.fdFields ?? []).filter((f) => f.fdIsSystem !== true && f.fdDisplay !== false)) {
1126
+ const builtField = await buildField(field, model, options, lang, locales);
1127
+ fields.push(builtField);
1128
+ if (stats)
1129
+ stats.optionsCompleted += builtField.optionCount;
1130
+ actionTargets.set(builtField.field.fdName, `${model.fdTableName}.${builtField.field.uuid}`);
1131
+ for (const error of builtField.propErrors) {
1132
+ validationErrors.push(`明细字段 ${model.fdTableName}.${field.fdName}(${builtField.resolved.desktopPkg}):请输入${error.label}(${error.key})`);
1133
+ }
1134
+ }
1135
+ // 明细表标题多语言(prop:label,content 额外带 default)
1136
+ const langKey = (0, random_1.isMessageKey)(model.fdLabelLangKey)
1137
+ ? model.fdLabelLangKey
1138
+ : (0, random_1.generateMessageKey)();
1139
+ model.fdLabelLangKey = langKey;
1140
+ lang[langKey] = {
1141
+ prop: 'label',
1142
+ name: model.fdTableName,
1143
+ type: 'input',
1144
+ content: {
1145
+ ...buildLangContent(locales, model.fdName, undefined, options.messages),
1146
+ default: model.fdName,
1147
+ },
1148
+ };
1149
+ // 明细模型自身属性
1150
+ model.fdAttribute = buildDetailAttribute(model, langKey, detailMeta);
1151
+ // 明细表控件必填属性校验
1152
+ const detailAttr = (0, random_1.safeJsonParse)(model.fdAttribute, {});
1153
+ for (const error of (0, widgets_1.validateRequiredProps)(detailMeta, detailAttr?.config ?? {})) {
1154
+ validationErrors.push(`明细表 ${model.fdTableName}(${constants_1.DETAIL_CONTROL.desktop}):请输入${error.label}(${error.key})`);
1155
+ }
1156
+ model.fdFontExtendData = model.fdFontExtendData ?? JSON.stringify({ passValue: false });
1157
+ model.fdTableNameAlias = model.fdTableNameAlias ?? model.fdTableName;
1158
+ model.dynamicProps = model.dynamicProps ?? {};
1159
+ if (options.formId) {
1160
+ model.fdXForm = model.fdXForm ?? { fdId: options.formId, fdName: options.formName };
1161
+ }
1162
+ details.push({ model, fields, langKey });
1163
+ }
1164
+ // 明细表控件需要写入 controlMappings(与栅格布局不同)
1165
+ const detailEntry = (pkg, meta) => {
1166
+ const xform = {
1167
+ ...(meta?.xform ?? {}),
1168
+ type: constants_1.DETAIL_CONTROL.logicalType,
1169
+ defaultLength: 0,
1170
+ maxLength: 0,
1171
+ };
1172
+ if (meta?.env && !xform.env)
1173
+ xform.env = meta.env;
1174
+ return {
1175
+ name: pkg,
1176
+ platform: pkg === constants_1.DETAIL_CONTROL.desktop ? 'desktop' : 'mobile',
1177
+ xform,
1178
+ };
1179
+ };
1180
+ detailMappings = {
1181
+ desktop: [detailEntry(constants_1.DETAIL_CONTROL.desktop, detailMeta)],
1182
+ mobile: [detailEntry(constants_1.DETAIL_CONTROL.mobile, detailMetaMobile)],
1183
+ };
1184
+ }
1185
+ // 布局:--layout 未显式指定且已有视图合法时沿用(保持设计器布局不被重建为平铺)
1186
+ const prevView = options.layout === undefined ? normalizePrevViewModel(options.prevViewModel) : undefined;
1187
+ let viewModel;
1188
+ if (prevView) {
1189
+ viewModel = prevView.viewModel;
1190
+ // 旧视图中引用的非字段级多语言(页签标题等)必须携带,否则页签标题会丢失
1191
+ for (const [key, entry] of Object.entries(options.prevLang ?? {})) {
1192
+ const prop = entry?.prop;
1193
+ if (prop && !FIELD_LANG_PROPS.has(prop) && !lang[key])
1194
+ lang[key] = entry;
1195
+ }
1196
+ if (stats) {
1197
+ const missing = collectUnreferencedFields(prevView.referencedKeys, built, details);
1198
+ if (missing.length) {
1199
+ stats.layoutWarnings = [
1200
+ `已有布局未包含 ${missing.length} 个字段/明细表(${missing.join('、')});如需展示请显式指定 --layout 重建布局`,
1201
+ ];
1202
+ }
1203
+ }
1204
+ }
1205
+ else {
1206
+ viewModel = buildViewModel(built, mainTableName, options, lang, locales, details, detailMappings);
1207
+ }
1208
+ // 控件动作(onChange / onLoad / onBeforeSubmit…),写入 attribute.formAttr.controlAction
1209
+ const formAttr = buildAttribute(models, main, options.prevFormAttr);
1210
+ const actionResult = applyControlActions(formAttr, actionTargets, options.actions ?? []);
1211
+ if (stats) {
1212
+ stats.actionsCompleted = actionResult.completed;
1213
+ if (actionResult.warnings.length)
1214
+ stats.actionWarnings = actionResult.warnings;
1215
+ if (validationErrors.length)
1216
+ stats.validationErrors = validationErrors;
1217
+ }
1218
+ return {
1219
+ id: options.formId ?? '',
1220
+ name: options.formName ?? '',
1221
+ tableName: mainTableName,
1222
+ entityName: options.entityName ?? constants_1.ENTITY_NAME,
1223
+ entityCode: options.entityCode ?? constants_1.ENTITY_CODE,
1224
+ dataModel: models,
1225
+ auth: buildAuth(models, options),
1226
+ attribute: { formAttr: JSON.stringify(formAttr) },
1227
+ error: JSON.stringify({}),
1228
+ lang: JSON.stringify(lang),
1229
+ authFilter: { detailAuthDefine: {} },
1230
+ extendMap: {},
1231
+ sign: { formula: {} },
1232
+ viewModel,
1233
+ };
1234
+ }
1235
+ /**
1236
+ * 依据规格创建数据项(字段)定义,用于向数据模型追加新字段。
1237
+ * 生成结果可直接放入 `IDataModel.fdFields`,随 fdConfig 一并保存。
1238
+ */
1239
+ async function createDataField(spec, options = {}) {
1240
+ const map = controlMapOf(options);
1241
+ const alias = { ...constants_1.CONTROL_TYPE_MAP, ...(options.controlMap ?? {}) };
1242
+ // 第三个 token 容错:既可写数据项类型(varchar/number),也可写控件类型(moneytext/select)
1243
+ let controlType = spec.controlType;
1244
+ let dataType = spec.dataType;
1245
+ if (!controlType && dataType && alias[dataType] && !map[dataType]) {
1246
+ controlType = dataType;
1247
+ dataType = undefined;
1248
+ }
1249
+ let entry;
1250
+ if (controlType) {
1251
+ entry = alias[controlType];
1252
+ if (!entry) {
1253
+ const valid = Object.keys(constants_1.CONTROL_TYPE_MAP).join(', ');
1254
+ throw new Error(`未知的控件类型:${controlType},可选值:${valid}`);
1255
+ }
1256
+ dataType = alias[controlType].dataType ?? dataType;
1257
+ }
1258
+ else if (dataType && map[dataType]) {
1259
+ entry = map[dataType];
1260
+ }
1261
+ if (!entry) {
1262
+ dataType = dataType ?? 'varchar';
1263
+ entry = map[dataType] ?? constants_1.FALLBACK_CONTROL;
1264
+ }
1265
+ const registry = options.registry;
1266
+ const meta = registry ? await registry.get(entry.desktop) : null;
1267
+ const xform = meta?.xform ?? {};
1268
+ return {
1269
+ fdName: spec.name || `${constants_1.FIELD_NAME_PREFIX}${Math.random().toString(36).slice(-6)}`,
1270
+ fdLabel: spec.label,
1271
+ fdDataType: xform.dataType ?? dataType,
1272
+ fdType: xform.type || entry.logicalType,
1273
+ fdDictType: xform.dictType,
1274
+ fdLength: xform.defaultLength ?? 0,
1275
+ fdIsSystem: false,
1276
+ fdDisplay: true,
1277
+ fdIsStored: true,
1278
+ fdState: 'effective',
1279
+ };
1280
+ }
1281
+ /** 解析 `name:label[:dataType|controlType[:controlType]]` 形式的字段规格 */
1282
+ function parseFieldSpec(raw) {
1283
+ const parts = raw.split(':').map((s) => s.trim());
1284
+ if (parts.length === 1) {
1285
+ return { label: parts[0] };
1286
+ }
1287
+ return {
1288
+ name: parts[0] || undefined,
1289
+ label: parts[1] || parts[0],
1290
+ dataType: parts[2] || undefined,
1291
+ controlType: parts[3] || undefined,
1292
+ };
1293
+ }
1294
+ /**
1295
+ * 组装保存/发布请求体。
1296
+ * 注意:外层 fdConfig 为 JSON 字符串,内层 viewModel[].fdConfig 同样为字符串。
1297
+ */
1298
+ async function buildFormDefinePayload(models, options) {
1299
+ const fdConfig = await buildFdConfig(models, options, options.stats);
1300
+ return {
1301
+ ...options.base,
1302
+ fdConfig: JSON.stringify(fdConfig),
1303
+ };
1304
+ }