weaver-e10-builder 1.2.5 → 1.5.2

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.
Files changed (230) hide show
  1. package/assets/org-employee-template.xlsx +0 -0
  2. package/dist/commands/form-add-physical-field.d.ts +3 -0
  3. package/dist/commands/form-add-physical-field.d.ts.map +1 -0
  4. package/dist/commands/form-add-physical-field.js +125 -0
  5. package/dist/commands/form-add-physical-field.js.map +1 -0
  6. package/dist/commands/form-button.d.ts.map +1 -1
  7. package/dist/commands/form-button.js +28 -5
  8. package/dist/commands/form-button.js.map +1 -1
  9. package/dist/commands/form-field-linkage.d.ts.map +1 -1
  10. package/dist/commands/form-field-linkage.js +27 -6
  11. package/dist/commands/form-field-linkage.js.map +1 -1
  12. package/dist/commands/form-formula-linkage.d.ts +3 -0
  13. package/dist/commands/form-formula-linkage.d.ts.map +1 -0
  14. package/dist/commands/form-formula-linkage.js +434 -0
  15. package/dist/commands/form-formula-linkage.js.map +1 -0
  16. package/dist/commands/form-formula.d.ts.map +1 -1
  17. package/dist/commands/form-formula.js +22 -3
  18. package/dist/commands/form-formula.js.map +1 -1
  19. package/dist/commands/form-list-click.d.ts.map +1 -1
  20. package/dist/commands/form-list-click.js +2 -1
  21. package/dist/commands/form-list-click.js.map +1 -1
  22. package/dist/commands/form-list-permission.js +1 -1
  23. package/dist/commands/form-list-permission.js.map +1 -1
  24. package/dist/commands/form-native.d.ts.map +1 -1
  25. package/dist/commands/form-native.js +11 -3
  26. package/dist/commands/form-native.js.map +1 -1
  27. package/dist/commands/form-physical-table.d.ts +3 -0
  28. package/dist/commands/form-physical-table.d.ts.map +1 -0
  29. package/dist/commands/form-physical-table.js +195 -0
  30. package/dist/commands/form-physical-table.js.map +1 -0
  31. package/dist/commands/form.d.ts.map +1 -1
  32. package/dist/commands/form.js +246 -549
  33. package/dist/commands/form.js.map +1 -1
  34. package/dist/commands/matrix.d.ts.map +1 -1
  35. package/dist/commands/matrix.js +175 -36
  36. package/dist/commands/matrix.js.map +1 -1
  37. package/dist/commands/org.d.ts +10 -0
  38. package/dist/commands/org.d.ts.map +1 -0
  39. package/dist/commands/org.js +402 -0
  40. package/dist/commands/org.js.map +1 -0
  41. package/dist/commands/role.d.ts.map +1 -1
  42. package/dist/commands/role.js +106 -62
  43. package/dist/commands/role.js.map +1 -1
  44. package/dist/commands/wf-form.d.ts.map +1 -1
  45. package/dist/commands/wf-form.js +501 -26
  46. package/dist/commands/wf-form.js.map +1 -1
  47. package/dist/commands/workflow-auth-resp.d.ts.map +1 -1
  48. package/dist/commands/workflow-auth-resp.js +53 -5
  49. package/dist/commands/workflow-auth-resp.js.map +1 -1
  50. package/dist/commands/workflow-create-auth-resp.d.ts.map +1 -1
  51. package/dist/commands/workflow-create-auth-resp.js +5 -1
  52. package/dist/commands/workflow-create-auth-resp.js.map +1 -1
  53. package/dist/commands/workflow-operator-sign.js +1 -1
  54. package/dist/commands/workflow-operator-sign.js.map +1 -1
  55. package/dist/commands/workflow.d.ts.map +1 -1
  56. package/dist/commands/workflow.js +78 -9
  57. package/dist/commands/workflow.js.map +1 -1
  58. package/dist/index.js +8 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/services/action-detail.d.ts.map +1 -1
  61. package/dist/services/action-detail.js +5 -4
  62. package/dist/services/action-detail.js.map +1 -1
  63. package/dist/services/actionflow/components/action_eb_update_compiler.d.ts.map +1 -1
  64. package/dist/services/actionflow/components/action_eb_update_compiler.js +22 -7
  65. package/dist/services/actionflow/components/action_eb_update_compiler.js.map +1 -1
  66. package/dist/services/actionflow/components/approve_create.d.ts.map +1 -1
  67. package/dist/services/actionflow/components/approve_create.js +6 -3
  68. package/dist/services/actionflow/components/approve_create.js.map +1 -1
  69. package/dist/services/actionflow/components/custom_set_form.d.ts.map +1 -1
  70. package/dist/services/actionflow/components/custom_set_form.js +35 -1
  71. package/dist/services/actionflow/components/custom_set_form.js.map +1 -1
  72. package/dist/services/actionflow/components/custom_trigger.d.ts.map +1 -1
  73. package/dist/services/actionflow/components/custom_trigger.js +4 -1
  74. package/dist/services/actionflow/components/custom_trigger.js.map +1 -1
  75. package/dist/services/actionflow/components/dml_execute.d.ts.map +1 -1
  76. package/dist/services/actionflow/components/dml_execute.js +7 -3
  77. package/dist/services/actionflow/components/dml_execute.js.map +1 -1
  78. package/dist/services/actionflow/components/document_actions.d.ts.map +1 -1
  79. package/dist/services/actionflow/components/document_actions.js +7 -3
  80. package/dist/services/actionflow/components/document_actions.js.map +1 -1
  81. package/dist/services/actionflow/components/hrm_actions.d.ts.map +1 -1
  82. package/dist/services/actionflow/components/hrm_actions.js +7 -3
  83. package/dist/services/actionflow/components/hrm_actions.js.map +1 -1
  84. package/dist/services/actionflow/components/multiple_execution.d.ts.map +1 -1
  85. package/dist/services/actionflow/components/multiple_execution.js +14 -5
  86. package/dist/services/actionflow/components/multiple_execution.js.map +1 -1
  87. package/dist/services/actionflow/components/output.js +1 -1
  88. package/dist/services/actionflow/components/output.js.map +1 -1
  89. package/dist/services/actionflow/components/send_message.js +2 -2
  90. package/dist/services/actionflow/components/send_message.js.map +1 -1
  91. package/dist/services/actionflow/components/workflow_actions.d.ts.map +1 -1
  92. package/dist/services/actionflow/components/workflow_actions.js +7 -3
  93. package/dist/services/actionflow/components/workflow_actions.js.map +1 -1
  94. package/dist/services/actionflow/context_preflight.d.ts.map +1 -1
  95. package/dist/services/actionflow/context_preflight.js +12 -2
  96. package/dist/services/actionflow/context_preflight.js.map +1 -1
  97. package/dist/services/actionflow/context_provider.d.ts.map +1 -1
  98. package/dist/services/actionflow/context_provider.js +21 -2
  99. package/dist/services/actionflow/context_provider.js.map +1 -1
  100. package/dist/services/actionflow/custom_trigger_context.d.ts +10 -0
  101. package/dist/services/actionflow/custom_trigger_context.d.ts.map +1 -0
  102. package/dist/services/actionflow/custom_trigger_context.js +46 -0
  103. package/dist/services/actionflow/custom_trigger_context.js.map +1 -0
  104. package/dist/services/actionflow/filter_rules.js +2 -2
  105. package/dist/services/actionflow/filter_rules.js.map +1 -1
  106. package/dist/services/actionflow/formula_service.d.ts +9 -1
  107. package/dist/services/actionflow/formula_service.d.ts.map +1 -1
  108. package/dist/services/actionflow/formula_service.js +59 -21
  109. package/dist/services/actionflow/formula_service.js.map +1 -1
  110. package/dist/services/actionflow/id_generator.d.ts +2 -0
  111. package/dist/services/actionflow/id_generator.d.ts.map +1 -1
  112. package/dist/services/actionflow/id_generator.js +13 -0
  113. package/dist/services/actionflow/id_generator.js.map +1 -1
  114. package/dist/services/actionflow/preflight_service.d.ts.map +1 -1
  115. package/dist/services/actionflow/preflight_service.js +6 -1
  116. package/dist/services/actionflow/preflight_service.js.map +1 -1
  117. package/dist/services/actionflow/workflow_compiler.d.ts.map +1 -1
  118. package/dist/services/actionflow/workflow_compiler.js +89 -35
  119. package/dist/services/actionflow/workflow_compiler.js.map +1 -1
  120. package/dist/services/auth-resp-condition.d.ts +35 -0
  121. package/dist/services/auth-resp-condition.d.ts.map +1 -0
  122. package/dist/services/auth-resp-condition.js +111 -0
  123. package/dist/services/auth-resp-condition.js.map +1 -0
  124. package/dist/services/condition-rules.d.ts.map +1 -1
  125. package/dist/services/condition-rules.js +4 -1
  126. package/dist/services/condition-rules.js.map +1 -1
  127. package/dist/services/esb-interface/document-parser.d.ts.map +1 -1
  128. package/dist/services/esb-interface/document-parser.js +62 -5
  129. package/dist/services/esb-interface/document-parser.js.map +1 -1
  130. package/dist/services/form-association.d.ts +19 -0
  131. package/dist/services/form-association.d.ts.map +1 -1
  132. package/dist/services/form-association.js +64 -0
  133. package/dist/services/form-association.js.map +1 -1
  134. package/dist/services/form-button-action.d.ts +7 -0
  135. package/dist/services/form-button-action.d.ts.map +1 -1
  136. package/dist/services/form-button-action.js +11 -1
  137. package/dist/services/form-button-action.js.map +1 -1
  138. package/dist/services/form-create.d.ts +4 -0
  139. package/dist/services/form-create.d.ts.map +1 -1
  140. package/dist/services/form-create.js +18 -12
  141. package/dist/services/form-create.js.map +1 -1
  142. package/dist/services/form-field-build.d.ts +71 -0
  143. package/dist/services/form-field-build.d.ts.map +1 -0
  144. package/dist/services/form-field-build.js +436 -0
  145. package/dist/services/form-field-build.js.map +1 -0
  146. package/dist/services/form-field-patch.d.ts +3 -1
  147. package/dist/services/form-field-patch.d.ts.map +1 -1
  148. package/dist/services/form-field-patch.js +23 -9
  149. package/dist/services/form-field-patch.js.map +1 -1
  150. package/dist/services/form-formula-linkage.d.ts +349 -0
  151. package/dist/services/form-formula-linkage.d.ts.map +1 -0
  152. package/dist/services/form-formula-linkage.js +546 -0
  153. package/dist/services/form-formula-linkage.js.map +1 -0
  154. package/dist/services/form-layout-insert.d.ts +84 -0
  155. package/dist/services/form-layout-insert.d.ts.map +1 -0
  156. package/dist/services/form-layout-insert.js +244 -0
  157. package/dist/services/form-layout-insert.js.map +1 -0
  158. package/dist/services/form-linkage-condition.d.ts +159 -0
  159. package/dist/services/form-linkage-condition.d.ts.map +1 -0
  160. package/dist/services/form-linkage-condition.js +307 -0
  161. package/dist/services/form-linkage-condition.js.map +1 -0
  162. package/dist/services/form-physical-table.d.ts +115 -0
  163. package/dist/services/form-physical-table.d.ts.map +1 -0
  164. package/dist/services/form-physical-table.js +153 -0
  165. package/dist/services/form-physical-table.js.map +1 -0
  166. package/dist/services/formula-detail.d.ts +98 -6
  167. package/dist/services/formula-detail.d.ts.map +1 -1
  168. package/dist/services/formula-detail.js +208 -26
  169. package/dist/services/formula-detail.js.map +1 -1
  170. package/dist/services/matrix-rows.d.ts +43 -0
  171. package/dist/services/matrix-rows.d.ts.map +1 -0
  172. package/dist/services/matrix-rows.js +104 -0
  173. package/dist/services/matrix-rows.js.map +1 -0
  174. package/dist/services/org-department.d.ts +114 -0
  175. package/dist/services/org-department.d.ts.map +1 -0
  176. package/dist/services/org-department.js +209 -0
  177. package/dist/services/org-department.js.map +1 -0
  178. package/dist/services/org-employee.d.ts +191 -0
  179. package/dist/services/org-employee.d.ts.map +1 -0
  180. package/dist/services/org-employee.js +400 -0
  181. package/dist/services/org-employee.js.map +1 -0
  182. package/dist/services/role-member.d.ts +99 -0
  183. package/dist/services/role-member.d.ts.map +1 -0
  184. package/dist/services/role-member.js +206 -0
  185. package/dist/services/role-member.js.map +1 -0
  186. package/dist/services/workflow-info.d.ts +83 -0
  187. package/dist/services/workflow-info.d.ts.map +1 -0
  188. package/dist/services/workflow-info.js +97 -0
  189. package/dist/services/workflow-info.js.map +1 -0
  190. package/dist/services/workflow-odoc-setting.d.ts +1 -1
  191. package/dist/services/workflow-odoc-setting.d.ts.map +1 -1
  192. package/dist/services/workflow-odoc-setting.js +15 -6
  193. package/dist/services/workflow-odoc-setting.js.map +1 -1
  194. package/dist/utils/column-name.d.ts +24 -0
  195. package/dist/utils/column-name.d.ts.map +1 -1
  196. package/dist/utils/column-name.js +16 -0
  197. package/dist/utils/column-name.js.map +1 -1
  198. package/ecodecmd/package.json +1 -2
  199. package/ecodecmd/src/loaddep.js +19 -9
  200. package/package.json +4 -4
  201. package/skill-docs/SKILL.md +13 -9
  202. package/skill-docs/_catalog.md +86 -81
  203. package/skill-docs/actionflows/SKILL.md +173 -170
  204. package/skill-docs/actionflows/references/components/logic/Output.md +1 -1
  205. package/skill-docs/actionflows/references/components/trigger/CustomTrigger.md +32 -0
  206. package/skill-docs/actionflows/references/components/workflow/ApproveCreate.md +1 -1
  207. package/skill-docs/references/common/guide-plan-template.md +9 -3
  208. package/skill-docs/references/common/ref-cli-commands.md +54 -23
  209. package/skill-docs/references/esb-interface/guide-import.md +4 -1
  210. package/skill-docs/references/form/guide-create-form.md +13 -0
  211. package/skill-docs/references/form/guide-field-linkage.md +2 -0
  212. package/skill-docs/references/form/guide-form-association.md +1 -1
  213. package/skill-docs/references/form/guide-form-formula-functions.md +36 -0
  214. package/skill-docs/references/form/guide-form-layout-incremental.md +75 -0
  215. package/skill-docs/references/form/guide-form-linkage.md +62 -4
  216. package/skill-docs/references/form/guide-form-physical-table.md +146 -0
  217. package/skill-docs/references/form/guide-formula-linkage.md +250 -0
  218. package/skill-docs/references/form/ref-form-fields.md +6 -0
  219. package/skill-docs/references/hrm/guide-matrix.md +79 -16
  220. package/skill-docs/references/hrm/guide-org-employee.md +124 -0
  221. package/skill-docs/references/report/cell-config-echo-guide.md +1 -1
  222. package/skill-docs/references/report/dataset-query-filter-guide.md +3 -3
  223. package/skill-docs/references/report/date-range-query-reference.md +2 -2
  224. package/skill-docs/references/report/query-condition-api-guide.md +11 -11
  225. package/skill-docs/references/report/report-designer-feature-map.md +1 -1
  226. package/skill-docs/references/report/report-permission-guide.md +2 -2
  227. package/skill-docs/references/workflow/guide-workflow-auth-resp.md +48 -22
  228. package/skill-docs/references/workflow/guide-workflow-odoc-setting.md +248 -0
  229. package/skill-docs/references/workflow/guide-workflow-odoc.md +23 -51
  230. package/skill-docs/references/workflow/guide-workflow.md +1 -1
@@ -1,14 +1,21 @@
1
1
  import { writeFileSync, existsSync, readFileSync } from 'fs';
2
2
  import { AuthSession } from '../core/auth.js';
3
3
  import { ApiClient } from '../core/client.js';
4
- import { toColumnName, toTableName } from '../utils/column-name.js';
5
- import { createCustomPhysicalForm, addInitialFieldsToCustomPhysicalForm, placeholderFromFormat, reapplyGroupLayout } from '../services/form-create.js';
6
- import { FormSubDetailService, resolveDetailTable } from '../services/form-sub-detail.js';
4
+ import { toColumnName, toTableName, resolveFieldKeys } from '../utils/column-name.js';
5
+ import { createCustomPhysicalForm, addInitialFieldsToCustomPhysicalForm, placeholderFromFormat, reapplyGroupLayout, queryFormStorageSetting } from '../services/form-create.js';
6
+ import { FormSubDetailService } from '../services/form-sub-detail.js';
7
7
  import { RICH_TYPE_MAP, getRichType, richIdUnits, BROWSER_KINDS } from '../services/rich-types.js';
8
8
  import { dropFieldComponents, prepareLayoutForSave } from '../services/form-field-drop.js';
9
9
  import { describeEditBaseFailure, resolveEditBase as resolveEditBasePure } from '../services/form-edit-base.js';
10
10
  import { applyFieldPatches, platformFieldShape } from '../services/form-field-patch.js';
11
11
  import { ensureDisplayColumns } from '../services/form-ebuilder-columns.js';
12
+ import { insertFieldIntoLayout, patchFieldLayout } from '../services/form-layout-insert.js';
13
+ import { buildLinkageAddPayload, addLinkage, resolveEffect, readLinkageConfig } from '../services/form-linkage-condition.js';
14
+ // 字段 jsonObj 的构造器(含 SYSTEM_FIELD_IDS / defaultEBuilderDataTitle)——
15
+ // 原先内联在下面 add-field 的 action 里,为了给「按物理表加字段」第二个入口复用而抽出去。
16
+ // 本文件仍有别处用这两个符号,所以在这里回导。
17
+ import { SYSTEM_FIELD_IDS, defaultEBuilderDataTitle, buildFieldJsonObject } from '../services/form-field-build.js';
18
+ import { listFormTables, readFieldRecords, mapFieldsToTables } from '../services/form-physical-table.js';
12
19
  import { buildFormulaParameters, defaultReturnType } from '../services/formula-detail.js';
13
20
  import { registerOptionTemplateCommand } from './form-option-template.js';
14
21
  /**
@@ -20,7 +27,7 @@ import { registerOptionTemplateCommand } from './form-option-template.js';
20
27
  const CK_CANON = {
21
28
  textarea: 'TextArea', ebuilder: 'EBuilder', datetime: 'DateTime',
22
29
  idcard: 'IDCard', mobile: 'Mobile', email: 'Email', phone: 'Phone',
23
- employee: 'Employee', department: 'Department', location: 'Location',
30
+ employee: 'Employee', department: 'Department', subcompany: 'SubCompany', location: 'Location',
24
31
  switch: 'Switch', select: 'Select', checkbox: 'CheckBox', radiobox: 'RadioBox',
25
32
  money: 'Money', number: 'Number', date: 'Date', text: 'Text',
26
33
  daterange: 'DateRange', detailtable: 'DetailTable', city: 'City',
@@ -84,12 +91,6 @@ function assertFieldOptions(fieldName, options) {
84
91
  }
85
92
  });
86
93
  }
87
- const SYSTEM_FIELD_IDS = new Set([
88
- 'name', 'creator', 'create_time', 'update_time', 'id',
89
- 'updater', 'data_status', 'flow_status', 'current_step',
90
- 'batchpubtags', 'batchmytags', 'operate', 'data_read_status',
91
- 'flow_system_number',
92
- ]);
93
94
  /** 系统字段的中文名 → 物理字段 id(form read 实测的平台自带字段)。建表时字段与此完全重名 → 跳过新建、直接复用系统字段。 */
94
95
  const SYSTEM_FIELD_NAME_TO_ID = {
95
96
  '标题': 'name',
@@ -114,14 +115,6 @@ const SYSTEM_FIELD_CFG_ID = {
114
115
  '最后更新人': 'updater',
115
116
  '数据状态': 'dataStatus',
116
117
  };
117
- // EBuilder dataTitle(关联字段标题栏)默认解析:优先字段名含「名称」的业务字段,无则第一个业务字段。
118
- // 需求:默认选带"名称"二字的字段(如 客户名称/类型名称),没有就默认选第一个。
119
- function defaultEBuilderDataTitle(fields) {
120
- const isBiz = (id) => /^\d{15,}$/.test(id) && !SYSTEM_FIELD_IDS.has(id);
121
- const biz = (fields || []).filter((f) => isBiz(String(f.fieldId ?? '')));
122
- const byName = biz.find((f) => String(f.fieldName ?? f.title ?? '').includes('名称'));
123
- return byName?.fieldId || biz[0]?.fieldId || 'name';
124
- }
125
118
  /**
126
119
  * 只读地看一眼 `getFields` 眼里这个字段长什么样 —— **仅用于把错误信息说清楚,绝不作为写回基底**。
127
120
  *
@@ -281,6 +274,22 @@ export function registerFormCommands(program) {
281
274
  }
282
275
  catch { /* 布局属性补读失败不影响主流程 */ }
283
276
  }
277
+ // 所属物理表(设计器「字段管理」的主表/其他)—— getFields 不带这个信息,
278
+ // 得用 getFormAGroupList(表清单)+ getFormFieldPage(字段记录上的 formTableId)拼出来。
279
+ if (formId && cmdOpts?.appId) {
280
+ try {
281
+ const scope = { formId, appId: cmdOpts.appId, objId };
282
+ const tables = await listFormTables(client, baseUrl, cmdOpts.appId, scope);
283
+ const records = await readFieldRecords(client, baseUrl, cmdOpts.appId, scope);
284
+ const byField = mapFieldsToTables(records, tables);
285
+ for (const f of allFields) {
286
+ const t = byField[String(f.fieldId)];
287
+ if (t)
288
+ f.formTable = { id: t.formTableId, name: t.name, type: t.tableType, tableName: t.tableName };
289
+ }
290
+ }
291
+ catch { /* 物理表归属读不到不影响主流程 */ }
292
+ }
284
293
  if (cmdOpts?.out) {
285
294
  // Write JSON to file with UTF-8 encoding, bypassing terminal encoding
286
295
  writeFileSync(cmdOpts.out, JSON.stringify(allFields, null, 2), 'utf-8');
@@ -291,10 +300,13 @@ export function registerFormCommands(program) {
291
300
  }
292
301
  else {
293
302
  console.log(`表单 ${objId} 共 ${fields.length} 个主表字段${subFields.length ? ` + ${subFields.length} 个明细表字段` : ''}:`);
303
+ // 主表那排 chip 上写的是表单名,逐行重复没信息量,显示成「主表」;附属物理表用它的真名
304
+ const tableLabel = (f) => (f.formTable ? (f.formTable.type === 'MAIN' ? '主表' : f.formTable.name) : '');
294
305
  for (const f of fields) {
295
306
  if (SYSTEM_FIELD_IDS.has(f.fieldId))
296
307
  continue;
297
- console.log(` [${f.fieldKey || f.fieldType}] ${f.fieldName} (fieldId: ${f.fieldId || f.id})`);
308
+ const t = tableLabel(f);
309
+ console.log(` [${f.fieldKey || f.fieldType}] ${f.fieldName} (fieldId: ${f.fieldId || f.id})${t ? ` · 物理表: ${t}` : ''}`);
298
310
  if (f.options)
299
311
  console.log(` 选项: ${JSON.stringify(f.options)}`);
300
312
  if (f.optionTemplate)
@@ -354,6 +366,8 @@ export function registerFormCommands(program) {
354
366
  + 'valueKey 须为数字、省略则平台生成)')
355
367
  .option('--menu', '建表后添加台账菜单(默认不加;漏加可用 `e10-cli menu add-table` 补)')
356
368
  .option('--layout <type>', '表单布局类型:FLOW(流式)/EXCEL(表格,缺省)。布局类型建表后不可切换', 'EXCEL')
369
+ .option('--table-name <name>', '自定义数据库表名。平台前缀(如 uf_)由 CLI 自动补,传 uf_xxx 或只传自定义部分都可以;'
370
+ + '须字母开头、只含字母数字下划线。缺省按表单名拼音首字母 + 随机数生成')
357
371
  .action(async (appId, name, cmdOpts) => {
358
372
  const opts = program.opts();
359
373
  const auth = await AuthSession.fromOptions(opts);
@@ -560,8 +574,10 @@ export function registerFormCommands(program) {
560
574
  if (f.formName)
561
575
  field.formName = f.formName;
562
576
  }
563
- field.dataKey = toColumnName(f.fieldName);
564
- field.columnName = field.dataKey;
577
+ // fields.json 可分别指定 dataKey / columnName;默认同值(都是拼音首字母)
578
+ const fKeys = resolveFieldKeys(f.fieldName, f);
579
+ field.dataKey = fKeys.dataKey;
580
+ field.columnName = fKeys.columnName;
565
581
  formFields.push(field);
566
582
  // DetailTable occupies 2 rows of layout space
567
583
  rowIdx += isDetail ? 2 : 1;
@@ -592,7 +608,17 @@ export function registerFormCommands(program) {
592
608
  formValues: { formName: name, formTitle: name, tableName, tableType: 'physical' },
593
609
  };
594
610
  // Use standard creation flow for controllable table names
595
- const tableSuffix = toTableName(name);
611
+ // --table-name 指定物理表名(uf_ 前缀由服务层 resolveCustomPhysicalTableName 补);缺省按表单名生成
612
+ const customTableName = cmdOpts.tableName?.trim();
613
+ if (customTableName) {
614
+ const setting = await queryFormStorageSetting(client, baseUrl, appId);
615
+ if (!setting.storageSettings.includes('physical')) {
616
+ console.error(`该应用未启用物理表(storageSettings=${setting.storageSettings.join('/') || '空'}),`
617
+ + '没有独立的数据库表名,--table-name 不适用');
618
+ process.exit(1);
619
+ }
620
+ }
621
+ const requestedTableName = customTableName || toTableName(name);
596
622
  const layoutType = String(cmdOpts.layout || 'EXCEL').toUpperCase();
597
623
  if (layoutType !== 'FLOW' && layoutType !== 'EXCEL') {
598
624
  console.error(`--layout 只支持 FLOW(流式) 或 EXCEL(表格),收到: ${cmdOpts.layout}`);
@@ -602,7 +628,7 @@ export function registerFormCommands(program) {
602
628
  let formId = '';
603
629
  let fieldGroups = [];
604
630
  try {
605
- const result = await createCustomPhysicalForm(client, baseUrl, appId, name, tableSuffix, false, layoutType);
631
+ const result = await createCustomPhysicalForm(client, baseUrl, appId, name, requestedTableName, false, layoutType);
606
632
  objId = result.objId;
607
633
  formId = result.formId;
608
634
  console.error(`表单已创建,ID: ${objId}` + (formId ? ` formId: ${formId}` : ''));
@@ -626,6 +652,9 @@ export function registerFormCommands(program) {
626
652
  // 文本最大长度(Text 缺省 100、TextArea 缺省 4000)+ 日期精度 format(Date/DateTime)需透传,
627
653
  // 否则 fields.json 标注的 maxLen/format 被丢(bug:长度默认 100 不生效、日期精度丢失)。
628
654
  maxLen: f.maxLen, format: f.format,
655
+ // 字段编码 dataKey / 数据库列名 columnName —— 两者是两个东西,可分别指定。
656
+ // 这里是**白名单透传**,漏一项就会静默丢(maxLen/format 历史上就漏过)。
657
+ dataKey: f.dataKey, columnName: f.columnName,
629
658
  }));
630
659
  const fieldResult = await addInitialFieldsToCustomPhysicalForm(client, baseUrl, appId, objId, formId, fieldDefsForStandard, auth.tenantKey, layoutType, systemOverrides);
631
660
  fieldGroups = fieldResult.fieldGroups || [];
@@ -769,7 +798,8 @@ export function registerFormCommands(program) {
769
798
  const colName = df?.columnName || df?.dataKey || '';
770
799
  fieldByName.set(f.fieldName, {
771
800
  fieldId: f.fieldId,
772
- dataKey: colName,
801
+ // dataKey 与 columnName 是两个东西,各自保留真值 —— 回写时不能互相顶替
802
+ dataKey: df?.dataKey || colName,
773
803
  columnName: colName,
774
804
  formTableId: df?.formTableId || formTableId,
775
805
  });
@@ -805,7 +835,7 @@ export function registerFormCommands(program) {
805
835
  openRelateView: false,
806
836
  title: ef.fieldName, type: 'EBuilder',
807
837
  dataCount: 0,
808
- dataKey: finfo.columnName,
838
+ dataKey: finfo.dataKey,
809
839
  systemField: true,
810
840
  columnTypeSwitch: false,
811
841
  appId,
@@ -1041,7 +1071,8 @@ export function registerFormCommands(program) {
1041
1071
  const colName = df?.columnName || df?.dataKey || '';
1042
1072
  rfByName.set(f.fieldName, {
1043
1073
  fieldId: f.fieldId,
1044
- dataKey: colName,
1074
+ // dataKey 与 columnName 是两个东西,各自保留真值
1075
+ dataKey: df?.dataKey || colName,
1045
1076
  columnName: colName,
1046
1077
  formTableId: df?.formTableId || rfFormTableId,
1047
1078
  });
@@ -1066,7 +1097,7 @@ export function registerFormCommands(program) {
1066
1097
  title: rf.fieldName,
1067
1098
  type: rf.componentKey === 'Text' ? 'Text' : rf.componentKey,
1068
1099
  dataCount: 0,
1069
- dataKey: myInfo.columnName,
1100
+ dataKey: myInfo.dataKey,
1070
1101
  systemField: true,
1071
1102
  readonlyTransLink: false,
1072
1103
  columnTypeSwitch: false,
@@ -1111,7 +1142,7 @@ export function registerFormCommands(program) {
1111
1142
  // (batch/save 初建不带 isSingle 到 layout comp)。之前只对 defaultValue:true 的跑 → 没标默认值
1112
1143
  // 的 Employee/Department 漏 isSingle → 平台默认多选(bug:标记单选仍多选)。现对所有 Employee/Department
1113
1144
  // 跑设 isSingle;defaultExtend(默认当前用户/部门)仅 defaultValue:true 时加。
1114
- const personFields = fieldDefs.filter(f => f.componentKey === 'Employee' || f.componentKey === 'Department');
1145
+ const personFields = fieldDefs.filter(f => f.componentKey === 'Employee' || f.componentKey === 'Department' || f.componentKey === 'SubCompany');
1115
1146
  if (personFields.length > 0) {
1116
1147
  const withDefault = personFields.filter(f => f.defaultValue === true);
1117
1148
  console.error(` 配置 ${personFields.length} 个 人员/部门 字段 isSingle${withDefault.length ? `(其中 ${withDefault.length} 个带默认当前用户/部门)` : ''}...`);
@@ -1140,10 +1171,12 @@ export function registerFormCommands(program) {
1140
1171
  const colName = perDetail?.columnName || perDetail?.dataKey || '';
1141
1172
  const colType = perDetail?.columnType || 'BIGINT';
1142
1173
  const isDept = pf.componentKey === 'Department';
1143
- const typeLabel = isDept ? 'Department' : 'Employee';
1144
- const labelCN = isDept ? '部门' : '人员';
1174
+ const isSubCompany = pf.componentKey === 'SubCompany';
1175
+ const typeLabel = isSubCompany ? 'SubCompany' : isDept ? 'Department' : 'Employee';
1176
+ const labelCN = isSubCompany ? '分部' : isDept ? '部门' : '人员';
1145
1177
  const jsonObj = {
1146
1178
  type: typeLabel,
1179
+ ...(isSubCompany ? { componentKey: 'Subcompany', defaultExtend: { type: 'custom', relation: 'current' } } : {}),
1147
1180
  title: pf.fieldName,
1148
1181
  isSingle: pf.multiple ? false : true,
1149
1182
  allList: [],
@@ -1154,7 +1187,8 @@ export function registerFormCommands(program) {
1154
1187
  pcDisplaySelectedCount: { type: 'default' },
1155
1188
  mobileDisplaySelectedCount: { type: 'default' },
1156
1189
  formTableId: perDetail?.formTableId || '',
1157
- dataKey: colName,
1190
+ // dataKey 与 columnName 是两个东西,各自保留真值
1191
+ dataKey: perDetail?.dataKey || colName,
1158
1192
  columnName: colName,
1159
1193
  fieldId: perField.fieldId,
1160
1194
  formId: resolvedFormId,
@@ -1190,7 +1224,7 @@ export function registerFormCommands(program) {
1190
1224
  const detailPersonDefs = fieldDefs
1191
1225
  .filter(f => f.componentKey === 'DetailTable' && Array.isArray(f.formFields))
1192
1226
  .flatMap(dt => (dt.formFields || [])
1193
- .filter(sf => sf.componentKey === 'Employee' || sf.componentKey === 'Department')
1227
+ .filter(sf => sf.componentKey === 'Employee' || sf.componentKey === 'Department' || sf.componentKey === 'SubCompany')
1194
1228
  .map(sf => ({ ...sf, __detailTable: dt.fieldName })));
1195
1229
  if (detailPersonDefs.length > 0) {
1196
1230
  const withDefault = detailPersonDefs.filter(sf => sf.defaultValue === true);
@@ -1241,14 +1275,16 @@ export function registerFormCommands(program) {
1241
1275
  continue;
1242
1276
  }
1243
1277
  const isDept = sfDef.componentKey === 'Department';
1244
- const typeLabel = isDept ? 'Department' : 'Employee';
1245
- const labelCN = isDept ? '部门' : '人员';
1278
+ const isSubCompany = sfDef.componentKey === 'SubCompany';
1279
+ const typeLabel = isSubCompany ? 'SubCompany' : isDept ? 'Department' : 'Employee';
1280
+ const labelCN = isSubCompany ? '分部' : isDept ? '部门' : '人员';
1246
1281
  // spread cur 全量(保留子字段所有结构,防最小 jsonObj 覆盖破坏)+ 设 isSingle。
1247
1282
  // 子字段单/多选由布局 comp 的 isSingle 控制(实测:getFields 的 multiSelect 是 dataset 元数据,
1248
1283
  // 不控制渲染;布局 comp isSingle 才是渲染源)。saveFormFieldNew 会同步更新布局 comp 的 isSingle。
1249
1284
  const jsonObj = {
1250
1285
  ...cur,
1251
1286
  type: typeLabel,
1287
+ ...(isSubCompany ? { componentKey: 'Subcompany', defaultExtend: { type: 'custom', relation: 'current' } } : {}),
1252
1288
  title: sfDef.fieldName,
1253
1289
  isSingle: sfDef.multiple ? false : true,
1254
1290
  formTableId: subFormTableId || cur.formTableId || '',
@@ -1324,7 +1360,8 @@ export function registerFormCommands(program) {
1324
1360
  ...(dtMinTargetId ? { min: { type: 'dateField', checkValue: String(dtMinTargetId) }, earlyDate: true } : {}),
1325
1361
  ...(dtMaxTargetId ? { max: { type: 'dateField', checkValue: String(dtMaxTargetId) }, lateDate: true } : {}),
1326
1362
  formTableId: dtDetail?.formTableId || '',
1327
- dataKey: colName, columnName: colName,
1363
+ // dataKey 与 columnName 是两个东西,各自保留真值
1364
+ dataKey: dtDetail?.dataKey || colName, columnName: colName,
1328
1365
  fieldId: dtField.fieldId, formId: resolvedFormId,
1329
1366
  showOrder: '0.00', groupId: '-1',
1330
1367
  };
@@ -1814,23 +1851,13 @@ export function registerFormCommands(program) {
1814
1851
  process.exit(1);
1815
1852
  const client = new ApiClient(auth);
1816
1853
  const baseUrl = auth.getBaseUrl();
1817
- // Parse config
1818
- let configStr = cmdOpts.config;
1819
- if (configStr.startsWith('@')) {
1820
- const { readFileSync, existsSync } = await import('fs');
1821
- const filePath = configStr.slice(1);
1822
- if (!existsSync(filePath)) {
1823
- console.error(`配置文件不存在: ${filePath}`);
1824
- process.exit(1);
1825
- }
1826
- configStr = readFileSync(filePath, 'utf-8');
1827
- }
1854
+ // Parse config(@文件 / 内联 JSON 两种写法与 `wf-form linkage` 共用同一份实现)
1828
1855
  let config;
1829
1856
  try {
1830
- config = JSON.parse(configStr);
1857
+ config = readLinkageConfig(cmdOpts.config);
1831
1858
  }
1832
- catch {
1833
- console.error('JSON 解析失败');
1859
+ catch (e) {
1860
+ console.error(e.message);
1834
1861
  process.exit(1);
1835
1862
  }
1836
1863
  // 校验联动 prop:必填且平台支持 hide/show/edit/readonly/required/hideLine(隐藏行,抓包实测平台枚举是 hideLine;
@@ -1859,59 +1886,21 @@ export function registerFormCommands(program) {
1859
1886
  console.error('无法获取 formId,请确认 appId 和 objId 正确');
1860
1887
  process.exit(1);
1861
1888
  }
1862
- // Build payload
1863
- const payload = {
1864
- fdEffect: {
1865
- formId,
1866
- effectModule: 'ebuilderform',
1867
- effectType: 'form',
1868
- effectName: '当前表单',
1869
- optionList: [{ effectValue: formId, effectName: '当前表单' }],
1870
- },
1871
- condition: {
1872
- targetId: formId,
1873
- targetName: formInfo?.formName || '表单',
1874
- targetModule: 'ebuilderform',
1875
- conditionType: 'FIELD',
1876
- conditionDetails: config.conditions.map((c) => ({
1877
- targetId: c.fieldId,
1878
- detailSymbol: c.operator || 'EQUAL',
1879
- sourceType: 'CONSTANT',
1880
- sourceContent: JSON.stringify({
1881
- formField: { id: c.fieldId },
1882
- ...(c.options ? { dataOptions: c.options } : { content: c.value }),
1883
- }),
1884
- })),
1885
- formulaId: '',
1886
- formulaName: '',
1887
- },
1888
- linkageDetails: config.actions.map((a) => ({
1889
- formId,
1890
- id: '0',
1891
- linkageObj: 'field',
1892
- linkageProp: a.prop, // hide, show, edit, readonly, required, hideRow
1893
- linkageShowValue: a.fields.map((f) => ({
1894
- dataKey: f.dataKey || '',
1895
- groupName: '表单字段',
1896
- address: f.isDetail ? '明细表' : '主表',
1897
- addressType: f.isDetail ? 'detail' : 'form',
1898
- groupId: 'formField',
1899
- name: f.name,
1900
- descType: f.fieldType || '',
1901
- id: f.fieldId,
1902
- content: f.name,
1903
- desc: f.desc || '',
1904
- })),
1905
- detailOptions: [],
1906
- })),
1907
- module: 'ebuilderform',
1908
- formId,
1909
- refId: formId,
1910
- enableState: '1',
1911
- linkageName: name,
1912
- customParam: { appId, objId },
1913
- };
1914
- const data = await client.post(`${baseUrl}/api/ebuilder/form/form/setting/viewlinkage/add?apid=${appId}`, payload, { ebbusinessid: formId });
1889
+ // 报文的拼法(尤其条件的算子与生效维度)收在 services/form-linkage-condition.ts ——
1890
+ // 那里记着「为空要写 NULL 且不能带 sourceContent」「表单布局维度的值取 layoutId」这类
1891
+ // 不看设计器实际报文就想不到的约束。
1892
+ const formName = formInfo?.formName || formInfo?.objName || '表单';
1893
+ const effect = await resolveEffect(client, baseUrl, formId, formName, config.effect);
1894
+ const { payload, warnings } = buildLinkageAddPayload({
1895
+ appId, objId, formId, formName, name,
1896
+ conditions: config.conditions || [],
1897
+ detailType: config.detailType,
1898
+ actions: config.actions || [],
1899
+ effect,
1900
+ });
1901
+ for (const w of warnings)
1902
+ console.error(` ⚠ ${w}`);
1903
+ const data = await addLinkage(client, baseUrl, appId, formId, payload);
1915
1904
  if (data?.code === 200 && data?.status) {
1916
1905
  const linkageId = data?.data || '';
1917
1906
  console.log(opts.json
@@ -2122,15 +2111,20 @@ export function registerFormCommands(program) {
2122
2111
  .option('--precision <n>', '小数位数(Number/Money 类型,缺省 Number=0 整数、Money=2、百分比=1)')
2123
2112
  .option('--percent', 'Number 百分比显示(numberType=percentage)')
2124
2113
  .option('--default <value>', '默认值(Text / TextArea / Number / Money,如 工时单价默认 1000)')
2125
- .option('--data-key <key>', '数据库字段名(默认按字段名生成拼音;字母数字下划线,不以数字开头)')
2114
+ .option('--data-key <key>', '字段编码 dataKey(默认按字段名生成拼音;字母数字下划线,不以数字开头)')
2115
+ .option('--column-name <name>', '数据库列名 columnName。**与 dataKey 是两个东西,可分别指定**;'
2116
+ + '不给时跟随 --data-key(两者都没给则同为拼音首字母)')
2126
2117
  .option('--desc <text>', '字段描述(设计器字段属性里的「描述」)')
2127
2118
  .option('--delimiter', 'Number/Money 千分位(Money 默认开,--no-delimiter 关闭)')
2128
2119
  .option('--capital', 'Number/Money 大写金额显示')
2129
2120
  .option('--sub-form-id <id>', '明细表 subFormId(在指定明细表里加字段)')
2130
- .option('--multiple', '多选(人员/部门/EBuilder/关联类字段/下拉Select;下拉多选=SelectMultiple 类型)')
2121
+ .option('--multiple', '多选(人员/部门/分部/EBuilder/关联类字段/下拉Select;下拉多选=SelectMultiple 类型)')
2131
2122
  .option('--max-len <n>', '文本最大长度(Text 缺省 100、TextArea 缺省 4000)')
2132
2123
  .option('--single-row', '独占一行显示(布局 singleRow:true,其余字段仍两列)')
2133
2124
  .option('--option-template <idOrName>', 'Select 绑定选项预设模板(ID 或名称;选项不内联,options=[])')
2125
+ .option('--required', '设为必填(同时写字段记录与布局组件,两处都写才完整)')
2126
+ .option('--reinit', '整表重建布局(initLayout)而不是增量插入:会换样式模板、重排所有已有位置、'
2127
+ + '并重建标签组件。只在自己建的表上用;不是自己建的表用默认的增量插入')
2134
2128
  .action(async (objId, appId, fieldName, fieldType, title, cmdOptsRaw) => {
2135
2129
  // fieldType 大小写归一(与建表一致):用户常传 "Textarea"/"Ebuilder",不归一则 type 存小写
2136
2130
  // "Textarea"(FLOW 渲染异常)、TextArea 分支漏跑。
@@ -2156,382 +2150,35 @@ export function registerFormCommands(program) {
2156
2150
  // DateRange 需要 3 个字段 ID(主 + 开始 + 结束),普通字段 1 个,富组件按 richIdUnits
2157
2151
  const isDateRange = fieldType === 'DateRange' || fieldType === 'DateInterval';
2158
2152
  const idCount = isDateRange ? 3 : richIdUnits(fieldType, {});
2159
- const genData = await client.post(`${baseUrl}/api/form/local/generatorIds?size=${idCount}&apid=${appId}`, {});
2153
+ // 多要一个:布局里字段占**两个** comp,值组件用 fieldId,标签组件的 `config.fieldId`
2154
+ // 是布局自己另生成的一个雪花 id(与字段定义无关,只需在布局内唯一)。
2155
+ const genData = await client.post(`${baseUrl}/api/form/local/generatorIds?size=${idCount + 1}&apid=${appId}`, {});
2160
2156
  const genIds = genData?.data?.generatorIds || [];
2161
2157
  const fieldId = genIds[0] || String(Date.now());
2162
2158
  const startId = genIds[1] || '';
2163
2159
  const endId = genIds[2] || '';
2160
+ const labelFieldId = genIds[idCount] || String(Date.now());
2164
2161
  const cmdOpts = cmdOptsRaw || {};
2165
2162
  const sformId = cmdOpts.sformId || '';
2166
2163
  const dataTitle = cmdOpts.dataTitle || '';
2167
2164
  const formName = cmdOpts.formName || '';
2168
- // Generate column name: pinyin initials + timestamp suffix
2169
- const colName = toColumnName(fieldName);
2170
- // Build jsonObj based on field type
2171
- const jsonObj = {
2172
- type: fieldType,
2173
- title: title || fieldName,
2174
- formTableId, fieldId, groupId: '-1', formId,
2175
- showOrder: '0.00',
2176
- dataKey: colName,
2177
- columnName: colName,
2178
- };
2179
- // 明细表字段:--sub-form-id 时 jsonObj 带 subFormId —— **字段落点由它决定**。
2180
- // 必须用 resolveDetailTable 而不是直接查 getEbField:后者列不出空明细表,
2181
- // 用户把明细字段删光后就再也加不回去(详见 services/form-sub-detail.ts)。
2182
- if (cmdOpts.subFormId) {
2183
- const sub = await resolveDetailTable(client, baseUrl, appId, objId, formId, cmdOpts.subFormId);
2184
- if (!sub) {
2185
- console.error(`未找到明细表 subFormId=${cmdOpts.subFormId}(getEbField 与 querySubForms 均无此表,请确认 id)`);
2186
- process.exit(1);
2187
- }
2188
- Object.assign(jsonObj, { subFormId: cmdOpts.subFormId });
2189
- // 空表拿不到物理表 id,保持主表的值不动:实测带主表 formTableId 一样落进明细表。
2190
- if (sub.formTableId)
2191
- jsonObj.formTableId = sub.formTableId;
2192
- }
2193
- // 富组件:映射到平台真实 type/componentKey + 附加配置(对齐 batch/save/formField 抓包)。
2194
- // 之前 jsonObj 只有 type 没有 componentKey → 平台 FormFieldDto.getComponentKey() 为 null → NPE。
2195
- const rich = getRichType(fieldType);
2196
- if (rich) {
2197
- jsonObj.type = rich.platformType;
2198
- jsonObj.componentKey = rich.platformComponentKey;
2199
- if (rich.build) {
2200
- const extra = richIdUnits(fieldType, {}) - 1;
2201
- let extraIds = [];
2202
- if (extra > 0) {
2203
- const extraRes = await client.post(`${baseUrl}/api/form/local/generatorIds?size=${extra}&apid=${appId}`, {});
2204
- extraIds = (extraRes?.data?.generatorIds || []).map(String);
2205
- }
2206
- const extraCfg = await rich.build({
2207
- def: {
2208
- multiple: cmdOpts.multiple,
2209
- browserType: cmdOpts.browserType,
2210
- browserFormSource: cmdOpts.browserFormSource,
2211
- }, fieldId, formId, formTableId, dataKey: colName, columnName: colName,
2212
- showOrder: '0.00', extraIds,
2213
- });
2214
- Object.assign(jsonObj, extraCfg);
2215
- }
2216
- }
2217
- // 非富组件也补 componentKey(平台要求,否则 FormFieldDto.getComponentKey() 为 null → NPE)。
2218
- // 富组件已在上方设置 platformComponentKey;此处 fallback 到类型名(平台对常见类型有归一容错)。
2219
- if (!jsonObj.componentKey)
2220
- jsonObj.componentKey = fieldType;
2221
- // Number/Money decimal precision: --precision 指定,缺省 Number=0 整数、Money=2、百分比=1
2222
- // 百分比:--percent → numberType=percentage;默认值:--default → content+isDefault
2223
- // 实测:设默认值必须连 isDelimiter/stepMode/step/isCapital 一起,否则平台丢默认(只设 content+isDefault 不落库)
2224
- if (fieldType === 'Number' || fieldType === 'Money') {
2225
- const defaultPrec = fieldType === 'Money' ? '2' : '0';
2226
- const isPercent = fieldType === 'Number' && cmdOpts.percent === true;
2227
- // 注意:commander 把 --default 映射到 opts.default(非 defaultValue)
2228
- const hasDefault = cmdOpts.default !== undefined && cmdOpts.default !== null && cmdOpts.default !== '';
2229
- // 千分位:Money 默认开(--no-delimiter 关闭)、Number 默认关(--delimiter 开启)
2230
- const isDelim = fieldType === 'Money' ? cmdOpts.delimiter !== false : cmdOpts.delimiter === true;
2231
- const isCap = cmdOpts.capital === true;
2232
- const patch = {
2233
- pointSize: cmdOpts.precision !== undefined ? cmdOpts.precision : (isPercent ? '1' : defaultPrec),
2234
- isDefault: hasDefault,
2235
- isDelimiter: isDelim,
2236
- isCapital: isCap,
2237
- capitalFormat: 'onlyUpperCase',
2238
- capitalPosition: 'outside',
2239
- capitalSetting: 'system',
2240
- capitalPrecision: 'yuan',
2241
- isExchangeRates: false,
2242
- };
2243
- if (fieldType === 'Number')
2244
- patch.numberType = isPercent ? 'percentage' : 'number';
2245
- // content 始终带(无默认值时空串)——浏览器抓包 Number 字段 getFormField 恒有 content:"",
2246
- // 缺 content 在 FLOW 渲染异常(与多行文本同因)。
2247
- if (fieldType === 'Number')
2248
- patch.content = hasDefault ? String(cmdOpts.default) : '';
2249
- if (hasDefault) {
2250
- patch.stepMode = false;
2251
- patch.step = 1;
2252
- }
2253
- Object.assign(jsonObj, patch);
2254
- }
2255
- if (fieldType === 'TextArea') {
2256
- // 多行文本:对齐浏览器抓包——content 恒 ""(无默认值时空串)、maxLen=4000。
2257
- // 缺 content 在 FLOW 渲染异常;maxLen 不发则前端最大长度回显异常。
2258
- Object.assign(jsonObj, { content: '', maxLen: 4000 });
2259
- }
2260
- if (fieldType === 'EInvoice') {
2261
- Object.assign(jsonObj, {
2262
- isSingle: false,
2263
- invoiceType: 1,
2264
- });
2265
- delete jsonObj.maxLen;
2266
- delete jsonObj.unique;
2267
- delete jsonObj.scan;
2268
- delete jsonObj.readonlyTransLink;
2269
- }
2270
- else if (fieldType === 'Date' || fieldType === 'DateTime') {
2271
- // 日期:默认精度 年月日(yyyy-MM-dd,对齐浏览器抓包);DateTime:默认带时:分(yyyy-MM-dd HH:mm 不含秒)。
2272
- // 默认值不填(isDefault:false);--format 覆盖精度。
2273
- const isDt = fieldType === 'DateTime';
2274
- const fmt = cmdOpts.format || (isDt ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd');
2275
- Object.assign(jsonObj, {
2276
- type: 'Date',
2277
- format: fmt,
2278
- placeholder: placeholderFromFormat(fmt),
2279
- timePanelStyle: { type: 'system' },
2280
- isDefault: false,
2281
- });
2282
- // 日期限制:--min-field 最早不得早于 / --max-field 最晚不得晚于另一字段(对齐抓包 min.dateField)。
2283
- // 目标字段 fieldId 从 getFields mainFields 按字段名解析。
2284
- if (cmdOpts.minField || cmdOpts.maxField) {
2285
- try {
2286
- const tfd = await client.post(`${baseUrl}/api/ebuilder/form/dataset/v1/getFields`, {
2287
- objId, userid: auth.userId, tenantKey: auth.tenantKey,
2288
- });
2289
- const tfields = tfd?.data?.mainFields || [];
2290
- const minTarget = cmdOpts.minField ? tfields.find((f) => f.fieldName === cmdOpts.minField)?.fieldId : '';
2291
- const maxTarget = cmdOpts.maxField ? tfields.find((f) => f.fieldName === cmdOpts.maxField)?.fieldId : '';
2292
- if (minTarget) {
2293
- jsonObj.min = { type: 'dateField', checkValue: String(minTarget) };
2294
- jsonObj.earlyDate = true;
2295
- }
2296
- if (maxTarget) {
2297
- jsonObj.max = { type: 'dateField', checkValue: String(maxTarget) };
2298
- jsonObj.lateDate = true;
2299
- }
2300
- }
2301
- catch { /* 解析失败则不加限制 */ }
2302
- }
2303
- delete jsonObj.maxLen;
2304
- delete jsonObj.unique;
2305
- delete jsonObj.scan;
2306
- delete jsonObj.readonlyTransLink;
2307
- }
2308
- else if (fieldType === 'City') {
2309
- // 城市/地区选择(对齐浏览器 payload:region 模式 + 系统级联层级 + 字母索引)
2310
- Object.assign(jsonObj, {
2311
- isSingle: false,
2312
- levelSetting: 'system',
2313
- isDefault: false,
2314
- useLetterIndex: true,
2315
- mode: 'region',
2316
- });
2317
- delete jsonObj.maxLen;
2318
- delete jsonObj.unique;
2319
- delete jsonObj.scan;
2320
- delete jsonObj.readonlyTransLink;
2321
- }
2322
- else if (fieldType === 'EBuilder') {
2323
- if (!sformId) {
2324
- console.error('EBuilder 类型必须指定 --sform-id(目标表单 objId)');
2325
- process.exit(1);
2326
- }
2327
- // Resolve formName from target form if not provided
2328
- let resolvedFormName = formName;
2329
- if (!resolvedFormName) {
2330
- try {
2331
- const tlist = await client.get(`${baseUrl}/api/bs/ebuilder/form/obj/getList/${appId}?apid=${appId}`, { ebbusinessid: appId });
2332
- const tf = (tlist?.data || []).find((f) => String(f.id) === String(sformId));
2333
- resolvedFormName = tf?.formName || tf?.name || sformId;
2334
- }
2335
- catch {
2336
- resolvedFormName = sformId;
2337
- }
2338
- }
2339
- // Resolve dataTitle:纯数字 fieldId 直接用;'name'/字段名 → 查目标表字段。
2340
- // 未传 --data-title 时按 name 处理:默认优先字段名含「名称」的业务字段(如 客户名称/类型名称),无则第一个业务字段。
2341
- let resolvedDataTitle = dataTitle || 'name';
2342
- if (!/^\d{15,}$/.test(resolvedDataTitle)) {
2343
- try {
2344
- const tfd = await client.post(`${baseUrl}/api/ebuilder/form/dataset/v1/getFields`, {
2345
- objId: sformId, userid: auth.userId, tenantKey: auth.tenantKey,
2346
- });
2347
- const tfields = tfd?.data?.mainFields || [];
2348
- if (resolvedDataTitle === 'name') {
2349
- resolvedDataTitle = defaultEBuilderDataTitle(tfields);
2350
- }
2351
- else {
2352
- const m = tfields.find((f) => f.fieldName === resolvedDataTitle)
2353
- || tfields.find((f) => String(f.fieldName || '').includes(resolvedDataTitle));
2354
- resolvedDataTitle = m?.fieldId || resolvedDataTitle;
2355
- }
2356
- }
2357
- catch { /* 解析失败保留原值 */ }
2358
- }
2359
- Object.assign(jsonObj, {
2360
- sformId,
2361
- dataTitle: resolvedDataTitle || 'name',
2362
- formName: resolvedFormName,
2363
- virtualForm: false,
2364
- displayType: 'line',
2365
- isAddForRelation: true,
2366
- isSingle: cmdOpts.multiple ? false : true, // 默认单选;--multiple 多选
2367
- viewDataDetail: true,
2368
- pcDisplaySelectedCount: { type: 'default' },
2369
- mobileDisplaySelectedCount: { type: 'default' },
2370
- quickAddParams: [],
2371
- relateViewData: [],
2372
- relateField: null,
2373
- appId,
2374
- virtualTable: false,
2375
- });
2376
- }
2377
- else if (fieldType === 'Flow') {
2378
- // 浏览-流程字段(对齐浏览器 payload:type=Flow + relateExt.module=workflow)
2379
- if (!sformId) {
2380
- console.error('Flow 类型必须指定 --sform-id(目标流程绑定的表单 objId)');
2381
- process.exit(1);
2382
- }
2383
- let resolvedFormName = formName;
2384
- if (!resolvedFormName) {
2385
- try {
2386
- const tlist = await client.get(`${baseUrl}/api/bs/ebuilder/form/obj/getList/${appId}?apid=${appId}`, { ebbusinessid: appId });
2387
- const tf = (tlist?.data || []).find((f) => String(f.id) === String(sformId));
2388
- resolvedFormName = tf?.formName || tf?.name || sformId;
2389
- }
2390
- catch {
2391
- resolvedFormName = sformId;
2392
- }
2393
- }
2394
- Object.assign(jsonObj, {
2395
- isAddForRelation: true,
2396
- sformId,
2397
- formName: resolvedFormName,
2398
- conditionOperate: 'and',
2399
- filterParams: '{}',
2400
- filterQueryParams: '{}',
2401
- columnFields: [],
2402
- displayType: 'line',
2403
- viewDataDetail: true,
2404
- pcDisplaySelectedCount: { type: 'default' },
2405
- mobileDisplaySelectedCount: { type: 'default' },
2406
- relateExt: { module: 'workflow' },
2407
- relateField: null,
2408
- quickAddParams: [],
2409
- });
2410
- }
2411
- else if (isDateRange) {
2412
- // 日期区间:补 start/end 两个 DateComponent + rangeInterval(对齐浏览器 payload)
2413
- const format = cmdOpts.format || 'yyyy-MM-dd';
2414
- const rangeTitle = title || fieldName;
2415
- const start = {
2416
- componentKey: 'DateComponent', content: '', describe: '', earlyDate: 'false',
2417
- format, isDefault: 'false', lateDate: 'false',
2418
- placeholder: placeholderFromFormat(format),
2419
- position: '', relateMultiple: 'false', required: 'false', size: 'medium',
2420
- title: `${rangeTitle}(开始时间)`, fieldId: startId,
2421
- min: { type: 'currentHour' }, max: { type: 'currentHour' },
2422
- };
2423
- const end = { ...start, title: `${rangeTitle}(结束时间)`, fieldId: endId };
2424
- Object.assign(jsonObj, {
2425
- isDefault: false,
2426
- start, end,
2427
- hasDisplayFormat: true,
2428
- rangeInterval: { limitType: 'max', timeUnit: 'day' },
2429
- });
2430
- }
2431
- else if (fieldType === 'IDCard') {
2432
- // 身份证号:平台原生简单字段,浏览器抓包 payload 极简(type/title/formTableId/dataKey/
2433
- // columnName/fieldId/formId/showOrder/groupId 即全部)——无 componentKey、无文本长度配置。
2434
- delete jsonObj.componentKey;
2435
- delete jsonObj.maxLen;
2436
- delete jsonObj.unique;
2437
- delete jsonObj.scan;
2438
- delete jsonObj.readonlyTransLink;
2439
- }
2440
- else {
2441
- // 文本最大长度:--max-len 指定,缺省 Text=100、TextArea=4000(对齐浏览器抓包;TextArea 在上面分支已设 content:"")
2442
- const defaultMaxLen = fieldType === 'TextArea' ? 4000 : 100;
2443
- const maxLen = cmdOpts.maxLen !== undefined ? Number(cmdOpts.maxLen) : defaultMaxLen;
2444
- Object.assign(jsonObj, {
2445
- maxLen: Number.isFinite(maxLen) && maxLen > 0 ? maxLen : defaultMaxLen,
2446
- unique: false, scan: false,
2447
- readonlyTransLink: false,
2448
- });
2449
- }
2450
- // 人员/部门多选:默认单选,--multiple 改多选(isSingle:false)。EBuilder 已在上文分支设过。
2451
- if (fieldType === 'Employee' || fieldType === 'Department') {
2452
- jsonObj.isSingle = cmdOpts.multiple ? false : true;
2453
- }
2454
- // 下拉多选是独立类型 SelectMultiple(非 Select + 开关)。浏览器抓包:type:"SelectMultiple",
2455
- // 无 componentKey/maxLen/unique/scan(else 分支给 Select 加的 junk 要清掉),options 单独配。
2456
- if (fieldType === 'Select' && cmdOpts.multiple) {
2457
- jsonObj.type = 'SelectMultiple';
2458
- jsonObj.componentKey = 'SelectMultiple';
2459
- delete jsonObj.maxLen;
2460
- delete jsonObj.unique;
2461
- delete jsonObj.scan;
2462
- delete jsonObj.readonlyTransLink;
2463
- }
2464
- // Select 绑定选项预设模板:--option-template(ID 或名称)。
2465
- // 浏览器抓包(2026-09-09):jsonObj 带 templateId+templateName+publicTemplate:false+options:[](选项不内联),
2466
- // 平台渲染时按模板拉选项。模板管理见 `form option-template`。
2467
- if (fieldType === 'Select' && cmdOpts.optionTemplate) {
2468
- // 先清 else 分支给文本类加的 junk + options 占位(模板字段不发选项)
2469
- delete jsonObj.maxLen;
2470
- delete jsonObj.unique;
2471
- delete jsonObj.scan;
2472
- delete jsonObj.readonlyTransLink;
2473
- const tplIdOrName = cmdOpts.optionTemplate;
2474
- // 解析模板:纯数字按 ID 直查详情;否则在 custom+system 列表按名称精确匹配
2475
- let tplId = '';
2476
- let tplName = '';
2477
- const readTpl = async (id) => {
2478
- const r = await client.post(`${baseUrl}/api/form/datasources/queryDataSourcesByTemplateValueKey`, { templateId: id });
2479
- return r?.formDataTemplate?.id ? r.formDataTemplate : null;
2480
- };
2481
- if (/^\d+$/.test(tplIdOrName)) {
2482
- const t = await readTpl(tplIdOrName);
2483
- if (!t) {
2484
- console.error(`选项模板 ${tplIdOrName} 不存在`);
2485
- process.exit(1);
2486
- }
2487
- tplId = String(t.id);
2488
- tplName = String(t.name);
2489
- }
2490
- else {
2491
- for (const type of ['custom', 'system']) {
2492
- const list = await client.post(`${baseUrl}/api/form/datasources/queryDataList`, { pageNo: 1, pageSize: 100, name: tplIdOrName, type });
2493
- const hit = (list?.templatePage?.result || []).find((r) => r.name === tplIdOrName);
2494
- if (hit) {
2495
- tplId = String(hit.id);
2496
- tplName = String(hit.name);
2497
- break;
2498
- }
2499
- }
2500
- if (!tplId) {
2501
- console.error(`未找到名为「${tplIdOrName}」的选项模板(custom+system 均已查)`);
2502
- process.exit(1);
2503
- }
2504
- }
2505
- Object.assign(jsonObj, {
2506
- hideScore: true,
2507
- optionBackgroundStyle: 'default',
2508
- options: [],
2509
- templateScore: {},
2510
- currentLevel: '1',
2511
- templateId: tplId,
2512
- templateName: tplName,
2513
- publicTemplate: false,
2514
- parentId: [],
2515
- });
2516
- if (cmdOpts.multiple) {
2517
- console.error('提示:模板下拉当前按单选(currentLevel=1)绑定;多选+模板组合未验证');
2518
- }
2519
- }
2520
- // 默认值 / 数据库字段名 / 描述:与 edit-field 共用同一套补丁规则。
2521
- // 放在最后 —— 前面各类型分支会写 content(如 TextArea 恒置 ''),先打补丁会被冲掉。
2522
- const patch = applyFieldPatches(jsonObj, {
2523
- defaultValue: cmdOpts.default,
2165
+ // dataKey(字段编码)与 columnName(数据库列名)是两个东西,默认同值。
2166
+ // 只给 --data-key 时列名跟随(历史行为);给了 --column-name 才分开。
2167
+ const keys = resolveFieldKeys(fieldName, {
2524
2168
  dataKey: cmdOpts.dataKey,
2525
- desc: cmdOpts.desc,
2169
+ columnName: cmdOpts.columnName,
2170
+ });
2171
+ const colName = keys.dataKey; // 布局 comp 等仍按字段编码引用
2172
+ // 字段 jsonObj 的构造抽到 services/form-field-build.ts —— 与「按物理表加字段」共用同一份。
2173
+ // 原先这约 340 行内联在这里,多一个入口就会变成两处真相。
2174
+ const jsonObj = await buildFieldJsonObject({
2175
+ client, baseUrl, appId, objId,
2176
+ userId: auth.userId, tenantKey: auth.tenantKey,
2177
+ fieldType, fieldName, title, formId, formTableId,
2178
+ fieldId, startId, endId,
2179
+ dataKey: keys.dataKey, columnName: keys.columnName,
2180
+ opts: cmdOpts,
2526
2181
  });
2527
- if (patch.errors.length > 0) {
2528
- for (const e of patch.errors)
2529
- console.error(` ❌ ${e}`);
2530
- console.error(' 已中止,未做任何改动。');
2531
- process.exit(1);
2532
- }
2533
- for (const w of patch.warnings)
2534
- console.error(` ⚠ ${w}`);
2535
2182
  const payload = {
2536
2183
  formId, module: 'ebuilderform',
2537
2184
  customParam: { appId, objId },
@@ -2553,83 +2200,116 @@ export function registerFormCommands(program) {
2553
2200
  console.error(` 关联显示列设置失败: ${e.message}`);
2554
2201
  }
2555
2202
  }
2556
- // Always relayout so new field appears in the form
2557
- console.error(' 重新生成布局...');
2558
- try {
2559
- const preLayout = await client.post(`${baseUrl}/api/ebuilder/form/builder/getFormLayout`, {
2560
- formId, module: 'ebuilderform', customParam: {}, formTableIdList: [],
2561
- designerParam: { onlyLayout: false, convert: false }, form: { id: formId },
2562
- }, { ebbusinessid: formId });
2563
- const layoutId = preLayout?.data?.formLayout?.id || '';
2564
- const layoutMultiId = preLayout?.data?.formLayout?.multipleLayoutId || '';
2565
- const layoutName = preLayout?.data?.formLayoutMultiple?.name || preLayout?.data?.formLayout?.name || '';
2566
- const layoutType = preLayout?.data?.formLayoutMultiple?.layoutType || 'EXCEL';
2567
- const configRes = await client.post(`${baseUrl}/api/ebuilder/form/form/view/relate/getConfig`, {
2568
- formId, module: 'ebuilderform', apiModule: 'ebuilder/form',
2569
- layoutId, customParam: { terminal: '', objId: formId },
2570
- initMode: 5, groupFieldIds: [], designerParam: { sourceModule: 'ebuilderform' },
2571
- }, { ebbusinessid: formId });
2572
- const rawConfigs = configRes?.data?.configs || [];
2573
- // saveFormFieldNew fields may not appear in getConfig — manually append
2574
- const CK = {
2575
- Text: 'Text', Textarea: 'TextArea', Number: 'NumberComponent', Money: 'Money',
2576
- Date: 'DateComponent', DateTime: 'DateComponent', Select: 'Select', CheckBox: 'CheckBox', RadioBox: 'RadioBox',
2577
- Employee: 'Employee', Department: 'Department', EBuilder: 'Ebuilder',
2578
- Phone: 'Phone', Email: 'Email', Mobile: 'Mobile', Location: 'Location', Switch: 'Switch', IDCard: 'IDCard',
2579
- };
2580
- const newFieldConfig = {
2581
- id: fieldId, name: title || fieldName,
2582
- componentKey: jsonObj.componentKey || CK[fieldType] || fieldType,
2583
- display: true, show: true, readOnly: false,
2584
- required: false, disableEdit: false, singleRow: cmdOpts.singleRow === true,
2585
- isDelete: false, type: fieldType, dataKey: colName,
2586
- };
2587
- const existingIds = new Set(rawConfigs.map((c) => String(c.id)));
2588
- if (cmdOpts.subFormId) {
2589
- // 明细字段:加进对应 DetailTable 的 children(明细字段嵌在 DetailTable 里,不是顶层)
2590
- const dtComp = rawConfigs.find((c) => String(c.id) === String(cmdOpts.subFormId) && c.type === 'DetailTable');
2591
- if (dtComp) {
2592
- const childIds = new Set((dtComp.children || []).map((x) => String(x.id)));
2593
- if (!childIds.has(fieldId)) {
2594
- dtComp.children = dtComp.children || [];
2595
- dtComp.children.push({ ...newFieldConfig, showOrder: dtComp.children.length });
2203
+ // 布局落点:**默认走增量插入** —— 读快照 → 塞一对 comp → saveFormAndLayout,全程不碰
2204
+ // initLayout。initLayout 是整表重建(换样式模板、重排所有位置、把标签组件全换成新 id),
2205
+ // 对不是我们建的表是破坏性的;详见 services/form-layout-insert.ts。
2206
+ //
2207
+ // 增量失败**不自动回落**到 initLayout —— 那等于把「悄悄毁掉别人的布局」变成默认行为。
2208
+ // 把错误讲清楚,让用户自己决定要不要 --reinit。
2209
+ if (!cmdOpts.reinit) {
2210
+ try {
2211
+ const fieldDef = (await fetchFieldPageRecords(client, baseUrl, appId, formId))
2212
+ .find((f) => String(f.fieldId) === String(fieldId));
2213
+ if (!fieldDef)
2214
+ throw new Error('字段已建立,但读回字段定义失败');
2215
+ const placed = await insertFieldIntoLayout(client, baseUrl, formId, {
2216
+ fieldId, labelFieldId, title: title || fieldName, fieldDef,
2217
+ subFormId: cmdOpts.subFormId,
2218
+ // 对齐 initLayout 那条路对新字段的默认:可编辑、显示。
2219
+ // 不写的话会**继承模板字段的状态** —— 模板若恰好是只读的,新字段一出生就只读。
2220
+ overrides: {
2221
+ readOnly: false, disableEdit: false, display: true,
2222
+ required: cmdOpts.required === true,
2223
+ ...(cmdOpts.singleRow === true ? { singleRow: true } : {}),
2224
+ },
2225
+ });
2226
+ console.error(` 布局已更新: excel[${placed.row},${placed.col}](增量插入,未重建)`);
2227
+ }
2228
+ catch (e) {
2229
+ console.error(` 布局更新失败: ${e.message}`);
2230
+ console.error(' 字段本身已建立,但没进布局。按上面那条修好重跑本命令即可(重跑不会重复建字段)。');
2231
+ }
2232
+ }
2233
+ else {
2234
+ console.error(' 重新生成布局(整表重建)...');
2235
+ try {
2236
+ const preLayout = await client.post(`${baseUrl}/api/ebuilder/form/builder/getFormLayout`, {
2237
+ formId, module: 'ebuilderform', customParam: {}, formTableIdList: [],
2238
+ designerParam: { onlyLayout: false, convert: false }, form: { id: formId },
2239
+ }, { ebbusinessid: formId });
2240
+ const layoutId = preLayout?.data?.formLayout?.id || '';
2241
+ const layoutMultiId = preLayout?.data?.formLayout?.multipleLayoutId || '';
2242
+ const layoutName = preLayout?.data?.formLayoutMultiple?.name || preLayout?.data?.formLayout?.name || '';
2243
+ const layoutType = preLayout?.data?.formLayoutMultiple?.layoutType || 'EXCEL';
2244
+ const configRes = await client.post(`${baseUrl}/api/ebuilder/form/form/view/relate/getConfig`, {
2245
+ formId, module: 'ebuilderform', apiModule: 'ebuilder/form',
2246
+ layoutId, customParam: { terminal: '', objId: formId },
2247
+ initMode: 5, groupFieldIds: [], designerParam: { sourceModule: 'ebuilderform' },
2248
+ }, { ebbusinessid: formId });
2249
+ const rawConfigs = configRes?.data?.configs || [];
2250
+ // saveFormFieldNew fields may not appear in getConfig — manually append
2251
+ const CK = {
2252
+ Text: 'Text', Textarea: 'TextArea', Number: 'NumberComponent', Money: 'Money',
2253
+ Date: 'DateComponent', DateTime: 'DateComponent', Select: 'Select', CheckBox: 'CheckBox', RadioBox: 'RadioBox',
2254
+ Employee: 'Employee', Department: 'Department', SubCompany: 'Subcompany', EBuilder: 'Ebuilder',
2255
+ Phone: 'Phone', Email: 'Email', Mobile: 'Mobile', Location: 'Location', Switch: 'Switch', IDCard: 'IDCard',
2256
+ };
2257
+ const newFieldConfig = {
2258
+ id: fieldId, name: title || fieldName,
2259
+ componentKey: jsonObj.componentKey || CK[fieldType] || fieldType,
2260
+ display: true, show: true, readOnly: false,
2261
+ required: cmdOpts.required === true, disableEdit: false, singleRow: cmdOpts.singleRow === true,
2262
+ isDelete: false, type: fieldType, dataKey: colName,
2263
+ };
2264
+ const existingIds = new Set(rawConfigs.map((c) => String(c.id)));
2265
+ if (cmdOpts.subFormId) {
2266
+ // 明细字段:加进对应 DetailTable 的 children(明细字段嵌在 DetailTable 里,不是顶层)
2267
+ const dtComp = rawConfigs.find((c) => String(c.id) === String(cmdOpts.subFormId) && c.type === 'DetailTable');
2268
+ if (dtComp) {
2269
+ const childIds = new Set((dtComp.children || []).map((x) => String(x.id)));
2270
+ if (!childIds.has(fieldId)) {
2271
+ dtComp.children = dtComp.children || [];
2272
+ dtComp.children.push({ ...newFieldConfig, showOrder: dtComp.children.length });
2273
+ }
2274
+ }
2275
+ else {
2276
+ console.error(` ⚠ 布局里未找到明细表 subFormId=${cmdOpts.subFormId},字段已加但未进布局`);
2596
2277
  }
2597
2278
  }
2598
- else {
2599
- console.error(` ⚠ 布局里未找到明细表 subFormId=${cmdOpts.subFormId},字段已加但未进布局`);
2279
+ else if (!existingIds.has(fieldId)) {
2280
+ rawConfigs.push({ ...newFieldConfig, showOrder: rawConfigs.length });
2600
2281
  }
2282
+ const configs = rawConfigs.map((c) => ({
2283
+ ...c,
2284
+ display: c.system ? c.display : true,
2285
+ show: c.system ? c.show : true,
2286
+ // 新加字段 saveFormFieldNew 后 getConfig 返回 readOnly:true(未铺列默认),
2287
+ // existingIds 命中时 newFieldConfig(readOnly:false) 会被丢弃,这里强制覆盖回 false,
2288
+ // 否则任何 add-field 新字段(Text/Employee/EBuilder 都中招)默认不可编辑。
2289
+ ...(String(c.id) === String(fieldId)
2290
+ ? { readOnly: false, required: cmdOpts.required === true }
2291
+ : {}),
2292
+ }));
2293
+ // DetailTable 置末(对齐 form create / form layout init):getConfig 可能把明细表夹中间,
2294
+ // 重排避免明细跑到普通字段上面。
2295
+ const orderedConfigs = [
2296
+ ...configs.filter((c) => c.type !== 'DetailTable' && c.componentKey !== 'DataTable'),
2297
+ ...configs.filter((c) => c.type === 'DetailTable' || c.componentKey === 'DataTable'),
2298
+ ];
2299
+ await client.post(`${baseUrl}/api/ebuilder/form/form/view/relate/initLayout?apid=${appId}`, {
2300
+ formId, module: 'ebuilderform', apiModule: 'ebuilder/form',
2301
+ layoutId, generateCollapse: true,
2302
+ styleTemplateId: layoutType === 'FLOW' ? '820000000000000001' : '700000000000000008', styleTemplateType: 1,
2303
+ configs: orderedConfigs, addLayout: false, ecodeType: ['js', 'css'],
2304
+ initMode: '1', customParam: { terminal: '', objId: formId },
2305
+ formLayoutMultiple: { isSys: true, id: layoutMultiId, name: layoutName, terminalType: 'PC', layoutType },
2306
+ designerParam: { onlyLayout: false },
2307
+ }, { ebbusinessid: formId });
2308
+ console.error(' 布局已更新');
2309
+ }
2310
+ catch (e) {
2311
+ console.error(` 布局更新失败: ${e.message}`);
2601
2312
  }
2602
- else if (!existingIds.has(fieldId)) {
2603
- rawConfigs.push({ ...newFieldConfig, showOrder: rawConfigs.length });
2604
- }
2605
- const configs = rawConfigs.map((c) => ({
2606
- ...c,
2607
- display: c.system ? c.display : true,
2608
- show: c.system ? c.show : true,
2609
- // 新加字段 saveFormFieldNew 后 getConfig 返回 readOnly:true(未铺列默认),
2610
- // existingIds 命中时 newFieldConfig(readOnly:false) 会被丢弃,这里强制覆盖回 false,
2611
- // 否则任何 add-field 新字段(Text/Employee/EBuilder 都中招)默认不可编辑。
2612
- ...(String(c.id) === String(fieldId) ? { readOnly: false } : {}),
2613
- }));
2614
- // DetailTable 置末(对齐 form create / form layout init):getConfig 可能把明细表夹中间,
2615
- // 重排避免明细跑到普通字段上面。
2616
- const orderedConfigs = [
2617
- ...configs.filter((c) => c.type !== 'DetailTable' && c.componentKey !== 'DataTable'),
2618
- ...configs.filter((c) => c.type === 'DetailTable' || c.componentKey === 'DataTable'),
2619
- ];
2620
- await client.post(`${baseUrl}/api/ebuilder/form/form/view/relate/initLayout?apid=${appId}`, {
2621
- formId, module: 'ebuilderform', apiModule: 'ebuilder/form',
2622
- layoutId, generateCollapse: true,
2623
- styleTemplateId: layoutType === 'FLOW' ? '820000000000000001' : '700000000000000008', styleTemplateType: 1,
2624
- configs: orderedConfigs, addLayout: false, ecodeType: ['js', 'css'],
2625
- initMode: '1', customParam: { terminal: '', objId: formId },
2626
- formLayoutMultiple: { isSys: true, id: layoutMultiId, name: layoutName, terminalType: 'PC', layoutType },
2627
- designerParam: { onlyLayout: false },
2628
- }, { ebbusinessid: formId });
2629
- console.error(' 布局已更新');
2630
- }
2631
- catch (e) {
2632
- console.error(` 布局更新失败: ${e.message}`);
2633
2313
  }
2634
2314
  }
2635
2315
  else {
@@ -2658,7 +2338,9 @@ export function registerFormCommands(program) {
2658
2338
  .option('--max-len <n>', '最大长度(正整数)。日期/时间/省市区/身份证/电子发票/下拉多选这几类没有该属性')
2659
2339
  .option('--default <value>', '默认值(Text / TextArea / Number / Money)')
2660
2340
  .option('--clear-default', '清空默认值')
2661
- .option('--data-key <key>', '数据库字段名(同时改 dataKey 与 columnName;字段已有数据时平台会拒绝)')
2341
+ .option('--data-key <key>', '字段编码 dataKey(字母数字下划线,不以数字开头;字段已有数据时平台会拒绝)')
2342
+ .option('--column-name <name>', '数据库列名 columnName。**与 dataKey 是两个东西,可分别指定**;'
2343
+ + '不给时跟随 --data-key。字段已有数据时平台会拒绝改列名')
2662
2344
  .option('--desc <text>', '字段描述(设计器字段属性里的「描述」)')
2663
2345
  .action(async (objId, appId, fieldId, editOpts) => {
2664
2346
  const opts = program.opts();
@@ -2842,7 +2524,8 @@ export function registerFormCommands(program) {
2842
2524
  }
2843
2525
  }
2844
2526
  }
2845
- // Apply required
2527
+ // `required` 不是字段记录的键(整张表的字段记录里都没有),写到 jsonObj 只是**保持与
2528
+ // 既有代码一致的形式**,真正生效的是保存成功之后那段布局 comp 补丁 —— 见下面 `patchFieldLayout`。
2846
2529
  if (editOpts.required) {
2847
2530
  jsonObj.required = true;
2848
2531
  }
@@ -2990,6 +2673,7 @@ export function registerFormCommands(program) {
2990
2673
  defaultValue: editOpts.default,
2991
2674
  clearDefault: editOpts.clearDefault,
2992
2675
  dataKey: editOpts.dataKey,
2676
+ columnName: editOpts.columnName,
2993
2677
  desc: editOpts.desc,
2994
2678
  });
2995
2679
  if (patch.errors.length > 0) {
@@ -3015,10 +2699,10 @@ export function registerFormCommands(program) {
3015
2699
  if (editOpts.type) {
3016
2700
  changes.push(typeChangeApplied ? `type=${editOpts.type}` : `type=${editOpts.type}(未变化)`);
3017
2701
  }
3018
- if (editOpts.required)
3019
- changes.push('required');
3020
- if (editOpts.noRequired)
3021
- changes.push('!required');
2702
+ // commander 把 `--no-required` 映射成 `required: false`(不是 `noRequired`),
2703
+ // 所以只判 noRequired 的话反向操作会漏报成「no changes」—— 改了却说没改。
2704
+ if (editOpts.required !== undefined)
2705
+ changes.push(editOpts.required ? 'required' : '!required');
3022
2706
  if (editOpts.sformId)
3023
2707
  changes.push(`EBuilder→${editOpts.sformId}`);
3024
2708
  if (editOpts.dataTitle !== undefined)
@@ -3028,6 +2712,19 @@ export function registerFormCommands(program) {
3028
2712
  if (editOpts.multiple !== undefined)
3029
2713
  changes.push(editOpts.multiple ? 'multiple' : '!multiple');
3030
2714
  changes.push(...patch.applied);
2715
+ // 必填**只存在于布局 comp 里**:字段记录没有 `required` 这个键(整张表的字段记录里都没有),
2716
+ // 所以上面写进 jsonObj 的那一下落不了库。只写字段记录 = 接口回 200、命令报成功、表单上行为
2717
+ // 一点没变。真正生效的是布局 comp 的 `config.required`,这里补上。
2718
+ if (editOpts.required !== undefined) {
2719
+ try {
2720
+ const { updated } = await patchFieldLayout(client, baseUrl, formId, fieldId, { required: editOpts.required === true });
2721
+ if (!updated)
2722
+ console.error(' 警告: 布局里没找到该字段的组件,必填未生效');
2723
+ }
2724
+ catch (e) {
2725
+ console.error(` 警告: 布局里的必填未更新(字段本身已改好): ${e.message}`);
2726
+ }
2727
+ }
3031
2728
  // EBuilder 字段的浏览显示列:add-field 建出来的会自动配,edit-field 改出来的此前漏了这一步
3032
2729
  // —— 字段能建出来,但浏览弹窗回落平台默认列,会把目标表的系统字段也列出来。
3033
2730
  // ensureDisplayColumns 只在「没配过」或「旧列对不上当前目标表」时才写,不会冲掉手工配好的列。