lan-low-code-cli 2.1.0 → 2.2.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,57 @@ 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
+ * `datasource` 的骨架键取自真实表单(queryCondition/tree/filters/isListThrough…),
25
+ * 这些是设计器「列表设置」弹窗的固有字段,缺失会导致设计器打开该控件时结构不完整。
26
+ */
27
+ function buildRelationProps(spec) {
28
+ const { target } = spec;
29
+ const columns = spec.columns;
30
+ return {
31
+ renderMode: 'singlelist',
32
+ modelName: MODELING_MODEL_NAME,
33
+ relationCfg: {
34
+ appCode: target.appCode,
35
+ xformName: target.xformName,
36
+ modelId: target.modelId,
37
+ showFields: spec.showFields,
38
+ refFieldName: spec.refFieldName,
39
+ tableType: 'main',
40
+ tableName: target.tableName,
41
+ fdFormType: target.fdFormType ?? 0,
42
+ },
43
+ datasource: {
44
+ queryCondition: { linkType: '$and', query: {} },
45
+ columns,
46
+ filters: spec.filters ?? columns,
47
+ tree: {
48
+ treeLoop: true,
49
+ querySorters: [],
50
+ conditionSetting: false,
51
+ relationCfg: {},
52
+ relationConditions: [{ operator: { value: '$eq' }, value: { format: 'field' } }],
53
+ queryCondition: {
54
+ linkType: '$and',
55
+ query: { type: 'conditionGroup', orAndOperator: 'type', condition: [] },
56
+ },
57
+ navigationMode: 'custom',
58
+ },
59
+ isListThrough: true,
60
+ isDisplayMode: true,
61
+ viewId: '',
62
+ conditionSetting: false,
63
+ isForwardView: true,
64
+ queryCollection: {},
65
+ sorters: [],
66
+ },
67
+ ...(spec.outParams ? { outParams: spec.outParams } : {}),
68
+ };
69
+ }
18
70
  /** 全局动作支持的(前端注册的)事件类型 */
19
71
  exports.GLOBAL_ACTION_EVENTS = ['onLoad', 'onBeforeSubmit', 'onAfterSubmit'];
20
72
  /** 字段级多语言的 prop 集合(沿用旧视图时这些条目由生成器重建,不从旧 lang 携带) */
@@ -68,6 +120,28 @@ function collectUnreferencedFields(referencedKeys, built, details) {
68
120
  }
69
121
  return missing;
70
122
  }
123
+ /**
124
+ * 由生成器统一管理的控件属性键:沿用已有 controlProps 时跳过(否则会覆盖新生成的身份信息、
125
+ * 选项、长度等)。其余键(如 address 的 range/multi/org、money 的 precision/symbol)
126
+ * 视为设计器配置,同控件包时原样沿用。
127
+ */
128
+ const CONTROL_PROP_MANAGED_KEYS = new Set([
129
+ 'id',
130
+ 'desktop',
131
+ 'mobile',
132
+ 'name',
133
+ 'uuid',
134
+ 'title',
135
+ 'span',
136
+ '$$tableType',
137
+ '$$tableName',
138
+ '$$lockMode',
139
+ 'options',
140
+ 'placeholder',
141
+ 'maxLength',
142
+ ]);
143
+ /** 不写入 fdFontExtendData 的键(options 由选项逻辑单独维护) */
144
+ const FONT_EXTEND_SKIP_KEYS = new Set(['options', 'rules']);
71
145
  /** 控件配置面板默认值中,不应写入 controlProps 的键(属于样式/布局,存放在 controlStyle) */
72
146
  const DEFAULT_EXCLUDE_KEYS = new Set([
73
147
  'layout',
@@ -235,12 +309,25 @@ function completeOptionsI18n(field, lang, locales, messages) {
235
309
  field.fdFontExtendData = JSON.stringify(fontExtend);
236
310
  return count;
237
311
  }
238
- /** 依据数据项类型/逻辑类型解析控件包与元数据 */
312
+ /**
313
+ * 依据数据项类型/逻辑类型解析控件包与元数据。
314
+ *
315
+ * 匹配顺序(前者优先):
316
+ * 1. `CONTROL_TYPE_MAP[fdType]`:按控件逻辑类型精确匹配(避免「金额/评分/下拉」
317
+ * 被降级成同数据项类型的默认控件,如 number -> 数字);
318
+ * 2. `controlMap[fdType]`:用户自定义映射;
319
+ * 3. **字段已有属性中的控件包**:CLI 未登记的环境控件按原样保留(不被降级破坏);
320
+ * 4. `DEFAULT_CONTROL_MAP[fdDataType]`:按数据项类型兜底;
321
+ * 5. 自定义映射中 logicalType 相同的条目;
322
+ * 6. `FALLBACK_CONTROL`(输入框)。
323
+ */
239
324
  async function resolveControl(field, options) {
240
325
  const map = controlMapOf(options);
241
- // 优先按控件逻辑类型(xform.type)匹配,避免把「金额/评分/下拉」等
242
- // 错误降级成同数据项类型的默认控件(如 number -> 数字)
243
326
  let entry = constants_1.CONTROL_TYPE_MAP[field.fdType] ?? map[field.fdType];
327
+ // 未登记控件:从字段已有属性还原控件包(并保留原字段类型,不被控件包声明覆盖)
328
+ const preserved = entry ? undefined : preservedControlOf(field);
329
+ if (!entry)
330
+ entry = preserved;
244
331
  if (!entry)
245
332
  entry = map[field.fdDataType];
246
333
  if (!entry)
@@ -253,11 +340,34 @@ async function resolveControl(field, options) {
253
340
  return {
254
341
  desktopPkg: entry.desktop,
255
342
  mobilePkg: entry.mobile,
256
- logicalType: meta?.xform?.type || entry.logicalType,
343
+ logicalType: preserved
344
+ ? field.fdType || preserved.logicalType
345
+ : meta?.xform?.type || entry.logicalType,
257
346
  meta,
258
347
  metaMobile,
259
348
  };
260
349
  }
350
+ /**
351
+ * 从字段已有 `fdAttribute` 中还原控件包。
352
+ *
353
+ * 用于 **CLI 未登记的环境控件**(平台新增或定制控件包):此时若直接回落到
354
+ * `FALLBACK_CONTROL`(输入框),`autofill` 会把表单里的高级控件**降级破坏**。
355
+ * 只要字段属性里带着控件包名,就原样沿用,控件类型得以保全。
356
+ */
357
+ function preservedControlOf(field) {
358
+ if (typeof field.fdAttribute !== 'string')
359
+ return undefined;
360
+ const attr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
361
+ const config = attr?.config;
362
+ const desktop = config?.controlProps?.desktop?.type;
363
+ if (!desktop)
364
+ return undefined;
365
+ return {
366
+ desktop,
367
+ mobile: config?.controlProps?.mobile?.type || desktop,
368
+ logicalType: config?.type || field.fdType || '',
369
+ };
370
+ }
261
371
  /** 从 fdFontExtendData 提取需要注入 controlProps 的前端扩展属性 */
262
372
  function pickFontExtend(data) {
263
373
  const picked = {};
@@ -271,6 +381,61 @@ function pickFontExtend(data) {
271
381
  function fontExtendOf(field) {
272
382
  return pickFontExtend((0, random_1.safeJsonParse)(field.fdFontExtendData, {}));
273
383
  }
384
+ /** 按点号路径取值(用于语义校验规则) */
385
+ function getByPath(source, path) {
386
+ return path.split('.').reduce((acc, key) => {
387
+ if (!acc || typeof acc !== 'object')
388
+ return undefined;
389
+ return acc[key];
390
+ }, source);
391
+ }
392
+ /**
393
+ * 控件语义校验:`config.json` 必填属性之外、由控件属性面板/数据源弹窗强制的规则
394
+ * (`CONTROL_SEMANTIC_RULES`,如业务关联的「关联表单」与「显示项」)。
395
+ *
396
+ * @returns 未通过规则的报错文案
397
+ */
398
+ function validateSemanticProps(logicalType, controlProps) {
399
+ const rules = constants_1.CONTROL_SEMANTIC_RULES[logicalType];
400
+ if (!rules?.length)
401
+ return [];
402
+ const get = (path) => getByPath(controlProps, path);
403
+ const renderMode = String(controlProps.renderMode ?? '');
404
+ const errors = [];
405
+ for (const rule of rules) {
406
+ if (rule.renderModes && !rule.renderModes.includes(renderMode))
407
+ continue;
408
+ const pass = rule.ok ? rule.ok(get) : Boolean(get(rule.path ?? ''));
409
+ if (!pass)
410
+ errors.push(rule.message);
411
+ }
412
+ return errors;
413
+ }
414
+ /**
415
+ * 把 `controlProps` 中「前端扩展数据」同步进 `fdFontExtendData`。
416
+ *
417
+ * 平台约定两处并存(前端 `syncApi.frontExtendData2Config` 反向同步、公式/接口 Schema 的
418
+ * `$ref` 依赖 `fdFontExtendData`)——例如 address(地址本)的 `orgTypeArr` 决定公式里
419
+ * 该字段是 `ORG_PERSON` 还是 `ORG_ALL`。缺了它,控件渲染仍走 controlProps,但公式取值
420
+ * 会退化。已有的其它键(如 options)原样保留。
421
+ */
422
+ function syncFontExtendData(field, meta, controlProps) {
423
+ const keys = (0, widgets_1.collectFrontExtendKeys)(meta).filter(({ key }) => !FONT_EXTEND_SKIP_KEYS.has(key));
424
+ if (!keys.length)
425
+ return;
426
+ const fontExtend = field.fdFontExtendData
427
+ ? (0, random_1.safeJsonParse)(field.fdFontExtendData, {})
428
+ : {};
429
+ for (const { path, key } of keys) {
430
+ const [head, child] = path.split('.');
431
+ const value = child
432
+ ? controlProps[head]?.[child]
433
+ : controlProps[head];
434
+ if (value !== undefined)
435
+ fontExtend[key] = value;
436
+ }
437
+ field.fdFontExtendData = JSON.stringify(fontExtend);
438
+ }
274
439
  /**
275
440
  * 生成数据项的 fdAttribute。
276
441
  *
@@ -293,11 +458,31 @@ function buildFieldAttribute(params) {
293
458
  controlDefaults[k] = defaults[k];
294
459
  });
295
460
  const showText = defaults.showText;
461
+ // 必填属性兜底默认值:支持 $title / $name 占位符(如「描述文字」默认取字段标题)
462
+ const requiredDefaults = {};
463
+ for (const [propKey, value] of Object.entries(constants_1.REQUIRED_PROP_DEFAULTS[resolved.desktopPkg] ?? {})) {
464
+ requiredDefaults[propKey] =
465
+ value === '$title' ? langKey : value === '$name' ? fieldName : value;
466
+ }
467
+ // 沿用同控件包的已有属性(设计器配置过的值优先于 config.json 默认值)
468
+ const carried = {};
469
+ const prev = params.prevControlProps;
470
+ const prevPkg = prev?.desktop?.type;
471
+ if (prev && prevPkg === resolved.desktopPkg) {
472
+ for (const [propKey, value] of Object.entries(prev)) {
473
+ if (!CONTROL_PROP_MANAGED_KEYS.has(propKey))
474
+ carried[propKey] = value;
475
+ }
476
+ }
296
477
  const controlProps = {
297
478
  ...controlDefaults,
479
+ // 设计器默认属性(控件包 config.json 未声明的,如业务关联的 renderMode=单一列表)
480
+ ...(constants_1.CONTROL_DEFAULT_PROPS[resolved.desktopPkg] ?? {}),
298
481
  // 必填属性兜底默认值(config.json 声明 required 但无 default 的项)
299
- ...constants_1.REQUIRED_PROP_DEFAULTS[resolved.desktopPkg],
482
+ ...requiredDefaults,
483
+ ...carried,
300
484
  ...params.fontExtend,
485
+ ...(params.extraControlProps ?? {}),
301
486
  // 保留已有选项:本函数会重建 controlProps,不显式带回会丢失用户配置的 options
302
487
  ...(params.options && params.options.length ? { options: params.options } : {}),
303
488
  ...(params.placeholderKey ? { placeholder: params.placeholderKey } : {}),
@@ -386,6 +571,16 @@ function enrichField(field, model, resolved) {
386
571
  }
387
572
  /** 依据数据项生成(或补全)控件配置,并回写 fdAttribute / fdLabelLangKey */
388
573
  async function buildField(field, model, options, lang, locales) {
574
+ // 业务关联配置(--relation)命中该字段:先把数据类型改写为 relation,
575
+ // 使控件解析走业务关联控件(无论原字段是文本还是下拉)
576
+ const relationSpec = options.relations?.find((item) => item.fieldName === field.fdName);
577
+ if (relationSpec) {
578
+ field.fdType = 'relation';
579
+ field.fdDataType = 'relation';
580
+ field.fdDictType = 'querySourceDict';
581
+ if (!field.fdLength)
582
+ field.fdLength = 200;
583
+ }
389
584
  const resolved = await resolveControl(field, options);
390
585
  const span = options.span ?? constants_1.DEFAULT_SPAN;
391
586
  enrichField(field, model, resolved);
@@ -400,24 +595,47 @@ async function buildField(field, model, options, lang, locales) {
400
595
  type: 'input',
401
596
  content: buildLangContent(locales, field.fdLabel || field.fdName, field.fdName, options.messages),
402
597
  };
403
- // 控件占位提示多语言(约定 key:{pkg}###langs###placeholder)
598
+ // 已有控件属性(沿用选项、提示文字等)
599
+ const prevAttr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
600
+ // 控件提示文字多语言。
601
+ // ① 已有提示文字优先沿用(设计器里的自定义文案不能丢,如「请选择交办日期」);
602
+ // ② 否则取控件自带译文——资源键**因控件而异**(datetime=pleaseEnter、m-datetime=enter、
603
+ // area=pleaseSelect,其余=placeholder),键名不对就取不到文案(日期控件提示文字缺失即此原因)。
404
604
  let placeholderKey;
405
605
  const registry = options.registry;
406
606
  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
- };
607
+ const prevPlaceholder = prevAttr?.config?.controlProps?.placeholder;
608
+ const prevLangEntry = typeof prevPlaceholder === 'string' ? options.prevLang?.[prevPlaceholder] : undefined;
609
+ if (typeof prevPlaceholder === 'string' &&
610
+ prevPlaceholder &&
611
+ (prevLangEntry || !(0, random_1.isMessageKey)(prevPlaceholder))) {
612
+ placeholderKey = prevPlaceholder;
613
+ if (prevLangEntry)
614
+ lang[prevPlaceholder] = prevLangEntry;
615
+ }
616
+ else {
617
+ const mapped = constants_1.CONTROL_PLACEHOLDER_LANG_KEYS[resolved.desktopPkg];
618
+ const candidates = [
619
+ ...new Set([mapped, ...constants_1.PLACEHOLDER_LANG_KEY_CANDIDATES].filter(Boolean)),
620
+ ];
621
+ for (const keyName of candidates) {
622
+ const langs = await registry.getElementLangs((0, widgets_1.elementLangKey)(resolved.desktopPkg, keyName));
623
+ // 控件自带的多语言(zh-cn/en-us/...),用于填充非主语言的真实译文
624
+ const elementLangs = langs ? (0, locale_1.normalizeLangMap)(langs) : undefined;
625
+ const value = elementLangs?.[locales[0]] ??
626
+ elementLangs?.[constants_1.DEFAULT_LOCALE] ??
627
+ (elementLangs ? Object.values(elementLangs)[0] : undefined);
628
+ if (!value)
629
+ continue;
630
+ placeholderKey = (0, random_1.generateMessageKey)();
631
+ lang[placeholderKey] = {
632
+ prop: 'placeholder',
633
+ name: field.fdName,
634
+ type: 'input',
635
+ content: buildLangContent(locales, value, undefined, options.messages, elementLangs),
636
+ };
637
+ break;
638
+ }
421
639
  }
422
640
  }
423
641
  // 新建选项(--option):写入 fdFontExtendData.options,作为选项的唯一来源
@@ -437,7 +655,6 @@ async function buildField(field, model, options, lang, locales) {
437
655
  field.fdFontExtendData = JSON.stringify(fontExtend);
438
656
  }
439
657
  // 保留已有的控件选项(数据项/设计器中配置),否则重建 attribute 会丢
440
- const prevAttr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
441
658
  const attrOptions = prevAttr?.config?.controlProps?.options;
442
659
  const fontOptions = field.fdFontExtendData
443
660
  ? ((0, random_1.safeJsonParse)(field.fdFontExtendData, {}).options ?? [])
@@ -462,13 +679,33 @@ async function buildField(field, model, options, lang, locales) {
462
679
  placeholderKey,
463
680
  fontExtend: fontExtendOf(field),
464
681
  options: prevOptions.length ? prevOptions : undefined,
682
+ prevControlProps: prevAttr?.config?.controlProps,
683
+ extraControlProps: relationSpec ? buildRelationProps(relationSpec) : undefined,
465
684
  });
466
685
  field.fdAttribute = attribute;
686
+ // 前端扩展数据同步(如 address 的 orgTypeArr/multi/range)
687
+ syncFontExtendData(field, resolved.meta, (config.controlProps ?? {}));
688
+ // 业务关联的前端扩展数据(控件源码 JSDoc 声明的 @frontExtendData 路径):
689
+ // modelId/refFieldName/tableType/tableName、datasource 各键、renderMode、outParams
690
+ if (relationSpec) {
691
+ const fontExtend = field.fdFontExtendData
692
+ ? (0, random_1.safeJsonParse)(field.fdFontExtendData, {})
693
+ : {};
694
+ const props = (config.controlProps ?? {});
695
+ for (const path of constants_1.RELATION_FRONT_EXTEND_PATHS) {
696
+ const value = getByPath(props, path);
697
+ if (value !== undefined)
698
+ fontExtend[path.split('.').pop()] = value;
699
+ }
700
+ field.fdFontExtendData = JSON.stringify(fontExtend);
701
+ }
467
702
  // 选项多语言(下拉/单选/多选)
468
703
  const optionCount = completeOptionsI18n(field, lang, locales, options.messages);
469
704
  // 控件必填属性校验(对齐设计器发布前校验)
470
705
  const propErrors = (0, widgets_1.validateRequiredProps)(resolved.meta, config);
471
- return { field, model, resolved, langKey, optionCount, propErrors };
706
+ // 控件语义校验(属性面板/数据源弹窗强制的规则,如业务关联的关联表单与显示项)
707
+ const semanticErrors = validateSemanticProps(resolved.logicalType, (config.controlProps ?? {}));
708
+ return { field, model, resolved, langKey, optionCount, propErrors, semanticErrors };
472
709
  }
473
710
  /** 构建虚拟行:控件节点在持久化格式中**只保留 key 引用** */
474
711
  function buildRows(built, options) {
@@ -489,7 +726,7 @@ function buildRows(built, options) {
489
726
  return rows;
490
727
  }
491
728
  /**
492
- * 栅格布局:`layout` → `@elem/layout-grid` → 若干 `@elem/layout-grid.GridItem`。
729
+ * 栅格布局:`layout` → `@elem/layout-grid` → `columns × rows` 个 `@elem/layout-grid.GridItem`。
493
730
  *
494
731
  * 结构依据真实环境表单(`layout~xxxx` / `controlProps:{id}`):
495
732
  * ```
@@ -497,6 +734,9 @@ function buildRows(built, options) {
497
734
  * └ @elem/layout-grid { controlProps:{ id, columns, rows } }
498
735
  * └ GridItem { controlProps:{ id, column, row, rowSpan, columnSpan, style:{backgroundColor:''} }, children:[{key:<fieldUuid>}] }
499
736
  * ```
737
+ * **最后一行的空位也要保留**:`GridItem` 总数 = `columns × rows`,未放控件的格子为
738
+ * `children: []`、`controlProps: { id, column, row }`(与设计器一致,已核对真实表单——
739
+ * 如《业务审批管理》cols=3 而字段只有 2 个,第 3 个格子为空仍在)。
500
740
  * 注意:栅格布局控件**不进** `controlMappings`(已核对真实表单)。
501
741
  */
502
742
  function buildGrid(built, columns) {
@@ -504,6 +744,7 @@ function buildGrid(built, columns) {
504
744
  const rows = Math.max(1, Math.ceil(built.length / cols));
505
745
  const layoutKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT);
506
746
  const gridKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID);
747
+ // 先放控件,再补齐最后一行的空位(总数 = cols × rows;无字段时不生成格子)
507
748
  const items = built.map((item, index) => {
508
749
  const itemKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID_ITEM);
509
750
  return {
@@ -521,6 +762,21 @@ function buildGrid(built, columns) {
521
762
  children: [{ key: item.field.uuid }],
522
763
  };
523
764
  });
765
+ for (let index = built.length; index < (built.length ? rows * cols : 0); index++) {
766
+ const itemKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID_ITEM);
767
+ items.push({
768
+ key: itemKey,
769
+ type: constants_1.LAYOUT_GRID_ITEM,
770
+ kind: 'container',
771
+ // 空位与设计器一致:仅 column/row/id,children 为空
772
+ controlProps: {
773
+ column: (index % cols) + 1,
774
+ row: Math.floor(index / cols) + 1,
775
+ id: itemKey,
776
+ },
777
+ children: [],
778
+ });
779
+ }
524
780
  return [
525
781
  {
526
782
  key: layoutKey,
@@ -1119,6 +1375,9 @@ async function buildFdConfig(models, options = {}, stats) {
1119
1375
  for (const error of builtField.propErrors) {
1120
1376
  validationErrors.push(`字段 ${field.fdName}(${builtField.resolved.desktopPkg}):请输入${error.label}(${error.key})`);
1121
1377
  }
1378
+ for (const error of builtField.semanticErrors) {
1379
+ validationErrors.push(`字段 ${field.fdName}(${builtField.resolved.desktopPkg}):${error}`);
1380
+ }
1122
1381
  }
1123
1382
  // ---------- 明细表 ----------
1124
1383
  const detailModels = models.filter((m) => m.fdType === 'detail');
@@ -1144,6 +1403,9 @@ async function buildFdConfig(models, options = {}, stats) {
1144
1403
  for (const error of builtField.propErrors) {
1145
1404
  validationErrors.push(`明细字段 ${model.fdTableName}.${field.fdName}(${builtField.resolved.desktopPkg}):请输入${error.label}(${error.key})`);
1146
1405
  }
1406
+ for (const error of builtField.semanticErrors) {
1407
+ validationErrors.push(`明细字段 ${model.fdTableName}.${field.fdName}(${builtField.resolved.desktopPkg}):${error}`);
1408
+ }
1147
1409
  }
1148
1410
  // 明细表标题多语言(prop:label,content 额外带 default)
1149
1411
  const langKey = (0, random_1.isMessageKey)(model.fdLabelLangKey)
@@ -1227,6 +1489,22 @@ async function buildFdConfig(models, options = {}, stats) {
1227
1489
  stats.actionWarnings = actionResult.warnings;
1228
1490
  if (validationErrors.length)
1229
1491
  stats.validationErrors = validationErrors;
1492
+ // 需到设计器完成配置的高级控件(关键属性是环境对象选择,CLI 无法替选)。
1493
+ // 仅在该字段确实缺配置(有必填/语义校验错误)时提示——已配置好的字段不该被误导;
1494
+ // associatedFields 无必填属性、问题只在发布时暴露,故无条件提示。
1495
+ const notes = new Set();
1496
+ for (const item of [...built, ...details.flatMap((d) => d.fields)]) {
1497
+ const logicalType = String(item.field.fdType);
1498
+ const note = constants_1.CONTROL_DESIGNER_NOTES[logicalType];
1499
+ if (!note)
1500
+ continue;
1501
+ const hasErrors = item.propErrors.length > 0 || item.semanticErrors.length > 0;
1502
+ if (!hasErrors && !constants_1.CONTROL_NOTE_WITHOUT_ERRORS.has(logicalType))
1503
+ continue;
1504
+ notes.add(`${item.field.fdLabel || item.field.fdName}(${logicalType})—— ${note}`);
1505
+ }
1506
+ if (notes.size)
1507
+ stats.controlWarnings = [...notes];
1230
1508
  }
1231
1509
  return {
1232
1510
  id: options.formId ?? '',
@@ -1259,6 +1537,14 @@ async function createDataField(spec, options = {}) {
1259
1537
  controlType = dataType;
1260
1538
  dataType = undefined;
1261
1539
  }
1540
+ // 容器类控件不是数据项(明细表/大数据明细/分页),给出明确指引而不是模糊报错。
1541
+ // 第三段既可写数据项类型也可写控件类型(见下方容错),两个位置都要拦。
1542
+ for (const token of [spec.controlType, spec.dataType]) {
1543
+ const container = token ? constants_1.CONTAINER_CONTROL_TYPES[token] : undefined;
1544
+ if (container) {
1545
+ throw new Error(`"${token}" 是容器类控件,不能用 --add-field 创建:${container.hint}`);
1546
+ }
1547
+ }
1262
1548
  let entry;
1263
1549
  if (controlType) {
1264
1550
  entry = alias[controlType];
@@ -1278,16 +1564,22 @@ async function createDataField(spec, options = {}) {
1278
1564
  const registry = options.registry;
1279
1565
  const meta = registry ? await registry.get(entry.desktop) : null;
1280
1566
  const xform = meta?.xform ?? {};
1567
+ // 元数据(控件包 config.json)优先,映射表兜底:高级控件中 desc/serial-number
1568
+ // 等控件包未声明 dataType,必须靠映射表补齐,否则数据项类型为空会导致保存失败
1569
+ const resolvedDataType = xform.dataType || entry.dataType || dataType || 'varchar';
1570
+ const resolvedDictType = xform.dictType || entry.dictType;
1571
+ const resolvedLength = xform.defaultLength ?? entry.defaultLength ?? 0;
1572
+ const resolvedStored = xform.isStored ?? entry.isStored ?? true;
1281
1573
  return {
1282
1574
  fdName: spec.name || `${constants_1.FIELD_NAME_PREFIX}${Math.random().toString(36).slice(-6)}`,
1283
1575
  fdLabel: spec.label,
1284
- fdDataType: xform.dataType ?? dataType,
1576
+ fdDataType: resolvedDataType,
1285
1577
  fdType: xform.type || entry.logicalType,
1286
- fdDictType: xform.dictType,
1287
- fdLength: xform.defaultLength ?? 0,
1578
+ ...(resolvedDictType ? { fdDictType: resolvedDictType } : {}),
1579
+ fdLength: resolvedLength,
1288
1580
  fdIsSystem: false,
1289
1581
  fdDisplay: true,
1290
- fdIsStored: true,
1582
+ fdIsStored: resolvedStored,
1291
1583
  fdState: 'effective',
1292
1584
  };
1293
1585
  }
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ const field_1 = require("./commands/field");
9
9
  const form_1 = require("./commands/form");
10
10
  const locale_1 = require("./commands/locale");
11
11
  const logger_1 = require("./utils/logger");
12
- const VERSION = '2.1.0';
12
+ const VERSION = '2.2.0';
13
13
  const program = new commander_1.Command();
14
14
  exports.program = program;
15
15
  program
@@ -103,9 +103,18 @@ ${logger_1.color.bold('从零到一')}
103
103
  ${logger_1.color.bold('常用查询')}
104
104
  llc app list [--keyword 名称] [--no-auth] 应用列表(fdId 即 --app-id)
105
105
  llc app forms --id <appId> 应用下表单列表
106
+ llc control list 环境可用控件(含高级控件;明细/分页为容器类)
106
107
  llc locale list 环境提供了哪些语言译文
107
108
  llc field library [--keyword 关键词] 字段库
108
109
 
110
+ ${logger_1.color.gray('高级控件(desc / tags / mdm / association / mechnumber …)与普通控件同样写法:')}
111
+ llc form autofill --id <fdId> --apply --add-field "字段名:标题:控件类型"
112
+ ${logger_1.color.gray('(需环境对象选择的控件会在生成时提示,并需到设计器补配置)')}
113
+
114
+ ${logger_1.color.gray('业务关联(relation)用 --relation 配置业务对象与显示项:')}
115
+ llc form autofill --id <fdId> --apply --publish \\
116
+ --add-field "fd_r:关联选择:relation" --relation "fd_r=目标表单名"
117
+
109
118
  环境变量:
110
119
  LLC_BASE_URL 服务端基址
111
120
  LLC_TOKEN 鉴权 Token
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseRelationSpecs = parseRelationSpecs;
4
+ exports.appIdOfForm = appIdOfForm;
5
+ exports.resolveRelations = resolveRelations;
6
+ const formDefine_1 = require("./api/formDefine");
7
+ /** 解析 `--relation`:`字段名=目标表单[:显示值=字段1,字段2][:显示项=字段1,字段2]` */
8
+ function parseRelationSpecs(raw) {
9
+ return raw.map((item) => {
10
+ const text = item.trim();
11
+ const eq = text.indexOf('=');
12
+ if (eq <= 0) {
13
+ throw new Error(`--relation 格式错误:${item},应为 "字段名=目标表单[:显示值=字段][:显示项=字段1,字段2]"`);
14
+ }
15
+ const fieldName = text.slice(0, eq).trim();
16
+ const rest = text.slice(eq + 1);
17
+ const [targetRaw, ...optionParts] = rest.split(':');
18
+ const target = targetRaw.trim();
19
+ if (!fieldName || !target) {
20
+ throw new Error(`--relation 格式错误:${item},字段名与目标表单都不能为空`);
21
+ }
22
+ const parsed = { fieldName, target };
23
+ for (const part of optionParts) {
24
+ const trimmed = part.trim();
25
+ if (!trimmed)
26
+ continue;
27
+ const sep = trimmed.indexOf('=');
28
+ if (sep <= 0) {
29
+ throw new Error(`--relation 选项格式错误:${part}(应为 显示值=字段 或 显示项=字段1,字段2)`);
30
+ }
31
+ const name = trimmed.slice(0, sep).trim();
32
+ const values = trimmed
33
+ .slice(sep + 1)
34
+ .split(',')
35
+ .map((v) => v.trim())
36
+ .filter(Boolean);
37
+ if (!values.length)
38
+ throw new Error(`--relation 选项 ${name} 不能为空(${part})`);
39
+ if (name === '显示值' || name === 'refField' || name === 'ref') {
40
+ parsed.refFields = values;
41
+ }
42
+ else if (name === '显示项' || name === 'columns') {
43
+ parsed.columns = values;
44
+ }
45
+ else {
46
+ throw new Error(`--relation 不支持的选项:${name}(支持 显示值 / 显示项)`);
47
+ }
48
+ }
49
+ return parsed;
50
+ });
51
+ }
52
+ /** 查找业务对象表单:按 fdId 或表单名(同名多份时优先与当前表单同应用) */
53
+ async function findTargetForm(client, key, currentAppId) {
54
+ const byId = /^[0-9a-z]{20,}$/i.test(key);
55
+ const page = await formDefine_1.modelingXFormApi.query(client, {
56
+ conditions: byId ? { fdId: key } : { fdName: key },
57
+ });
58
+ const list = (page.content ?? []);
59
+ if (!list.length)
60
+ throw new Error(`未找到业务对象(目标表单):${key}`);
61
+ if (list.length > 1) {
62
+ const same = currentAppId ? list.filter((f) => f.fdApp?.fdId === currentAppId) : [];
63
+ if (same.length === 1)
64
+ return same[0];
65
+ throw new Error(`业务对象「${key}」匹配到 ${list.length} 个表单,请改用 fdId 指定:` +
66
+ list.map((f) => `${f.fdName}(${f.fdId}/应用 ${f.fdApp?.fdName ?? '-'})`).join(';'));
67
+ }
68
+ return list[0];
69
+ }
70
+ /** 查询表单所属应用 id(业务对象同名消歧用) */
71
+ async function appIdOfForm(client, formId) {
72
+ const page = await formDefine_1.modelingXFormApi.query(client, { conditions: { fdId: formId } });
73
+ const first = (page.content ?? [])[0];
74
+ return first?.fdApp?.fdId;
75
+ }
76
+ /** 目标表单主表的业务字段(非系统、显示) */
77
+ function businessFields(main) {
78
+ return (main?.fdFields ?? []).filter((f) => !f.fdIsSystem && f.fdDisplay !== false);
79
+ }
80
+ /**
81
+ * 查询环境解析业务关联:定位目标表单 → 取主表字段 → 构造 `IRelationSpec`。
82
+ *
83
+ * @param currentAppId 当前表单所属应用(同名表单消歧用)
84
+ */
85
+ async function resolveRelations(client, parsed, currentAppId) {
86
+ const specs = [];
87
+ for (const item of parsed) {
88
+ const form = await findTargetForm(client, item.target, currentAppId);
89
+ const design = await formDefine_1.modelingXFormApi.loadDesign(client, form.fdId);
90
+ const cfg = typeof design.fdConfig === 'string' ? JSON.parse(design.fdConfig) : design.fdConfig;
91
+ const main = (cfg?.dataModel ?? []).find((m) => m.fdType === 'main');
92
+ const fields = businessFields(main);
93
+ if (!fields.length)
94
+ throw new Error(`业务对象「${form.fdName}」主表没有可用业务字段`);
95
+ const labelOf = new Map(fields.map((f) => [f.fdName, f.fdLabel || f.fdName]));
96
+ // 显示项:缺省为该表全部业务字段
97
+ const columnNames = item.columns?.length ? item.columns : fields.map((f) => f.fdName);
98
+ const unknownColumns = columnNames.filter((n) => !labelOf.has(n));
99
+ if (unknownColumns.length) {
100
+ throw new Error(`业务对象「${form.fdName}」中不存在显示项字段:${unknownColumns.join('、')};` +
101
+ `可用字段:${fields.map((f) => f.fdName).join('、')}`);
102
+ }
103
+ // 显示值:缺省取首个文本类业务字段(无则取第一个业务字段)
104
+ const refNames = item.refFields ??
105
+ [
106
+ fields.find((f) => ['text', 'textarea'].includes(String(f.fdType)))?.fdName ??
107
+ fields[0].fdName,
108
+ ];
109
+ const unknownRefs = refNames.filter((n) => !labelOf.has(n));
110
+ if (unknownRefs.length) {
111
+ throw new Error(`业务对象「${form.fdName}」中不存在显示值字段:${unknownRefs.join('、')};` +
112
+ `可用字段:${fields.map((f) => f.fdName).join('、')}`);
113
+ }
114
+ specs.push({
115
+ fieldName: item.fieldName,
116
+ target: {
117
+ // appCode 存的是应用 fdId(与真实表单一致)
118
+ appCode: form.fdApp?.fdId ?? '',
119
+ xformName: form.fdName,
120
+ modelId: form.fdId,
121
+ tableName: main?.fdTableName ?? form.fdTableName ?? '',
122
+ fdFormType: form.fdType ?? 0,
123
+ },
124
+ refFieldName: refNames.map((n) => `$${n}$`).join(''),
125
+ showFields: refNames.map((n) => `$${labelOf.get(n)}$`).join(''),
126
+ columns: columnNames.map((n) => ({ name: n, label: labelOf.get(n) })),
127
+ });
128
+ }
129
+ return specs;
130
+ }