lan-low-code-cli 2.0.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.
package/README.md CHANGED
@@ -101,6 +101,7 @@ llc form create --name <名称> [选项]
101
101
  | `--auth-file <path>` | 精细字段权限(JSON),见「字段权限」章节 |
102
102
  | `--action <spec>` | 控件动作:`"字段名:事件:函数源码或@js文件"`,可重复;全局动作用 `$global`,见「控件动作」章节 |
103
103
  | `--action-file <path>` | 控件动作定义文件(JSON),见「控件动作」章节 |
104
+ | `--relation <spec>` | 业务关联:`"字段名=目标表单[:显示值=字段][:显示项=字段1,字段2]"`,可重复,见「关联类控件」章节 |
104
105
  | `--no-config` | 仅创建空表单,不生成字段配置 |
105
106
  | `--publish` | 创建后立即发布 |
106
107
  | `--dry-run` | 只打印将提交的数据,不发起请求 |
@@ -188,14 +189,14 @@ llc field gen-key --name <名称> # 生成字段编码
188
189
 
189
190
  **先查环境再选控件**:
190
191
 
191
- `ash
192
+ ```bash
192
193
  llc control list # 列出环境全部可用控件(含桌面/移动端包、数据项类型、是否登记、是否支持选项)
193
194
  llc control list --all # 含布局/外观类
194
195
  llc control show --type moneytext # 查看某控件的属性面板配置项与默认值
195
196
  llc control show --element @elem/xform-select --json
196
- `
197
+ ```
197
198
 
198
- control list 的数据源是 {origin}/web/sys/artifact/elements/index.json + 各控件 config.json(首次运行会逐个拉取并落盘缓存,之后秒回);已登记 表示可直接用于 --add-field,支持选项 表示可用 --option(dictType=optionDict)。
199
+ control list 的数据源是 `{origin}/web/sys/artifact/elements/index.json` + 各控件 `config.json`(首次运行会逐个拉取并落盘缓存,之后秒回)。`备注` 含义:`已登记` = 可直接用于 `--add-field`;`支持选项` = 可用 `--option`(`dictType=optionDict`);`容器类` = 明细/分页等需专用参数;`已废弃` = 平台标记废弃(如 `relation`);`需设计器配置` = 关键属性是环境对象选择,CLI 只建骨架。
199
200
 
200
201
  `--add-field` 的第三个 token 既可写**数据项类型**(`varchar`/`number`/…),
201
202
  也可直接写**控件类型**(`moneytext`/`select`/…),第四 token 为控件类型:
@@ -218,10 +219,144 @@ control list 的数据源是 {origin}/web/sys/artifact/elements/index.json + 各
218
219
  | 数值 | `numbertext`、`moneytext`、`rate`、`slider`、`timepicker`、`daterange` |
219
220
  | 选择 | `select`、`radio`、`checkbox`(均支持 `--option` 配选项) |
220
221
  | 日期 | `timestamp`、`timeZone` |
221
- | 组织/地址 | `address` |
222
+ | 组织人员 | `address`(**地址本**,选人/选组织,非地址文本,见下) |
222
223
  | 附件/媒体 | `attachment`、`image`、`video` |
223
224
  | 编码/码图 | `barcode`、`qrcode` |
224
225
  | 其它 | `switch`、`hidden`、`dividing`、`subject`、`relation`、`relationQuery` |
226
+ | 高级·元素类 | `desc`(说明文字)、`interval`(分割线)、`button`、`html`、`iframe`、`review`(审批意见)、`timeAxis`(时间轴)、`associatedFields`(关联字段)、`serial-number`(明细表序号) |
227
+ | 高级·编号类 | `mechnumber`(机制编号)、`manualNumber`(手工编号)、`reserveNumber`(预留编号) |
228
+ | 高级·数据类 | `association`(关联选择)、`refTemplate`(关联模板)、`foreignKey`(外键)、`cfg`(配置项)、`mdm`(主数据)、`dynamic`(动态控件)、`component`、`agency`、`area`、`tags`、`location`、`calculate`、`fileEdit`、`invoice`、`ocr`、`auditNote`、`template`、`ikm-author`、`ikm-category`、`ikm-content`、`ikm-player` |
229
+
230
+ 高级控件(环境中 67 个 `kind=control` 控件已登记 64 个,其余 3 个是容器类)使用注意:
231
+
232
+ - **元素类**(`desc`/`html`/`interval`/`button`/`iframe`/`review`/`timeAxis`/`associatedFields`/`serial-number`)
233
+ `fdIsStored=false`:仍写入 `dataModel`,但不建物理列(与设计器一致);
234
+ - **容器类**(`detail` 明细表、`bigDetail` 大数据明细表、`paged` 分页容器)**不是数据项**,
235
+ 不能用 `--add-field` 创建——明细表用 `--detail`;`bigDetail`/`paged` 暂不支持(环境 1628 个表单中无使用样例);
236
+ - **需设计器补充配置**的控件(关联范围 / 主数据 / 配置项 / 编号规则 / 识别类型 …):CLI 只创建骨架,
237
+ 生成时会打印提示;缺必填属性时发布校验会拦截(见「控件必填属性校验」)。
238
+
239
+ ```bash
240
+ # 高级控件用法与普通控件一致(第三个 token 写控件类型)
241
+ llc form autofill --id <fdId> --apply --add-field "fd_note:说明文字:desc" --add-field "fd_code:平台编码:mechnumber"
242
+ ```
243
+
244
+ #### `address`:地址本(组织 / 人员选择器)
245
+
246
+ ⚠️ **`address` 不是「地址(文本)」控件**,而是平台的**地址本**——用来选人、选部门/组织/岗位/用户组。
247
+ 凡涉及组织架构与人员的字段(责任人、经办人、所属部门、归属单位…)都用这个类型。
248
+
249
+ 落库特征(与真实表单核对):`fdDataType=address`、`fdDictType=orgElementDict`(组织人员字典,
250
+ 环境内唯一的 `orgElementDict` 控件)、`fdIsStored=true`。
251
+
252
+ | 属性 | 含义 |
253
+ |---|---|
254
+ | `range` | 选择范围:`all`(全部组织,默认)/ `custom`(自定义——此时才出现预选相关属性) |
255
+ | `org.orgTypeArr` | 可选类型,**位掩码数组**:`1`=组织、`2`=部门、`4`=岗位、`8`=人员、`16`=用户组;组合值如 `3`=组织或部门、`12`=岗位或人员、`31`=全部。CLI 默认 `['8']`(仅人员) |
256
+ | `multi` | 是否多选(默认 `false`,单选) |
257
+ | `preSelectType` / `preSelectValue` / `nextDept` | 仅 `range='custom'` 生效(预选类型 / 预选值 / 越过部门) |
258
+ | `relation` | 联动传值(`relationKey`/`relationValue`/`relationEnum`/`relationTableName`) |
259
+ | `orgAvailable` / `showAvatar` / `showType` | 是否可选组织、显示头像、显示类型 |
260
+
261
+ 平台把上述属性**同时**存两份:控件属性 `controlProps` 与数据项 `fdFontExtendData`
262
+ (前端 `syncApi.frontExtendData2Config` 反向同步;公式/接口 Schema 的 `$ref` 依赖它——
263
+ 如 `orgTypeArr:['8']` → `ORG_PERSON`、缺省则为 `ORG_ALL`)。CLI 两者都会写入并保持一致。
264
+
265
+ ```bash
266
+ # 选人字段(默认:全部范围 / 仅人员 / 单选)
267
+ llc form autofill --id <fdId> --apply --add-field "fd_owner:责任人:address"
268
+ ```
269
+
270
+ > 需要「多选 / 自定义范围 / 可选部门」等配置时,用设计器改(CLI 会把这些配置**原样保留**,
271
+ > 不会因为重新生成而丢失);平台预置的 `fd_creator`/`fd_owner`/`fd_alter` 等也是 `address` 类型,
272
+ > 但只靠 `fdFontExtendData.orgTypeArr` 表达类型(`fd_creator`=人员、`fd_creator_dept`=部门)。
273
+
274
+ #### 关联类控件(`relation` / `relationQuery` / `association` / …)
275
+
276
+ | 控件 | 含义 | CLI 状态 |
277
+ |---|---|---|
278
+ | `relation` | **业务关联**(关联其它表单的业务数据) | ⚠️ 控件包被平台标记**废弃**(`xform.group=deprecated`,但存量表单大量在用:环境内 1371 个字段)。控件包 `config.json` 里**没有任何必填属性**,但属性面板由设计器拼装、有强制要求——CLI 已按设计器规则做**语义校验**(见下),裸字段会被拦截。新表单建议改用 `association` |
279
+ | `relationQuery` | 关联查询 | 需设计器配置查询页签(`tabs` 必填 → 校验拦截) |
280
+ | `association` | 关联选择 | 需设计器配置关联范围与列表列(`xlistColumn` 必填 → 校验拦截) |
281
+ | `foreignKey` | 外键关联 | 需设计器配置映射类型与目标模型(`mappedType` 必填 → 校验拦截) |
282
+ | `refTemplate` | 关联模板 | 需设计器选择关联模板(校验拦截) |
283
+ | `associatedFields` | 关联字段(展示关联表单的字段) | 无必填属性,但未配置关联源时**后端发布会 500**(生成时告警) |
284
+
285
+ 业务关联的真实落库结构(已用《科技成果登记》等真实表单核对):`fdDataType=relation`、
286
+ `fdDictType=querySourceDict`、`fdLength=200`,核心配置在 `controlProps` 与 `fdFontExtendData`:
287
+
288
+ ```
289
+ controlProps.relationCfg = { appCode, xformName, modelId, tableName, showFields, refFieldName, fdFormType }
290
+ controlProps.datasource = { columns:[{name,label}], queryCondition, tree, relationConditions }
291
+ controlProps.outParams = { params:[{ sourceField:{fdName,tableName,tableType}, targetField:{…} }] }
292
+ controlProps.mappedType / multi / renderMode / direction / rowCount / isForwardView / modelId / modelName
293
+ ```
294
+
295
+ 这些是「选业务对象」的结果,CLI 无法推断、也不伪造——需在设计器里选好(选好后
296
+ `autofill` 会原样保留这些属性,见「设计器配置的控件属性会被保留」)。
297
+
298
+ **业务关联的必填项(CLI 已按设计器规则校验,见「控件语义校验」)**:
299
+
300
+ | 必填项 | controlProps | 规则来源 |
301
+ |---|---|---|
302
+ | 关联表单(业务对象)+ 显示值 | `relationCfg.appCode` / `relationCfg.refFieldName` | IDE `controlConfig.tsx` 的 `relationForm` 校验器「关联表单不能为空」 |
303
+ | 数据源显示项 | `datasource.columns` 非空 | IDE `dataSource` 校验器 + `dataSource/formDetails.tsx` 显示项 `required: true`(适用显示方式 `singlelist`/`mullist`/`treeList`/`multiTreeList`) |
304
+ | 卡片设置(卡片类显示方式) | `datasource.card.displayName` / `displayImage` + `columns` | 「卡片设置为必填项」 |
305
+ | 导航设置(树+卡片/列表类) | `datasource.tree.conditionsFromTree` / `relationConditions` / `relationCfg` | 「导航设置为必填项」(分类导航除外) |
306
+ | 树层级设置(`panel`/`tree` 类) | `datasource.tree.relationConditions` | 「树层级设置为必填项」 |
307
+
308
+ CLI 生成业务关联字段时会写入设计器的默认属性(依据环境 1371 个真实字段的分布):
309
+ `renderMode: 'singlelist'`(95%)、`direction: 'column'`、`rowCount: 3`、`isForwardView: 'no'`
310
+ (`mappedType` 仅 5% 字段使用,属按需配置,不做默认)。
311
+
312
+ ```bash
313
+ llc form autofill --id <fdId> --apply --add-field "fd_r:业务关联:relation"
314
+ # → ✖ 字段 fd_r(@elem/xform-relation):关联表单不能为空:需在设计器中选择业务对象并设置显示值…
315
+ # → ✖ 字段 fd_r(@elem/xform-relation):列表设置为必填项:需在数据源配置中选择显示项(datasource.columns)
316
+ # 退出码 1(--force 可跳过,但该控件仍需到设计器补配置)
317
+ ```
318
+
319
+ #### 用 `--relation` 配置业务关联(含业务对象与显示项)
320
+
321
+ `--relation` 把「字段 = 目标表单」解析成完整的业务关联配置(`form create` / `form autofill` 均支持):
322
+
323
+ ```bash
324
+ # 最简:目标按表单名(同名多份时优先当前表单所属应用)或 fdId
325
+ llc form autofill --id <fdId> --apply --detail "出库明细=fd_dtl_out_name:物料名称:relation" \
326
+ --relation "fd_dtl_out_name=物料信息"
327
+
328
+ # 指定显示值(选人看到的文案)与显示项(列表列)
329
+ llc form autofill --id <fdId> --apply \
330
+ --relation "fd_customer=客户信息:显示值=fd_cust_name:显示项=fd_cust_name,fd_cust_code,fd_level"
331
+ ```
332
+
333
+ | 部分 | 说明 | 缺省 |
334
+ |---|---|---|
335
+ | `字段名` | 目标字段物理名(主表或明细表字段,命中后自动改写为 `relation` 控件) | — |
336
+ | `目标表单` | 表单名或 fdId;按名匹配到多份时优先当前表单所属应用,仍歧义则报错列出候选 | — |
337
+ | `显示值=字段` | 存入 `relationCfg.refFieldName`/`showFields`(多项用逗号,写 `$字段名$`/`$标题$`) | 目标表首个文本类业务字段 |
338
+ | `显示项=字段1,字段2` | 存入 `datasource.columns`(设计器强制必填) | 目标表**全部**业务字段 |
339
+
340
+ 解析后会写入:`relationCfg`(业务对象)、`datasource`(数据源骨架 + 显示项 + 筛选项 + 树设置)、
341
+ `renderMode: 'singlelist'`、`modelName`,并把这些键同步到 `fdFontExtendData`(控件源码 JSDoc 的
342
+ `@frontExtendData` 声明);`datasource` 骨架结构取自真实表单,保证设计器打开该控件时结构完整。
343
+
344
+ **实测案例**(物料管理应用):把《物料出库单》明细表的「物料名称」由单行文本改为关联《物料信息》:
345
+
346
+ ```bash
347
+ llc form autofill --id 1k4ihs42swdw2vebw38dpgr036rj6uqi0tw0 --apply --publish \
348
+ --detail "出库明细=fd_dtl_out_name:物料名称:relation" \
349
+ --relation "fd_dtl_out_name=物料信息"
350
+ # → 业务关联:fd_dtl_out_name → 物料信息(显示值 物料名称,显示项 11 项:物料名称/规格型号/…/默认供应商)
351
+ # → 落库:fdType=relation、fdDictType=querySourceDict、renderMode=singlelist、fdStatus=official
352
+ ```
353
+
354
+ > 注意:切换为业务关联后,该字段**历史数据**(原文本值)不会自动解析成关联记录——设计器切换
355
+ > 关联表单时也有同样提示。另外「选中物料后自动带出其它列」(传出参数 `outParams`,如把
356
+ > 物料编码/参考单价写回明细行)尚未提供 CLI 参数,需要的话可以再加。
357
+
358
+ > 平台标记废弃的控件共 3 个:`relation`(业务关联)、`associatedFields`(关联字段)、`timeAxis`(时间轴)。
359
+ > `llc control list` 会以 `已废弃` 备注并汇总列出。
225
360
 
226
361
  `--option` 为选择类控件建选项,写入 `controlProps.options` 与 `fdFontExtendData.options`,
227
362
  并生成 `prop:'option'` 的多语言条目:
@@ -275,6 +410,9 @@ llc form autofill --id <fdId> --apply --publish \
275
410
 
276
411
  要点(均为真实表单核对结论):
277
412
 
413
+ - **栅格始终是满格的**:`GridItem` 总数 = `columns × rows`,最后一行的空位也会保留
414
+ (`children: []`、`controlProps: { id, column, row }`)——与设计器一致(真实样本如
415
+ 《业务审批管理》`columns=3` 但只有 2 个字段,第 3 格仍存在)。旧版本会删掉末行多余格子,已修复。
278
416
  - **栅格布局控件不进 `controlMappings`**;**多页签控件必须进**(`tab-appearance` / `tabs` / `tabpane` 三件套)。
279
417
  - 多页签的 key 约定:外观 key 形如 `@elem/xform-tab-appearance~xxxxxx`,
280
418
  tabs 节点 key 为 `<外观key>-tabItem`,面板为 `<外观key>-tabN`,
@@ -453,6 +591,7 @@ llc form autofill --id <fdId> --apply --publish --action-file ./actions.json
453
591
  | 函数签名 | 控件级 `function onChange(value, rowNum)`(value 当前值,rowNum 明细行号);运行时闭包内可用 `MKXFORM`(如 `MKXFORM.setValue('表名.字段名', v)`) |
454
592
  | key | `表名.控件uuid`(与运行时 `addActionToControlProps` 的查找规则一致);明细表字段同样适用 |
455
593
  | 函数重名 | 自动追加随机后缀(与前端 `mergeControlAction` 行为一致) |
594
+ | 函数命名 | 控件级动作禁止裸事件名(`onChange` 等)——CLI 自动规范为 `onChange_<控件id>` 并同步重写函数源码声明;`$global` 全局动作保持裸事件名(如 `onLoad`) |
456
595
  | 目标字段不存在 | 跳过并告警,不阻断 |
457
596
  | 已有动作 | **保留**。`autofill` 会加载已有 `formAttr` 合并写入,不会清掉设计器里配置的动作/表单规则 |
458
597
 
@@ -485,7 +624,10 @@ CLI 在 `form create` / `form autofill` 保存前会校验**控件必填属性
485
624
 
486
625
  - 校验对象:控件 `config.json` 属性面板中满足
487
626
  `required && !disabled && !protected && !slotScope` 的**叶子属性项**;
488
- - 值为空(`undefined/null/''/空数组`)即报错,格式 `请输入<label>(key)`;
627
+ - 值为空(`undefined/null/''`)即报错,格式 `请输入<label>(key)`;
628
+ **空数组不算空**——设计器规则是 `{required:true}`(不带 `type`),async-validator 仅在
629
+ `type === 'array'` 时才把 `[]` 判为空,因此 `preSelectValue: []`(地址本自定义范围的真实结构)
630
+ 是合法的,CLI 必须一致,否则会对合法表单误报并中止保存;
489
631
  - 与设计器一致地跳过:带 `enableWhen`/`hidden` 的条件渲染项(未渲染不校验)、
490
632
  分组容器(baseGroup/styleGroup 等)、`inputLocale` 类标题项(取 `title` 值);
491
633
  - 校验失败默认**中止保存/发布**(预览模式仅提示);加 `--force` 可跳过。
@@ -502,12 +644,46 @@ CLI 按 `src/constants.ts#REQUIRED_PROP_DEFAULTS` 自动补齐,
502
644
  | 控件 | 属性 | 默认值 |
503
645
  |---|---|---|
504
646
  | textarea | height | `"3"`(3 行,设计器实测) |
505
- | address | org / multi | `{"orgTypeArr":["8"],…}` / `false`(设计器实测) |
647
+ | address(地址本) | org / multi | `{"orgTypeArr":["8"],"defaultValueType":"null"}` / `false`(设计器实测;`8`=人员) |
506
648
  | rate | max | `5` |
507
649
  | slider | min / max | `0` / `100` |
508
650
  | rich-text | height | `300`(config.json min 200) |
509
651
  | bar-code | height | `80` |
510
652
  | hyperlinks | largestSet | `"infinit"`(选项首项) |
653
+ | description | defaultTextValue | `$title`(字段标题的多语言 key,见下方占位符) |
654
+ | tag | tabsRange / multi | `{categorys:[],targs:[],rangeType:'all'}` / `true` |
655
+ | location | allowed / mapContent / searchNearBy | `false` / `{address:'formattedAddress'}` / `{…}`(实测) |
656
+ | area | defaultValueType / precisionRegion / showHierarchy | `'null'` / `'county'` / `'all'` |
657
+ | html | source | `{html:'',css:'',javascript:''}` |
658
+ | iframe | heigth / expressionFormulaVO | `150` / `{}` |
659
+ | file-edit | height / allowImport / accept | `600` / `true` / `{docx,[.doc,.docx]}`(实测) |
660
+ | association | mode / processStatus | `'multiple'` / `['30']`(实测) |
661
+ | mdm | renderMode / direction / rowCount | `'singlelist'` / `'column'` / `3` |
662
+ | cfg | renderMode / cascader / showLevel / selectLevel | `'select'` / `true` / `'lastLevel'` / `'allLevel'` |
663
+ | calculate | statisticMode / defaultValueType | `'SUM'` / `'empty'` |
664
+ | interval / button / agency / associated-fields / time-axis / review / attach-demo / dynamic | 样式与开关类 | 设计器实测值 |
665
+
666
+ `REQUIRED_PROP_DEFAULTS` 支持两个**字段级占位符**(生成时解析):
667
+ `$title` → 字段多语言 key(`langKey`)、`$name` → 字段物理名(`fdName`)。
668
+
669
+ **设计器默认属性**(`src/constants.ts#CONTROL_DEFAULT_PROPS`):控件包 `config.json` **未声明**、
670
+ 但设计器新建字段时一定会写入的项。这类值不属于「必填」,缺失时运行时兜底不可靠:
671
+
672
+ | 控件 | 属性 | 默认值 | 依据 |
673
+ |---|---|---|---|
674
+ | relation(业务关联) | renderMode / direction / rowCount / isForwardView | `'singlelist'` / `'column'` / `3` / `'no'` | 环境 1371 个关联字段分布 95%/99%/99%/98% |
675
+ | moneytext(金额) | precision / symbol | `2` / `''` | 环境 115 个金额字段 `precision` 出现率 **100%**(88% 为 2);平台数据迁移代码缺省时按 0 位小数落库,故显式写 2 位。`symbol`(币种)属业务选择(69% CNY、28% 空),取中立空值 |
676
+
677
+ 已有字段的该项值若已由设计器配置,**原值优先**(`carried` 覆盖默认值),`autofill` 不会改写。
678
+
679
+ **需要「环境对象选择」的属性 CLI 不伪造**(选了才有意义):关联范围 `xlistColumn`、
680
+ 主数据 `mdm`、配置项 `cfg`、接口 `renderMode`(dynamic)、编号规则 `fdSysNumber`、
681
+ 映射类型 `mappedType`、识别类型 `discernType`、审批显示字段 `defaultFields`、
682
+ 默认附件/图片 `defaultValue` 等。生成时会给出「需在设计器中配置」提示,
683
+ 保存时被发布前校验拦截(`--force` 可跳过,但生成的控件需要人工补配置才能真正可用)。
684
+
685
+ 特例:`associatedFields`(关联字段)**没有必填属性**,但未配置关联源时后端发布会直接
686
+ 失败(HTTP 500,已实测)——CLI 会在生成时提示,需到设计器指定关联源字段。
511
687
 
512
688
  例外:`upcasing.relationControl`(联动控件)是语义性选择,CLI 不兜底;
513
689
  未指定时校验会拦截,需显式给出(存于 `fdFontExtendData.relationControl = {fdName, fdLabel}`)。
@@ -516,6 +692,15 @@ CLI 按 `src/constants.ts#REQUIRED_PROP_DEFAULTS` 自动补齐,
516
692
  (如 datetime 的 `defaultValueType='formula'` 时公式配置 `defaultValueFormulaVO` 必填);
517
693
  条件不成立或为复杂表达式时跳过,与设计器一致。
518
694
 
695
+ **控件语义校验(`src/constants.ts#CONTROL_SEMANTIC_RULES`)**:有些控件的属性面板由**设计器拼装**
696
+ (控件包 `config.json` 的 `config` 为空),必填项不在 `config.json` 里,通用必填校验覆盖不到。
697
+ CLI 按设计器源码中的校验器补了一层「语义校验」,每条规则可声明 `renderModes`(仅特定显示方式生效)。
698
+
699
+ 目前覆盖 `relation`(业务关联):`relationCfg`(关联表单)+ `datasource.columns`(显示项)
700
+ + 卡片/导航/树层级设置,文案与设计器一致(关联表单不能为空 / 列表设置为必填项 / 卡片设置为必填项 /
701
+ 导航设置为必填项 / 树层级设置为必填项)。存量影响面已实测:环境 1371 个业务关联字段中
702
+ 仅 34 个(2.5%,涉及 26 个表单)不合规,这些历史表单 `autofill` 时报错,需 `--force` 或到设计器补齐。
703
+
519
704
  **初始值模式**:新建字段 `defaultValueType` 默认为 `null`(无初始值),与控件 `config.json` 默认一致;
520
705
  不要写 `formula`——公式模式下 `defaultValueFormulaVO` 必填,设计器提交会被拦截。
521
706
  若需要公式初始值,按以下结构写入 `fdFontExtendData`(迁移自前端 `dataMigration.ts`):
@@ -695,7 +880,19 @@ CLI 会按需拉取并缓存到 `~/.llc/widgets-cache.json`。从中获取:
695
880
  - `env`:运行环境(`["xform"]`)
696
881
  - `config[].default`:控件默认属性(`aiFill/showText/passValue/encrypt/maskFormatType/encryptMonth/failureRule` 等)
697
882
 
698
- `placeholder` 通过 `POST {baseUrl}/sys-lang/getFrontResource`(key 约定 `{pkg}###langs###placeholder`)获取并写入 `lang`。
883
+ `placeholder`(提示文字)通过 `POST {baseUrl}/sys-lang/getFrontResource` 获取并写入 `lang`。**资源键名因控件而异**
884
+ (来源:控件源码 `defaultProps.placeholder`,见 `src/constants.ts#CONTROL_PLACEHOLDER_LANG_KEYS`):
885
+
886
+ | 资源键 | 控件 | 环境文案(zh-cn) |
887
+ |---|---|---|
888
+ | `pleaseEnter` | `@elem/xform-datetime`、`@elem/xform-timepicker`(含移动端 timepicker) | 请选择日期 / 请选择时间 |
889
+ | `enter` | `@elem/xform-m-datetime` | 请选择日期 |
890
+ | `pleaseSelect` | `@elem/xform-area` 系列 | 请选择 |
891
+ | `placeholder` | 其余控件(input/select/money/number/textarea/phone-number/rich-text…) | 请输入 / 请选择 |
892
+
893
+ > 此前 CLI 一律查 `###langs###placeholder`,导致日期控件取不到文案、**提示文字缺失**;现已按控件取键,
894
+ > 并保留未命中时的兜底探测(`placeholder → pleaseEnter → enter → pleaseSelect`)。
895
+ > **已有提示文字优先沿用**(设计器里的自定义文案不会被覆盖,如「请选择月份」)。
699
896
 
700
897
  内置的 `fdDataType → 控件包` 入口映射(见 `src/constants.ts`),可通过项目级 `lcode.config.json` 覆盖:
701
898
 
@@ -728,6 +925,44 @@ CLI 会按需拉取并缓存到 `~/.llc/widgets-cache.json`。从中获取:
728
925
  | clob | `@elem/xform-textarea` | `@elem/xform-m-textarea` | textarea |
729
926
  | relation | `@elem/xform-relation` | `@elem/xform-m-relation` | relation |
730
927
 
928
+ ### 高级控件映射与「未登记控件」保护
929
+
930
+ `CONTROL_TYPE_MAP`(`src/constants.ts`)覆盖环境中的 64/67 个 `kind=control` 控件。
931
+ 高级控件条目逐项按真实环境核对,除控件包外还携带落库元信息:
932
+
933
+ | 字段 | 写入 | 说明 |
934
+ |---|---|---|
935
+ | `dataType` | `fdDataType` | 控件包 `config.json` 未声明时以映射表兜底(`desc`/`serial-number`/`bigDetail` 等) |
936
+ | `dictType` | `fdDictType` | 如 `serialNumberDict`、`manyToManyDict`、`mapDict`、`attachmentDict` |
937
+ | `defaultLength` | `fdLength` | 如 `tags=2000`、`component=100` |
938
+ | `isStored` | `fdIsStored` | 元素类控件为 `false`(不建物理列) |
939
+
940
+ 对**未登记**的控件(平台新增或定制控件包),`autofill` **不再降级为输入框**:
941
+ `resolveControl` 会从字段已有 `fdAttribute.config.controlProps.desktop.type` 还原控件包,
942
+ 并原样保留 `fdType`,避免把环境里的高级控件改写成普通文本框(可安全地对存量表单做
943
+ `autofill`)。`llc control list` 会同时打印未登记控件与容器类控件清单。
944
+
945
+ ### 设计器配置的控件属性会被保留
946
+
947
+ 生成 `controlProps` 时,**同控件包**(`controlProps.desktop.type` 未变)的已有属性会被沿用,
948
+ 仅身份信息(`id`/`name`/`uuid`/`title`/`desktop`/`mobile`/`span`/`$$tableType`/`$$tableName`)、
949
+ `options`、`maxLength` 由生成器统一重建。换控件类型时不沿用(避免残留旧属性)。
950
+
951
+ `placeholder`(提示文字)是「**已有则沿用、缺失则补默认**」:设计器里的自定义文案(如
952
+ 「请选择月份」)连同其 `lang` 条目一起保留;没有时才按控件资源键补上默认文案(如
953
+ 日期控件→「请选择日期」、时间→「请选择时间」、文本类→「请输入」、下拉→「请选择」)。
954
+
955
+ 因此设计器里配置过的属性不会因为重新生成而丢失,例如:
956
+
957
+ | 控件 | 保留下来的属性 |
958
+ |---|---|
959
+ | `address`(地址本) | `range`、`multi`、`org.orgTypeArr`、`preSelectType`/`preSelectValue`、`nextDept` … |
960
+ | `moneytext` | `precision`、`symbol`、`showCount` … |
961
+ | 任意控件 | `placeholder`(自定义提示文字 + 对应多语言) |
962
+
963
+ 同时这些「前端扩展属性」(`config.json` 中声明 `frontExtendData` 的项)会同步写入
964
+ `fdFontExtendData`,与平台两处并存的约定一致。
965
+
731
966
  > 输出约定:除 `--json` 的结果外,所有日志/表格输出到 **stderr**,便于 `llc ... --json > out.json`。
732
967
 
733
968
  ## 七、目录结构
package/bin/llc.js CHANGED
File without changes
@@ -100,6 +100,18 @@ exports.modelingXFormApi = {
100
100
  conditions: { 'fdApp.fdId': request.fdAppId },
101
101
  columns: ['fdId', 'fdName', 'fdStatus', 'fdTableName'],
102
102
  }),
103
+ /**
104
+ * 按条件查询表单(业务对象解析等场景)。
105
+ *
106
+ * 注意:`list` 接口**必须带 conditions**(无条件调用会触发后端空指针 500);
107
+ * 返回含 `fdApp`(应用)与 `fdType`(表单定义类型),供 `--relation` 解析业务对象。
108
+ */
109
+ query: (client, request = {}) => client.post(`${MODELING_XFORM_BASE}/list`, {
110
+ pageNo: 1,
111
+ pageSize: 50,
112
+ columns: ['fdId', 'fdName', 'fdTableName', 'fdApp', 'fdType'],
113
+ ...request,
114
+ }),
103
115
  /** 生成建模业务表单数据库表名(mk_model_yyyyMMdd + 随机5位) */
104
116
  generateTableName: (client) => client.post(`${MODELING_XFORM_BASE}/generateTableName`),
105
117
  /** 检查表名是否可用(true=可用) */
@@ -66,16 +66,22 @@ function registerListCommand(control) {
66
66
  const short = pkg.name.replace(/^@elem\/xform-/, '');
67
67
  const mobileCandidate = `@elem/xform-m-${short}`;
68
68
  const type = String(xform.type ?? '');
69
+ const container = constants_1.CONTAINER_CONTROL_TYPES[type];
69
70
  rows.push({
70
71
  type,
71
- dataType: String(xform.dataType ?? ''),
72
- dictType: String(xform.dictType ?? ''),
72
+ dataType: String(xform.dataType ?? constants_1.CONTROL_TYPE_MAP[type]?.dataType ?? ''),
73
+ dictType: String(xform.dictType ?? constants_1.CONTROL_TYPE_MAP[type]?.dictType ?? ''),
73
74
  for: String(xform.for ?? ''),
74
75
  kind: String(xform.kind ?? ''),
75
76
  desktop: pkg.name,
76
77
  mobile: mobileNames.has(mobileCandidate) ? mobileCandidate : pkg.name,
77
78
  registered: Boolean(constants_1.CONTROL_TYPE_MAP[type]),
78
79
  options: xform.dictType === constants_1.OPTION_DICT_TYPE,
80
+ container: Boolean(container),
81
+ hint: container?.hint ?? '',
82
+ // 平台标记为废弃的控件包(xform.group='deprecated',如 relation 业务关联)
83
+ deprecated: String(xform.group ?? '') === 'deprecated',
84
+ needsDesigner: Boolean(constants_1.CONTROL_DESIGNER_NOTES[type]),
79
85
  });
80
86
  }
81
87
  const kind = opts.all ? undefined : String(opts.kind ?? 'control');
@@ -91,10 +97,35 @@ function registerListCommand(control) {
91
97
  适用: r.for,
92
98
  桌面端: r.desktop,
93
99
  移动端: r.mobile === r.desktop ? '同桌面端' : r.mobile,
94
- 备注: [r.registered ? '已登记' : '', r.options ? '支持选项' : ''].filter(Boolean).join('/'),
100
+ 备注: [
101
+ r.registered ? '已登记' : r.container ? '容器类' : '未登记',
102
+ r.options ? '支持选项' : '',
103
+ r.deprecated ? '已废弃' : '',
104
+ r.needsDesigner ? '需设计器配置' : '',
105
+ ]
106
+ .filter(Boolean)
107
+ .join('/'),
95
108
  })));
96
109
  const registered = filtered.filter((r) => r.registered).length;
110
+ const containers = filtered.filter((r) => r.container);
97
111
  logger_1.logger.info(logger_1.color.gray(`共 ${filtered.length} 个(kind=${kind ?? '全部'});其中 ${registered} 个已登记到 CONTROL_TYPE_MAP,可直接用于 --add-field`));
112
+ if (containers.length) {
113
+ logger_1.logger.info(logger_1.color.gray(`另有 ${containers.length} 个容器类控件(需专用参数创建):` +
114
+ containers.map((r) => `${r.type} —— ${r.hint}`).join(';')));
115
+ }
116
+ const unregistered = filtered.filter((r) => !r.registered && !r.container);
117
+ if (unregistered.length) {
118
+ logger_1.logger.info(logger_1.color.gray(`未登记控件 ${unregistered.length} 个(autofill 会按字段已有属性原样保留,不会降级):` +
119
+ unregistered.map((r) => r.type).join('、')));
120
+ }
121
+ const deprecated = filtered.filter((r) => r.deprecated);
122
+ if (deprecated.length) {
123
+ logger_1.logger.info(logger_1.color.gray(`平台已标记废弃 ${deprecated.length} 个(不建议新表单使用):` + deprecated.map((r) => r.type).join('、')));
124
+ }
125
+ const needsDesigner = filtered.filter((r) => r.needsDesigner);
126
+ if (needsDesigner.length) {
127
+ logger_1.logger.info(logger_1.color.gray(`需到设计器补配置 ${needsDesigner.length} 个:` + needsDesigner.map((r) => r.type).join('、')));
128
+ }
98
129
  });
99
130
  }
100
131
  /* PART3 */
@@ -8,6 +8,7 @@ const client_1 = require("../client");
8
8
  const config_1 = require("../config");
9
9
  const constants_1 = require("../constants");
10
10
  const generator_1 = require("../generator");
11
+ const relation_1 = require("../relation");
11
12
  const types_1 = require("../types");
12
13
  const logger_1 = require("../utils/logger");
13
14
  const icon_1 = require("../utils/icon");
@@ -203,6 +204,13 @@ function parseActionFile(file) {
203
204
  }
204
205
  return result;
205
206
  }
207
+ /** 打印业务关联解析结果(字段 → 业务对象、显示值、显示项数量) */
208
+ function logRelations(specs) {
209
+ for (const spec of specs) {
210
+ logger_1.logger.info(` 业务关联:${spec.fieldName} → ${spec.target.xformName}(显示值 ${spec.showFields.replace(/\$/g, '')},` +
211
+ `显示项 ${spec.columns.length} 项:${spec.columns.map((c) => c.label).join('/')})`);
212
+ }
213
+ }
206
214
  /** 汇总 `--action` + `--action-file` */
207
215
  function resolveActions(opts) {
208
216
  const specs = parseActionSpecs(opts.action ?? []);
@@ -574,6 +582,24 @@ async function buildDetailModels(client, formId, formName, specs, controlMap, re
574
582
  }
575
583
  return result;
576
584
  }
585
+ /**
586
+ * 把(新建或调整后的)明细表模型合并进数据模型列表。
587
+ *
588
+ * **必须按下标替换而不是追加**:`--detail` 复用已有明细表时,该表本身已在
589
+ * `models` 里,直接 `[...models, ...details]` 会在负载中产生两份同名明细模型
590
+ * (同一 `fdId`/`fdTableName`),保存结果取决于服务端去重策略,不可依赖。
591
+ */
592
+ function mergeDetailModels(models, details) {
593
+ const result = [...models];
594
+ for (const detail of details) {
595
+ const index = result.findIndex((m) => m.fdType === 'detail' && (m.fdId === detail.fdId || m.fdTableName === detail.fdTableName));
596
+ if (index >= 0)
597
+ result[index] = detail;
598
+ else
599
+ result.push(detail);
600
+ }
601
+ return result;
602
+ }
577
603
  /** 公共的生成器选项 */
578
604
  function generatorOptions(opts, formType, appId) {
579
605
  const family = resolveApiFamily(formType, opts.api, appId);
@@ -660,6 +686,7 @@ function registerFormCommand(program) {
660
686
  .option('--auth-file <path>', '精细字段权限(JSON),如 {"add":{"fd_col_a":{"required":true}}}')
661
687
  .option('--action <spec>', '控件动作:"字段名:事件:函数源码或@js文件",可重复;全局动作用 $global(onLoad/onBeforeSubmit/onAfterSubmit)', collect, [])
662
688
  .option('--action-file <path>', '控件动作定义文件(JSON),见 README「控件动作」章节')
689
+ .option('--relation <spec>', '业务关联:"字段名=目标表单[:显示值=字段][:显示项=字段1,字段2]",可重复;目标可写表单名或 fdId', collect, [])
663
690
  .option('--force', '跳过控件必填属性校验(默认校验失败会中止保存/发布)')
664
691
  .option('--no-config', '仅创建空表单,不生成字段配置')
665
692
  .option('--publish', '创建后立即发布')
@@ -841,6 +868,9 @@ function registerFormCommand(program) {
841
868
  if (actionSpecs.length) {
842
869
  logger_1.logger.info(` 控件动作:${actionSpecs.length} 个`);
843
870
  }
871
+ // 业务关联:解析目标表单(业务对象)与显示项
872
+ const relationSpecs = await (0, relation_1.resolveRelations)(client, (0, relation_1.parseRelationSpecs)(opts.relation ?? []), appId);
873
+ logRelations(relationSpecs);
844
874
  if (layoutOptions.layout && layoutOptions.layout !== 'rows') {
845
875
  logger_1.logger.info(` 布局:${layoutOptions.layout}`);
846
876
  }
@@ -851,7 +881,7 @@ function registerFormCommand(program) {
851
881
  let withDetail = withNew;
852
882
  if (detailSpecs.length) {
853
883
  const details = await buildDetailModels(client, fdId, base.fdName, detailSpecs, controlMap, registry, withNew);
854
- withDetail = [...withNew, ...details];
884
+ withDetail = mergeDetailModels(withNew, details);
855
885
  logger_1.logger.info(` 明细表:${details.length} 个`);
856
886
  }
857
887
  if (opts.addField?.length) {
@@ -877,6 +907,7 @@ function registerFormCommand(program) {
877
907
  permissions,
878
908
  options: optionSpecs,
879
909
  actions: actionSpecs,
910
+ relations: relationSpecs.length ? relationSpecs : undefined,
880
911
  });
881
912
  if (stats.optionsCompleted) {
882
913
  logger_1.logger.info(` 完善选项多语言:${stats.optionsCompleted} 个`);
@@ -884,7 +915,7 @@ function registerFormCommand(program) {
884
915
  if (stats.actionsCompleted) {
885
916
  logger_1.logger.info(` 配置控件动作:${stats.actionsCompleted} 个`);
886
917
  }
887
- for (const warning of stats.actionWarnings ?? []) {
918
+ for (const warning of [...(stats.actionWarnings ?? []), ...(stats.controlWarnings ?? [])]) {
888
919
  logger_1.logger.warn(warning);
889
920
  }
890
921
  assertPropValidation(stats, opts, true);
@@ -1048,6 +1079,7 @@ function registerFormCommand(program) {
1048
1079
  .option('--auth-file <path>', '精细字段权限(JSON),如 {"add":{"fd_col_a":{"required":true}}}')
1049
1080
  .option('--action <spec>', '控件动作:"字段名:事件:函数源码或@js文件",可重复;全局动作用 $global(onLoad/onBeforeSubmit/onAfterSubmit)', collect, [])
1050
1081
  .option('--action-file <path>', '控件动作定义文件(JSON),见 README「控件动作」章节')
1082
+ .option('--relation <spec>', '业务关联:"字段名=目标表单[:显示值=字段][:显示项=字段1,字段2]",可重复;目标可写表单名或 fdId', collect, [])
1051
1083
  .option('--force', '跳过控件必填属性校验(默认校验失败会中止保存/发布)')
1052
1084
  .option('--mechanism-entity-name <name>', '机制实体名(决定控件库)')
1053
1085
  .option('--mechanism-entity-code <code>', '机制编码')
@@ -1107,7 +1139,7 @@ function registerFormCommand(program) {
1107
1139
  let withDetail = withNew;
1108
1140
  if (detailSpecs.length) {
1109
1141
  const details = await buildDetailModels(client, opts.id, vo.fdName, detailSpecs, controlMap, registry, withNew);
1110
- withDetail = [...withNew, ...details];
1142
+ withDetail = mergeDetailModels(withNew, details);
1111
1143
  logger_1.logger.info(` 明细表:${details.length} 个(新建 ${details.filter((d) => !withNew.some((m) => m.fdId === d.fdId)).length},复用 ${details.filter((d) => withNew.some((m) => m.fdId === d.fdId)).length})`);
1112
1144
  }
1113
1145
  const actionSpecs = resolveActions(opts);
@@ -1116,6 +1148,9 @@ function registerFormCommand(program) {
1116
1148
  if (actionSpecs.length) {
1117
1149
  logger_1.logger.info(` 控件动作:${actionSpecs.length} 个`);
1118
1150
  }
1151
+ // 业务关联:解析目标表单(业务对象)与显示项(同名表单优先当前表单所属应用)
1152
+ const relationSpecs = await (0, relation_1.resolveRelations)(client, (0, relation_1.parseRelationSpecs)(opts.relation ?? []), await (0, relation_1.appIdOfForm)(client, opts.id));
1153
+ logRelations(relationSpecs);
1119
1154
  const stats = { optionsCompleted: 0, actionsCompleted: 0 };
1120
1155
  const payload = await (0, generator_1.buildFormDefinePayload)(withDetail, {
1121
1156
  base: {
@@ -1140,6 +1175,7 @@ function registerFormCommand(program) {
1140
1175
  permissions,
1141
1176
  options: optionSpecs,
1142
1177
  actions: actionSpecs,
1178
+ relations: relationSpecs.length ? relationSpecs : undefined,
1143
1179
  prevFormAttr: prev.prevFormAttr,
1144
1180
  prevViewModel: prev.prevViewModel,
1145
1181
  prevLang: prev.prevLang,
@@ -1151,7 +1187,11 @@ function registerFormCommand(program) {
1151
1187
  if (stats.actionsCompleted) {
1152
1188
  logger_1.logger.info(` 配置控件动作:${stats.actionsCompleted} 个`);
1153
1189
  }
1154
- for (const warning of [...(stats.actionWarnings ?? []), ...(stats.layoutWarnings ?? [])]) {
1190
+ for (const warning of [
1191
+ ...(stats.actionWarnings ?? []),
1192
+ ...(stats.layoutWarnings ?? []),
1193
+ ...(stats.controlWarnings ?? []),
1194
+ ]) {
1155
1195
  logger_1.logger.warn(warning);
1156
1196
  }
1157
1197
  assertPropValidation(stats, opts, Boolean(opts.apply));