lan-low-code-cli 2.1.0 → 2.3.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.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GLOBAL_ACTION_EVENTS = exports.ACTION_GLOBAL_TARGET = void 0;
4
+ exports.buildRelationProps = buildRelationProps;
4
5
  exports.resolveLocales = resolveLocales;
5
6
  exports.completeOptionsI18n = completeOptionsI18n;
6
7
  exports.buildViewModel = buildViewModel;
@@ -15,6 +16,62 @@ const random_1 = require("../utils/random");
15
16
  const widgets_1 = require("../widgets");
16
17
  /** `$global`:动作目标为表单全局(onLoad / onBeforeSubmit / onAfterSubmit) */
17
18
  exports.ACTION_GLOBAL_TARGET = '$global';
19
+ /** 建模表单的数据模型类名(业务关联的 `modelName`,取自真实表单) */
20
+ const MODELING_MODEL_NAME = 'com.landray.sys.xform.core.entity.design.SysXFormDesign';
21
+ /**
22
+ * 构建业务关联的 `controlProps` 片段(`relationCfg` + `datasource` + 显示方式)。
23
+ *
24
+ * 骨架逐键对齐**设计器手动创建的真实字段**(物料入库单 `fd_col_9bg1ms`):
25
+ * - `refFieldName` 是 `$<字段名>$` 引用(运行时解析为所选记录的显示值)——
26
+ * 早期版本误写成 `$<表名>.<字段名>$`,运行时解析不到就会把字面量显示出来;
27
+ * - 不写 `showFields`(设计器不生成该键);
28
+ * - `datasource.queryCondition` 是 `{ linkType, query: {} }`——带 `conditionGroup` 的
29
+ * 完整条件结构会导致运行时**列表加载失败**;
30
+ * - `isDisplayMode` 必须为 `false`(true 是「仅展示」模式);
31
+ * - 不写 `queryCollection` / `sorters` / `querySorters`(设计器未配置时不生成)。
32
+ */
33
+ function buildRelationProps(spec) {
34
+ const { target } = spec;
35
+ const columns = spec.columns;
36
+ return {
37
+ renderMode: 'singlelist',
38
+ modelName: MODELING_MODEL_NAME,
39
+ relationCfg: {
40
+ appCode: target.appCode,
41
+ xformName: target.xformName,
42
+ modelId: target.modelId,
43
+ refFieldName: spec.refFieldName,
44
+ tableType: 'main',
45
+ tableName: target.tableName,
46
+ fdFormType: target.fdFormType ?? 0,
47
+ },
48
+ datasource: {
49
+ queryCondition: { linkType: '$and', query: {} },
50
+ columns,
51
+ filters: spec.filters ?? [],
52
+ tree: {
53
+ treeLoop: true,
54
+ querySorters: [],
55
+ conditionSetting: false,
56
+ relationCfg: {},
57
+ relationConditions: [{ operator: { value: '$eq' }, value: { format: 'field' } }],
58
+ queryCondition: {
59
+ linkType: '$and',
60
+ query: { type: 'conditionGroup', condition: [] },
61
+ },
62
+ navigationMode: 'custom',
63
+ // 树导航的实体引用(sys-modeling#<表单fdId>),设计器创建的字段必有
64
+ entityName: `sys-modeling#${target.modelId}`,
65
+ },
66
+ isListThrough: true,
67
+ isDisplayMode: false,
68
+ viewId: '',
69
+ conditionSetting: false,
70
+ isForwardView: true,
71
+ },
72
+ ...(spec.outParams ? { outParams: spec.outParams } : {}),
73
+ };
74
+ }
18
75
  /** 全局动作支持的(前端注册的)事件类型 */
19
76
  exports.GLOBAL_ACTION_EVENTS = ['onLoad', 'onBeforeSubmit', 'onAfterSubmit'];
20
77
  /** 字段级多语言的 prop 集合(沿用旧视图时这些条目由生成器重建,不从旧 lang 携带) */
@@ -68,6 +125,28 @@ function collectUnreferencedFields(referencedKeys, built, details) {
68
125
  }
69
126
  return missing;
70
127
  }
128
+ /**
129
+ * 由生成器统一管理的控件属性键:沿用已有 controlProps 时跳过(否则会覆盖新生成的身份信息、
130
+ * 选项、长度等)。其余键(如 address 的 range/multi/org、money 的 precision/symbol)
131
+ * 视为设计器配置,同控件包时原样沿用。
132
+ */
133
+ const CONTROL_PROP_MANAGED_KEYS = new Set([
134
+ 'id',
135
+ 'desktop',
136
+ 'mobile',
137
+ 'name',
138
+ 'uuid',
139
+ 'title',
140
+ 'span',
141
+ '$$tableType',
142
+ '$$tableName',
143
+ '$$lockMode',
144
+ 'options',
145
+ 'placeholder',
146
+ 'maxLength',
147
+ ]);
148
+ /** 不写入 fdFontExtendData 的键(options 由选项逻辑单独维护) */
149
+ const FONT_EXTEND_SKIP_KEYS = new Set(['options', 'rules']);
71
150
  /** 控件配置面板默认值中,不应写入 controlProps 的键(属于样式/布局,存放在 controlStyle) */
72
151
  const DEFAULT_EXCLUDE_KEYS = new Set([
73
152
  'layout',
@@ -235,12 +314,25 @@ function completeOptionsI18n(field, lang, locales, messages) {
235
314
  field.fdFontExtendData = JSON.stringify(fontExtend);
236
315
  return count;
237
316
  }
238
- /** 依据数据项类型/逻辑类型解析控件包与元数据 */
317
+ /**
318
+ * 依据数据项类型/逻辑类型解析控件包与元数据。
319
+ *
320
+ * 匹配顺序(前者优先):
321
+ * 1. `CONTROL_TYPE_MAP[fdType]`:按控件逻辑类型精确匹配(避免「金额/评分/下拉」
322
+ * 被降级成同数据项类型的默认控件,如 number -> 数字);
323
+ * 2. `controlMap[fdType]`:用户自定义映射;
324
+ * 3. **字段已有属性中的控件包**:CLI 未登记的环境控件按原样保留(不被降级破坏);
325
+ * 4. `DEFAULT_CONTROL_MAP[fdDataType]`:按数据项类型兜底;
326
+ * 5. 自定义映射中 logicalType 相同的条目;
327
+ * 6. `FALLBACK_CONTROL`(输入框)。
328
+ */
239
329
  async function resolveControl(field, options) {
240
330
  const map = controlMapOf(options);
241
- // 优先按控件逻辑类型(xform.type)匹配,避免把「金额/评分/下拉」等
242
- // 错误降级成同数据项类型的默认控件(如 number -> 数字)
243
331
  let entry = constants_1.CONTROL_TYPE_MAP[field.fdType] ?? map[field.fdType];
332
+ // 未登记控件:从字段已有属性还原控件包(并保留原字段类型,不被控件包声明覆盖)
333
+ const preserved = entry ? undefined : preservedControlOf(field);
334
+ if (!entry)
335
+ entry = preserved;
244
336
  if (!entry)
245
337
  entry = map[field.fdDataType];
246
338
  if (!entry)
@@ -253,11 +345,35 @@ async function resolveControl(field, options) {
253
345
  return {
254
346
  desktopPkg: entry.desktop,
255
347
  mobilePkg: entry.mobile,
256
- logicalType: meta?.xform?.type || entry.logicalType,
348
+ logicalType: preserved
349
+ ? field.fdType || preserved.logicalType
350
+ : meta?.xform?.type || entry.logicalType,
351
+ kind: entry.kind ?? 'control',
257
352
  meta,
258
353
  metaMobile,
259
354
  };
260
355
  }
356
+ /**
357
+ * 从字段已有 `fdAttribute` 中还原控件包。
358
+ *
359
+ * 用于 **CLI 未登记的环境控件**(平台新增或定制控件包):此时若直接回落到
360
+ * `FALLBACK_CONTROL`(输入框),`autofill` 会把表单里的高级控件**降级破坏**。
361
+ * 只要字段属性里带着控件包名,就原样沿用,控件类型得以保全。
362
+ */
363
+ function preservedControlOf(field) {
364
+ if (typeof field.fdAttribute !== 'string')
365
+ return undefined;
366
+ const attr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
367
+ const config = attr?.config;
368
+ const desktop = config?.controlProps?.desktop?.type;
369
+ if (!desktop)
370
+ return undefined;
371
+ return {
372
+ desktop,
373
+ mobile: config?.controlProps?.mobile?.type || desktop,
374
+ logicalType: config?.type || field.fdType || '',
375
+ };
376
+ }
261
377
  /** 从 fdFontExtendData 提取需要注入 controlProps 的前端扩展属性 */
262
378
  function pickFontExtend(data) {
263
379
  const picked = {};
@@ -271,6 +387,61 @@ function pickFontExtend(data) {
271
387
  function fontExtendOf(field) {
272
388
  return pickFontExtend((0, random_1.safeJsonParse)(field.fdFontExtendData, {}));
273
389
  }
390
+ /** 按点号路径取值(用于语义校验规则) */
391
+ function getByPath(source, path) {
392
+ return path.split('.').reduce((acc, key) => {
393
+ if (!acc || typeof acc !== 'object')
394
+ return undefined;
395
+ return acc[key];
396
+ }, source);
397
+ }
398
+ /**
399
+ * 控件语义校验:`config.json` 必填属性之外、由控件属性面板/数据源弹窗强制的规则
400
+ * (`CONTROL_SEMANTIC_RULES`,如业务关联的「关联表单」与「显示项」)。
401
+ *
402
+ * @returns 未通过规则的报错文案
403
+ */
404
+ function validateSemanticProps(logicalType, controlProps) {
405
+ const rules = constants_1.CONTROL_SEMANTIC_RULES[logicalType];
406
+ if (!rules?.length)
407
+ return [];
408
+ const get = (path) => getByPath(controlProps, path);
409
+ const renderMode = String(controlProps.renderMode ?? '');
410
+ const errors = [];
411
+ for (const rule of rules) {
412
+ if (rule.renderModes && !rule.renderModes.includes(renderMode))
413
+ continue;
414
+ const pass = rule.ok ? rule.ok(get) : Boolean(get(rule.path ?? ''));
415
+ if (!pass)
416
+ errors.push(rule.message);
417
+ }
418
+ return errors;
419
+ }
420
+ /**
421
+ * 把 `controlProps` 中「前端扩展数据」同步进 `fdFontExtendData`。
422
+ *
423
+ * 平台约定两处并存(前端 `syncApi.frontExtendData2Config` 反向同步、公式/接口 Schema 的
424
+ * `$ref` 依赖 `fdFontExtendData`)——例如 address(地址本)的 `orgTypeArr` 决定公式里
425
+ * 该字段是 `ORG_PERSON` 还是 `ORG_ALL`。缺了它,控件渲染仍走 controlProps,但公式取值
426
+ * 会退化。已有的其它键(如 options)原样保留。
427
+ */
428
+ function syncFontExtendData(field, meta, controlProps) {
429
+ const keys = (0, widgets_1.collectFrontExtendKeys)(meta).filter(({ key }) => !FONT_EXTEND_SKIP_KEYS.has(key));
430
+ if (!keys.length)
431
+ return;
432
+ const fontExtend = field.fdFontExtendData
433
+ ? (0, random_1.safeJsonParse)(field.fdFontExtendData, {})
434
+ : {};
435
+ for (const { path, key } of keys) {
436
+ const [head, child] = path.split('.');
437
+ const value = child
438
+ ? controlProps[head]?.[child]
439
+ : controlProps[head];
440
+ if (value !== undefined)
441
+ fontExtend[key] = value;
442
+ }
443
+ field.fdFontExtendData = JSON.stringify(fontExtend);
444
+ }
274
445
  /**
275
446
  * 生成数据项的 fdAttribute。
276
447
  *
@@ -293,11 +464,39 @@ function buildFieldAttribute(params) {
293
464
  controlDefaults[k] = defaults[k];
294
465
  });
295
466
  const showText = defaults.showText;
467
+ // 必填属性兜底默认值:支持 $title / $name 占位符(如「描述文字」默认取字段标题)
468
+ const requiredDefaults = {};
469
+ for (const [propKey, value] of Object.entries(constants_1.REQUIRED_PROP_DEFAULTS[resolved.desktopPkg] ?? {})) {
470
+ requiredDefaults[propKey] =
471
+ value === '$title' ? langKey : value === '$name' ? fieldName : value;
472
+ }
473
+ // 沿用同控件包的已有属性(设计器配置过的值优先于 config.json 默认值)
474
+ const carried = {};
475
+ const prev = params.prevControlProps;
476
+ const prevPkg = prev?.desktop?.type;
477
+ if (prev && prevPkg === resolved.desktopPkg) {
478
+ // 业务关联重建时(extraControlProps 提供 relationCfg/datasource),旧字段可能带着
479
+ // 早期版本写入的空 outParams——设计器创建的字段没有这个键,不沿用
480
+ const skipStaleOutParams = params.extraControlProps != null && !('outParams' in params.extraControlProps);
481
+ for (const [propKey, value] of Object.entries(prev)) {
482
+ if (skipStaleOutParams && propKey === 'outParams')
483
+ continue;
484
+ if (!CONTROL_PROP_MANAGED_KEYS.has(propKey))
485
+ carried[propKey] = value;
486
+ }
487
+ }
296
488
  const controlProps = {
297
489
  ...controlDefaults,
490
+ // 设计器默认属性(控件包 config.json 未声明的,如业务关联的 renderMode=单一列表)
491
+ ...(constants_1.CONTROL_DEFAULT_PROPS[resolved.desktopPkg] ?? {}),
298
492
  // 必填属性兜底默认值(config.json 声明 required 但无 default 的项)
299
- ...constants_1.REQUIRED_PROP_DEFAULTS[resolved.desktopPkg],
493
+ ...requiredDefaults,
494
+ // 设计器新建控件的默认属性(逐键取自设计器落库,见 CONTROL_DESIGNER_DEFAULTS)——
495
+ // 按控件逻辑类型匹配,修正 CLI 与设计器新建控件的默认值差异
496
+ ...(constants_1.CONTROL_DESIGNER_DEFAULTS[resolved.logicalType] ?? {}),
497
+ ...carried,
300
498
  ...params.fontExtend,
499
+ ...(params.extraControlProps ?? {}),
301
500
  // 保留已有选项:本函数会重建 controlProps,不显式带回会丢失用户配置的 options
302
501
  ...(params.options && params.options.length ? { options: params.options } : {}),
303
502
  ...(params.placeholderKey ? { placeholder: params.placeholderKey } : {}),
@@ -328,7 +527,7 @@ function buildFieldAttribute(params) {
328
527
  const config = {
329
528
  key,
330
529
  type: logicalType,
331
- kind: 'control',
530
+ kind: resolved.kind,
332
531
  label: langKey,
333
532
  controlProps,
334
533
  labelProps,
@@ -374,10 +573,9 @@ function enrichField(field, model, resolved) {
374
573
  passValue: false,
375
574
  trace: false,
376
575
  encrypt: false,
377
- // 初始值模式:config.json 默认 'null'(无初始值)。不能写 'formula'——
378
- // 公式模式下 defaultValueFormulaVO 必填,设计器提交会被拦截
379
- defaultValueType: 'null',
380
576
  encryptDefinition: {},
577
+ // 注意:不写 defaultValueType——由 CONTROL_DESIGNER_DEFAULTS 提供各控件
578
+ // 的设计器默认值(formula/fixed/defalut…),并在构建后回同步到此处
381
579
  });
382
580
  }
383
581
  if (!field.fdIsSystem && (field.fdOrder === undefined || field.fdOrder === null)) {
@@ -386,6 +584,16 @@ function enrichField(field, model, resolved) {
386
584
  }
387
585
  /** 依据数据项生成(或补全)控件配置,并回写 fdAttribute / fdLabelLangKey */
388
586
  async function buildField(field, model, options, lang, locales) {
587
+ // 业务关联配置(--relation)命中该字段:先把数据类型改写为 relation,
588
+ // 使控件解析走业务关联控件(无论原字段是文本还是下拉)
589
+ const relationSpec = options.relations?.find((item) => item.fieldName === field.fdName);
590
+ if (relationSpec) {
591
+ field.fdType = 'relation';
592
+ field.fdDataType = 'relation';
593
+ field.fdDictType = 'querySourceDict';
594
+ if (!field.fdLength)
595
+ field.fdLength = 200;
596
+ }
389
597
  const resolved = await resolveControl(field, options);
390
598
  const span = options.span ?? constants_1.DEFAULT_SPAN;
391
599
  enrichField(field, model, resolved);
@@ -400,24 +608,65 @@ async function buildField(field, model, options, lang, locales) {
400
608
  type: 'input',
401
609
  content: buildLangContent(locales, field.fdLabel || field.fdName, field.fdName, options.messages),
402
610
  };
403
- // 控件占位提示多语言(约定 key:{pkg}###langs###placeholder)
611
+ // 已有控件属性(沿用选项、提示文字等)
612
+ const prevAttr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
613
+ // 控件提示文字多语言。
614
+ // ① 已有提示文字优先沿用(设计器里的自定义文案不能丢,如「请选择交办日期」);
615
+ // ② 否则取控件自带译文——资源键**因控件而异**(datetime=pleaseEnter、m-datetime=enter、
616
+ // area=pleaseSelect,其余=placeholder),键名不对就取不到文案(日期控件提示文字缺失即此原因)。
404
617
  let placeholderKey;
405
618
  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
- };
619
+ // 门控:config.json 声明了 placeholder 的控件 + 面板由设计器拼装(config.json 未声明
620
+ // 但设计器新建时同样写提示文字)的控件。两者都不满足的(radio/checkbox/button 等)
621
+ // 设计器也不写提示文字——不能仅凭资源探测,否则会多写(已与设计器新建表单核对)
622
+ if (registry &&
623
+ ((0, widgets_1.hasConfigItem)(resolved.meta, 'placeholder') || constants_1.PLACEHOLDER_PANEL_PKGS[resolved.desktopPkg])) {
624
+ const prevPlaceholder = prevAttr?.config?.controlProps?.placeholder;
625
+ const prevLangEntry = typeof prevPlaceholder === 'string' ? options.prevLang?.[prevPlaceholder] : undefined;
626
+ if (typeof prevPlaceholder === 'string' &&
627
+ prevPlaceholder &&
628
+ (prevLangEntry || !(0, random_1.isMessageKey)(prevPlaceholder))) {
629
+ placeholderKey = prevPlaceholder;
630
+ if (prevLangEntry)
631
+ lang[prevPlaceholder] = prevLangEntry;
632
+ }
633
+ else {
634
+ const mapped = constants_1.CONTROL_PLACEHOLDER_LANG_KEYS[resolved.desktopPkg];
635
+ const candidates = [
636
+ ...new Set([mapped, ...constants_1.PLACEHOLDER_LANG_KEY_CANDIDATES].filter(Boolean)),
637
+ ];
638
+ for (const keyName of candidates) {
639
+ const langs = await registry.getElementLangs((0, widgets_1.elementLangKey)(resolved.desktopPkg, keyName));
640
+ // 控件自带的多语言(zh-cn/en-us/...),用于填充非主语言的真实译文
641
+ const elementLangs = langs ? (0, locale_1.normalizeLangMap)(langs) : undefined;
642
+ const value = elementLangs?.[locales[0]] ??
643
+ elementLangs?.[constants_1.DEFAULT_LOCALE] ??
644
+ (elementLangs ? Object.values(elementLangs)[0] : undefined);
645
+ if (!value)
646
+ continue;
647
+ placeholderKey = (0, random_1.generateMessageKey)();
648
+ lang[placeholderKey] = {
649
+ prop: 'placeholder',
650
+ name: field.fdName,
651
+ type: 'input',
652
+ content: buildLangContent(locales, value, undefined, options.messages, elementLangs),
653
+ };
654
+ break;
655
+ }
656
+ // 拼装面板控件(config.json 未声明 placeholder):控件资源探测不到时使用
657
+ // 设计器的通用默认文案(选择类=请选择、输入类=请输入,见 PLACEHOLDER_PANEL_PKGS)
658
+ if (!placeholderKey) {
659
+ const fallbackText = constants_1.PLACEHOLDER_PANEL_PKGS[resolved.desktopPkg];
660
+ if (fallbackText) {
661
+ placeholderKey = (0, random_1.generateMessageKey)();
662
+ lang[placeholderKey] = {
663
+ prop: 'placeholder',
664
+ name: field.fdName,
665
+ type: 'input',
666
+ content: buildLangContent(locales, fallbackText, undefined, options.messages),
667
+ };
668
+ }
669
+ }
421
670
  }
422
671
  }
423
672
  // 新建选项(--option):写入 fdFontExtendData.options,作为选项的唯一来源
@@ -437,7 +686,6 @@ async function buildField(field, model, options, lang, locales) {
437
686
  field.fdFontExtendData = JSON.stringify(fontExtend);
438
687
  }
439
688
  // 保留已有的控件选项(数据项/设计器中配置),否则重建 attribute 会丢
440
- const prevAttr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
441
689
  const attrOptions = prevAttr?.config?.controlProps?.options;
442
690
  const fontOptions = field.fdFontExtendData
443
691
  ? ((0, random_1.safeJsonParse)(field.fdFontExtendData, {}).options ?? [])
@@ -450,6 +698,21 @@ async function buildField(field, model, options, lang, locales) {
450
698
  label: item?.label,
451
699
  value: item?.value,
452
700
  }));
701
+ // 选项类控件缺省选项:对齐设计器新建时的默认选项(选项1/2/3,值 1/2/3,含多语言)。
702
+ // CLI 此前不写默认选项,下拉/单选/多选运行时是空选项(真实缺陷,已与设计器新建表单核对)
703
+ const defaultOptions = !prevOptions.length && !specified?.length &&
704
+ ['radio', 'checkbox', 'select', 'select~multi'].includes(String(field.fdType))
705
+ ? ['1', '2', '3'].map((value, i) => {
706
+ const key = (0, random_1.generateMessageKey)();
707
+ lang[key] = {
708
+ prop: 'option',
709
+ name: field.fdName,
710
+ type: 'input',
711
+ content: buildLangContent(locales, `选项${i + 1}`, undefined, options.messages),
712
+ };
713
+ return { label: key, value };
714
+ })
715
+ : undefined;
453
716
  const { attribute, config } = buildFieldAttribute({
454
717
  fieldUuid: field.uuid,
455
718
  langKey,
@@ -461,14 +724,49 @@ async function buildField(field, model, options, lang, locales) {
461
724
  resolved,
462
725
  placeholderKey,
463
726
  fontExtend: fontExtendOf(field),
464
- options: prevOptions.length ? prevOptions : undefined,
727
+ options: prevOptions.length ? prevOptions : defaultOptions,
728
+ prevControlProps: prevAttr?.config?.controlProps,
729
+ extraControlProps: relationSpec ? buildRelationProps(relationSpec) : undefined,
465
730
  });
466
731
  field.fdAttribute = attribute;
732
+ // 前端扩展数据同步(如 address 的 orgTypeArr/multi/range)
733
+ syncFontExtendData(field, resolved.meta, (config.controlProps ?? {}));
734
+ // fdFontExtendData.defaultValueType 与 controlProps 保持一致(设计器行为:
735
+ // 新建控件的默认初始值模式为 formula/fixed/defalut 等,见 CONTROL_DESIGNER_DEFAULTS)
736
+ {
737
+ const feObj = (0, random_1.safeJsonParse)(field.fdFontExtendData, {});
738
+ const cpObj = (config.controlProps ?? {});
739
+ if (cpObj.defaultValueType !== undefined && feObj.defaultValueType !== cpObj.defaultValueType) {
740
+ feObj.defaultValueType = cpObj.defaultValueType;
741
+ field.fdFontExtendData = JSON.stringify(feObj);
742
+ }
743
+ }
744
+ // 业务关联的前端扩展数据(控件源码 JSDoc 声明的 @frontExtendData 路径):
745
+ // modelId/refFieldName/tableType/tableName、datasource 各键、renderMode、outParams
746
+ if (relationSpec) {
747
+ const fontExtend = field.fdFontExtendData
748
+ ? (0, random_1.safeJsonParse)(field.fdFontExtendData, {})
749
+ : {};
750
+ // 早期版本生成、现在不再写的键:清掉,避免运行时拿到过期结构
751
+ for (const stale of ['queryCollection', 'querySorters', 'sorters'])
752
+ delete fontExtend[stale];
753
+ if (!relationSpec.outParams)
754
+ delete fontExtend.outParams;
755
+ const props = (config.controlProps ?? {});
756
+ for (const path of constants_1.RELATION_FRONT_EXTEND_PATHS) {
757
+ const value = getByPath(props, path);
758
+ if (value !== undefined)
759
+ fontExtend[path.split('.').pop()] = value;
760
+ }
761
+ field.fdFontExtendData = JSON.stringify(fontExtend);
762
+ }
467
763
  // 选项多语言(下拉/单选/多选)
468
764
  const optionCount = completeOptionsI18n(field, lang, locales, options.messages);
469
765
  // 控件必填属性校验(对齐设计器发布前校验)
470
766
  const propErrors = (0, widgets_1.validateRequiredProps)(resolved.meta, config);
471
- return { field, model, resolved, langKey, optionCount, propErrors };
767
+ // 控件语义校验(属性面板/数据源弹窗强制的规则,如业务关联的关联表单与显示项)
768
+ const semanticErrors = validateSemanticProps(resolved.logicalType, (config.controlProps ?? {}));
769
+ return { field, model, resolved, langKey, optionCount, propErrors, semanticErrors };
472
770
  }
473
771
  /** 构建虚拟行:控件节点在持久化格式中**只保留 key 引用** */
474
772
  function buildRows(built, options) {
@@ -489,7 +787,7 @@ function buildRows(built, options) {
489
787
  return rows;
490
788
  }
491
789
  /**
492
- * 栅格布局:`layout` → `@elem/layout-grid` → 若干 `@elem/layout-grid.GridItem`。
790
+ * 栅格布局:`layout` → `@elem/layout-grid` → `columns × rows` 个 `@elem/layout-grid.GridItem`。
493
791
  *
494
792
  * 结构依据真实环境表单(`layout~xxxx` / `controlProps:{id}`):
495
793
  * ```
@@ -497,6 +795,9 @@ function buildRows(built, options) {
497
795
  * └ @elem/layout-grid { controlProps:{ id, columns, rows } }
498
796
  * └ GridItem { controlProps:{ id, column, row, rowSpan, columnSpan, style:{backgroundColor:''} }, children:[{key:<fieldUuid>}] }
499
797
  * ```
798
+ * **最后一行的空位也要保留**:`GridItem` 总数 = `columns × rows`,未放控件的格子为
799
+ * `children: []`、`controlProps: { id, column, row }`(与设计器一致,已核对真实表单——
800
+ * 如《业务审批管理》cols=3 而字段只有 2 个,第 3 个格子为空仍在)。
500
801
  * 注意:栅格布局控件**不进** `controlMappings`(已核对真实表单)。
501
802
  */
502
803
  function buildGrid(built, columns) {
@@ -504,6 +805,7 @@ function buildGrid(built, columns) {
504
805
  const rows = Math.max(1, Math.ceil(built.length / cols));
505
806
  const layoutKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT);
506
807
  const gridKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID);
808
+ // 先放控件,再补齐最后一行的空位(总数 = cols × rows;无字段时不生成格子)
507
809
  const items = built.map((item, index) => {
508
810
  const itemKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID_ITEM);
509
811
  return {
@@ -521,6 +823,21 @@ function buildGrid(built, columns) {
521
823
  children: [{ key: item.field.uuid }],
522
824
  };
523
825
  });
826
+ for (let index = built.length; index < (built.length ? rows * cols : 0); index++) {
827
+ const itemKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID_ITEM);
828
+ items.push({
829
+ key: itemKey,
830
+ type: constants_1.LAYOUT_GRID_ITEM,
831
+ kind: 'container',
832
+ // 空位与设计器一致:仅 column/row/id,children 为空
833
+ controlProps: {
834
+ column: (index % cols) + 1,
835
+ row: Math.floor(index / cols) + 1,
836
+ id: itemKey,
837
+ },
838
+ children: [],
839
+ });
840
+ }
524
841
  return [
525
842
  {
526
843
  key: layoutKey,
@@ -676,6 +993,9 @@ function wrapAppearance(tableName, children) {
676
993
  key: constants_1.MAIN,
677
994
  type: constants_1.MAIN,
678
995
  kind: 'container',
996
+ // 设计器保存时经 setMainCmpt 写入(ide/editor/runtime/business/feature.ts),
997
+ // 值为空也必须带键:CLI 曾缺失该属性,与设计器发布的视图不一致
998
+ controlProps: { importBtn: null, onDesktopImport: null },
679
999
  children,
680
1000
  },
681
1001
  ],
@@ -687,7 +1007,7 @@ function wrapAppearance(tableName, children) {
687
1007
  * 注意:栅格布局(`layout` / `@elem/layout-grid` / `GridItem`)**不写入**此处,
688
1008
  * 而多页签(`tab-appearance` / `tabs` / `tabpane`)**需要写入** —— 均为真实表单核对结论。
689
1009
  */
690
- function buildControlMappings(built, options = {}) {
1010
+ function buildControlMappings(built, options = {}, details = []) {
691
1011
  /** 布局控件的兜底元数据(控件包无 xform 时用,与真实表单的 xform.type 一致) */
692
1012
  const layoutDefaults = {
693
1013
  [constants_1.TAB_APPEARANCE]: 'tabAppearance',
@@ -697,11 +1017,13 @@ function buildControlMappings(built, options = {}) {
697
1017
  const build = (platform) => {
698
1018
  const seen = new Set();
699
1019
  const list = [];
700
- built.forEach((item) => {
1020
+ // **主表 + 明细字段**的控件都要进映射:运行时按 controlMappings 解析控件,
1021
+ // 明细字段的控件缺失时明细表渲染不完整(如顶部操作栏不出现)——真实缺陷已核对
1022
+ for (const item of [...built, ...details.flatMap((d) => d.fields)]) {
701
1023
  const pkg = platform === 'desktop' ? item.resolved.desktopPkg : item.resolved.mobilePkg;
702
1024
  const meta = platform === 'desktop' ? item.resolved.meta : item.resolved.metaMobile;
703
1025
  if (!pkg || seen.has(pkg))
704
- return;
1026
+ continue;
705
1027
  seen.add(pkg);
706
1028
  const xform = { ...(meta?.xform ?? {}), type: item.resolved.logicalType };
707
1029
  // 前端 handleFindXFormWidgets 会把包级 env 同步进 xform.env
@@ -710,7 +1032,7 @@ function buildControlMappings(built, options = {}) {
710
1032
  xform.env = meta.env;
711
1033
  }
712
1034
  list.push({ name: pkg, platform, xform });
713
- });
1035
+ }
714
1036
  if (options.layout === 'tabs') {
715
1037
  const pkgs = platform === 'desktop'
716
1038
  ? [constants_1.TAB_APPEARANCE, constants_1.TABS, constants_1.TAB_PANE]
@@ -745,7 +1067,7 @@ function buildViewModel(built, mainTableName, options = {}, lang = {}, locales =
745
1067
  });
746
1068
  // 页签多语言只生成一次,供桌面端渲染树复用
747
1069
  const panes = options.layout === 'tabs' ? resolveTabs(built, options, lang, locales) : [];
748
- const mappings = buildControlMappings(built, options);
1070
+ const mappings = buildControlMappings(built, options, details);
749
1071
  if (detailMappings?.desktop.length)
750
1072
  mappings.desktop.push(...detailMappings.desktop);
751
1073
  if (detailMappings?.mobile.length)
@@ -820,7 +1142,7 @@ function buildAuth(models, options = {}) {
820
1142
  hide: false,
821
1143
  ...perm.edit?.[field.fdName],
822
1144
  };
823
- viewFields[field.fdName] = { visible: true, ...perm.view?.[field.fdName] };
1145
+ viewFields[field.fdName] = { visible: true, hide: false, ...perm.view?.[field.fdName] };
824
1146
  });
825
1147
  add[model.fdTableName] = {
826
1148
  ...constants_1.DETAIL_ROW_FLAGS.add,
@@ -857,7 +1179,28 @@ function buildAuth(models, options = {}) {
857
1179
  hide: false,
858
1180
  ...perm.edit?.[field.fdName],
859
1181
  };
860
- viewFields[field.fdName] = { visible: true, ...perm.view?.[field.fdName] };
1182
+ viewFields[field.fdName] = { visible: true, hide: false, ...perm.view?.[field.fdName] };
1183
+ });
1184
+ // 明细表在主表 auth 中以「字段条目」出现,携带行操作(添加行/删除行/导入/导出)。
1185
+ // 缺失时运行时明细表顶部操作栏不渲染(真实缺陷,设计器保存的结构已核对)
1186
+ models
1187
+ .filter((m) => m.fdType === 'detail')
1188
+ .forEach((detail) => {
1189
+ addFields[detail.fdTableName] = {
1190
+ ...constants_1.DETAIL_MAIN_FIELD_BASE.add,
1191
+ ...perm.add?.[detail.fdTableName],
1192
+ operations: constants_1.DETAIL_MAIN_FIELD_OPERATIONS.add,
1193
+ };
1194
+ editFields[detail.fdTableName] = {
1195
+ ...constants_1.DETAIL_MAIN_FIELD_BASE.add,
1196
+ ...perm.edit?.[detail.fdTableName],
1197
+ operations: constants_1.DETAIL_MAIN_FIELD_OPERATIONS.add,
1198
+ };
1199
+ viewFields[detail.fdTableName] = {
1200
+ ...constants_1.DETAIL_MAIN_FIELD_BASE.view,
1201
+ ...perm.view?.[detail.fdTableName],
1202
+ operations: constants_1.DETAIL_MAIN_FIELD_OPERATIONS.view,
1203
+ };
861
1204
  });
862
1205
  add[model.fdTableName] = { fields: addFields };
863
1206
  edit[model.fdTableName] = { fields: editFields };
@@ -1119,6 +1462,9 @@ async function buildFdConfig(models, options = {}, stats) {
1119
1462
  for (const error of builtField.propErrors) {
1120
1463
  validationErrors.push(`字段 ${field.fdName}(${builtField.resolved.desktopPkg}):请输入${error.label}(${error.key})`);
1121
1464
  }
1465
+ for (const error of builtField.semanticErrors) {
1466
+ validationErrors.push(`字段 ${field.fdName}(${builtField.resolved.desktopPkg}):${error}`);
1467
+ }
1122
1468
  }
1123
1469
  // ---------- 明细表 ----------
1124
1470
  const detailModels = models.filter((m) => m.fdType === 'detail');
@@ -1144,6 +1490,9 @@ async function buildFdConfig(models, options = {}, stats) {
1144
1490
  for (const error of builtField.propErrors) {
1145
1491
  validationErrors.push(`明细字段 ${model.fdTableName}.${field.fdName}(${builtField.resolved.desktopPkg}):请输入${error.label}(${error.key})`);
1146
1492
  }
1493
+ for (const error of builtField.semanticErrors) {
1494
+ validationErrors.push(`明细字段 ${model.fdTableName}.${field.fdName}(${builtField.resolved.desktopPkg}):${error}`);
1495
+ }
1147
1496
  }
1148
1497
  // 明细表标题多语言(prop:label,content 额外带 default)
1149
1498
  const langKey = (0, random_1.isMessageKey)(model.fdLabelLangKey)
@@ -1214,6 +1563,54 @@ async function buildFdConfig(models, options = {}, stats) {
1214
1563
  ];
1215
1564
  }
1216
1565
  }
1566
+ // 沿用视图时同样补齐 `<main>` 节点的 controlProps(设计器保存时经 setMainCmpt 写入,
1567
+ // CLI 早期版本重建的视图缺失;详见 wrapAppearance 注释),并把 controlMappings
1568
+ // 与重建的映射做并集——旧视图的映射可能缺明细字段的控件(真实缺陷)
1569
+ const rebuiltMappings = buildControlMappings(built, options, details);
1570
+ for (const vm of viewModel) {
1571
+ const viewConfig = (0, random_1.safeJsonParse)(vm.fdConfig, {});
1572
+ const render = viewConfig?.view?.render;
1573
+ if (!render)
1574
+ continue;
1575
+ let patched = false;
1576
+ const patchMain = (nodes) => {
1577
+ for (const node of nodes) {
1578
+ if (!node || typeof node !== 'object')
1579
+ continue;
1580
+ if (node.type === constants_1.MAIN) {
1581
+ node.controlProps = {
1582
+ importBtn: null,
1583
+ onDesktopImport: null,
1584
+ ...(node.controlProps ?? {}),
1585
+ };
1586
+ patched = true;
1587
+ return true;
1588
+ }
1589
+ if (Array.isArray(node.children) && patchMain(node.children))
1590
+ return true;
1591
+ }
1592
+ return false;
1593
+ };
1594
+ for (const platform of Object.keys(render)) {
1595
+ if (Array.isArray(render[platform]))
1596
+ patchMain(render[platform]);
1597
+ }
1598
+ // controlMappings 并集:旧条目保留(如页签控件),重建条目按 name 覆盖(元数据最新)
1599
+ const cm = viewConfig.view?.controlMappings;
1600
+ if (cm) {
1601
+ for (const platform of ['desktop', 'mobile']) {
1602
+ const byName = new Map((Array.isArray(cm[platform]) ? cm[platform] : []).map((e) => [e.name, e]));
1603
+ for (const entry of rebuiltMappings[platform] ?? [])
1604
+ byName.set(entry.name, entry);
1605
+ for (const entry of detailMappings?.[platform] ?? [])
1606
+ byName.set(entry.name, entry);
1607
+ cm[platform] = [...byName.values()];
1608
+ patched = true;
1609
+ }
1610
+ }
1611
+ if (patched)
1612
+ vm.fdConfig = JSON.stringify(viewConfig);
1613
+ }
1217
1614
  }
1218
1615
  else {
1219
1616
  viewModel = buildViewModel(built, mainTableName, options, lang, locales, details, detailMappings);
@@ -1227,7 +1624,28 @@ async function buildFdConfig(models, options = {}, stats) {
1227
1624
  stats.actionWarnings = actionResult.warnings;
1228
1625
  if (validationErrors.length)
1229
1626
  stats.validationErrors = validationErrors;
1627
+ // 需到设计器完成配置的高级控件(关键属性是环境对象选择,CLI 无法替选)。
1628
+ // 仅在该字段确实缺配置(有必填/语义校验错误)时提示——已配置好的字段不该被误导;
1629
+ // associatedFields 无必填属性、问题只在发布时暴露,故无条件提示。
1630
+ const notes = new Set();
1631
+ for (const item of [...built, ...details.flatMap((d) => d.fields)]) {
1632
+ const logicalType = String(item.field.fdType);
1633
+ const note = constants_1.CONTROL_DESIGNER_NOTES[logicalType];
1634
+ if (!note)
1635
+ continue;
1636
+ const hasErrors = item.propErrors.length > 0 || item.semanticErrors.length > 0;
1637
+ if (!hasErrors && !constants_1.CONTROL_NOTE_WITHOUT_ERRORS.has(logicalType))
1638
+ continue;
1639
+ notes.add(`${item.field.fdLabel || item.field.fdName}(${logicalType})—— ${note}`);
1640
+ }
1641
+ if (notes.size)
1642
+ stats.controlWarnings = [...notes];
1230
1643
  }
1644
+ // attribute:formAttr 重建,其余顶层键(dataUnique / subjectRule 等)从已有设计整体保留
1645
+ let prevAttributeObj = {};
1646
+ if (options.prevAttribute)
1647
+ prevAttributeObj = (0, random_1.safeJsonParse)(options.prevAttribute, {});
1648
+ delete prevAttributeObj.formAttr;
1231
1649
  return {
1232
1650
  id: options.formId ?? '',
1233
1651
  name: options.formName ?? '',
@@ -1236,7 +1654,7 @@ async function buildFdConfig(models, options = {}, stats) {
1236
1654
  entityCode: options.entityCode ?? constants_1.ENTITY_CODE,
1237
1655
  dataModel: models,
1238
1656
  auth: buildAuth(models, options),
1239
- attribute: { formAttr: JSON.stringify(formAttr) },
1657
+ attribute: { ...prevAttributeObj, formAttr: JSON.stringify(formAttr) },
1240
1658
  error: JSON.stringify({}),
1241
1659
  lang: JSON.stringify(lang),
1242
1660
  authFilter: { detailAuthDefine: {} },
@@ -1259,6 +1677,14 @@ async function createDataField(spec, options = {}) {
1259
1677
  controlType = dataType;
1260
1678
  dataType = undefined;
1261
1679
  }
1680
+ // 容器类控件不是数据项(明细表/大数据明细/分页),给出明确指引而不是模糊报错。
1681
+ // 第三段既可写数据项类型也可写控件类型(见下方容错),两个位置都要拦。
1682
+ for (const token of [spec.controlType, spec.dataType]) {
1683
+ const container = token ? constants_1.CONTAINER_CONTROL_TYPES[token] : undefined;
1684
+ if (container) {
1685
+ throw new Error(`"${token}" 是容器类控件,不能用 --add-field 创建:${container.hint}`);
1686
+ }
1687
+ }
1262
1688
  let entry;
1263
1689
  if (controlType) {
1264
1690
  entry = alias[controlType];
@@ -1278,16 +1704,22 @@ async function createDataField(spec, options = {}) {
1278
1704
  const registry = options.registry;
1279
1705
  const meta = registry ? await registry.get(entry.desktop) : null;
1280
1706
  const xform = meta?.xform ?? {};
1707
+ // 元数据(控件包 config.json)优先,映射表兜底:高级控件中 desc/serial-number
1708
+ // 等控件包未声明 dataType,必须靠映射表补齐,否则数据项类型为空会导致保存失败
1709
+ const resolvedDataType = xform.dataType || entry.dataType || dataType || 'varchar';
1710
+ const resolvedDictType = xform.dictType || entry.dictType;
1711
+ const resolvedLength = xform.defaultLength ?? entry.defaultLength ?? 0;
1712
+ const resolvedStored = xform.isStored ?? entry.isStored ?? true;
1281
1713
  return {
1282
1714
  fdName: spec.name || `${constants_1.FIELD_NAME_PREFIX}${Math.random().toString(36).slice(-6)}`,
1283
1715
  fdLabel: spec.label,
1284
- fdDataType: xform.dataType ?? dataType,
1716
+ fdDataType: resolvedDataType,
1285
1717
  fdType: xform.type || entry.logicalType,
1286
- fdDictType: xform.dictType,
1287
- fdLength: xform.defaultLength ?? 0,
1718
+ ...(resolvedDictType ? { fdDictType: resolvedDictType } : {}),
1719
+ fdLength: resolvedLength,
1288
1720
  fdIsSystem: false,
1289
1721
  fdDisplay: true,
1290
- fdIsStored: true,
1722
+ fdIsStored: resolvedStored,
1291
1723
  fdState: 'effective',
1292
1724
  };
1293
1725
  }