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