weaver-e10-builder 1.0.3-beta → 1.0.7-beta

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 (82) hide show
  1. package/dist/commands/form-add-detail-table.d.ts +3 -0
  2. package/dist/commands/form-add-detail-table.d.ts.map +1 -0
  3. package/dist/commands/form-add-detail-table.js +153 -0
  4. package/dist/commands/form-add-detail-table.js.map +1 -0
  5. package/dist/commands/form-association.d.ts +3 -0
  6. package/dist/commands/form-association.d.ts.map +1 -0
  7. package/dist/commands/form-association.js +299 -0
  8. package/dist/commands/form-association.js.map +1 -0
  9. package/dist/commands/form-list-filter.d.ts.map +1 -1
  10. package/dist/commands/form-list-filter.js +4 -2
  11. package/dist/commands/form-list-filter.js.map +1 -1
  12. package/dist/commands/form-list-search-group.d.ts.map +1 -1
  13. package/dist/commands/form-list-search-group.js +10 -3
  14. package/dist/commands/form-list-search-group.js.map +1 -1
  15. package/dist/commands/form-list-stats.d.ts.map +1 -1
  16. package/dist/commands/form-list-stats.js +3 -1
  17. package/dist/commands/form-list-stats.js.map +1 -1
  18. package/dist/commands/form-list-utils.d.ts +13 -0
  19. package/dist/commands/form-list-utils.d.ts.map +1 -1
  20. package/dist/commands/form-list-utils.js +87 -17
  21. package/dist/commands/form-list-utils.js.map +1 -1
  22. package/dist/commands/form-list.d.ts.map +1 -1
  23. package/dist/commands/form-list.js +51 -0
  24. package/dist/commands/form-list.js.map +1 -1
  25. package/dist/commands/form-native.d.ts +17 -0
  26. package/dist/commands/form-native.d.ts.map +1 -0
  27. package/dist/commands/form-native.js +136 -0
  28. package/dist/commands/form-native.js.map +1 -0
  29. package/dist/commands/form-permission.d.ts.map +1 -1
  30. package/dist/commands/form-permission.js +8 -1
  31. package/dist/commands/form-permission.js.map +1 -1
  32. package/dist/commands/form.d.ts.map +1 -1
  33. package/dist/commands/form.js +6 -2
  34. package/dist/commands/form.js.map +1 -1
  35. package/dist/commands/matrix.d.ts.map +1 -1
  36. package/dist/commands/matrix.js +8 -6
  37. package/dist/commands/matrix.js.map +1 -1
  38. package/dist/commands/wf-form.d.ts +3 -0
  39. package/dist/commands/wf-form.d.ts.map +1 -0
  40. package/dist/commands/wf-form.js +1002 -0
  41. package/dist/commands/wf-form.js.map +1 -0
  42. package/dist/commands/workflow-condition.d.ts.map +1 -1
  43. package/dist/commands/workflow-condition.js +193 -60
  44. package/dist/commands/workflow-condition.js.map +1 -1
  45. package/dist/commands/workflow-create-workflow.d.ts +3 -0
  46. package/dist/commands/workflow-create-workflow.d.ts.map +1 -0
  47. package/dist/commands/workflow-create-workflow.js +214 -0
  48. package/dist/commands/workflow-create-workflow.js.map +1 -0
  49. package/dist/commands/workflow-node-robot.d.ts +3 -0
  50. package/dist/commands/workflow-node-robot.d.ts.map +1 -0
  51. package/dist/commands/workflow-node-robot.js +180 -0
  52. package/dist/commands/workflow-node-robot.js.map +1 -0
  53. package/dist/commands/workflow-node-validate.js +1 -1
  54. package/dist/commands/workflow-node-validate.js.map +1 -1
  55. package/dist/commands/workflow-node.d.ts.map +1 -1
  56. package/dist/commands/workflow-node.js +171 -9
  57. package/dist/commands/workflow-node.js.map +1 -1
  58. package/dist/commands/workflow-segment.d.ts +3 -0
  59. package/dist/commands/workflow-segment.d.ts.map +1 -0
  60. package/dist/commands/workflow-segment.js +333 -0
  61. package/dist/commands/workflow-segment.js.map +1 -0
  62. package/dist/commands/workflow-subprocess.d.ts +3 -0
  63. package/dist/commands/workflow-subprocess.d.ts.map +1 -0
  64. package/dist/commands/workflow-subprocess.js +496 -0
  65. package/dist/commands/workflow-subprocess.js.map +1 -0
  66. package/dist/commands/workflow.d.ts.map +1 -1
  67. package/dist/commands/workflow.js +134 -56
  68. package/dist/commands/workflow.js.map +1 -1
  69. package/dist/core/lzw.d.ts +16 -0
  70. package/dist/core/lzw.d.ts.map +1 -0
  71. package/dist/core/lzw.js +71 -0
  72. package/dist/core/lzw.js.map +1 -0
  73. package/dist/index.js +16 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/services/condition-rules.d.ts.map +1 -1
  76. package/dist/services/condition-rules.js +83 -30
  77. package/dist/services/condition-rules.js.map +1 -1
  78. package/dist/services/workflow-layout.d.ts +13 -2
  79. package/dist/services/workflow-layout.d.ts.map +1 -1
  80. package/dist/services/workflow-layout.js +73 -27
  81. package/dist/services/workflow-layout.js.map +1 -1
  82. package/package.json +1 -1
@@ -0,0 +1,1002 @@
1
+ import { AuthSession } from '../core/auth.js';
2
+ import { ApiClient } from '../core/client.js';
3
+ import { toSubTableName, toColumnName } from '../utils/column-name.js';
4
+ import { normalizeDetailTableChildren, placeholderFromFormat } from '../services/form-create.js';
5
+ import { randomUUID } from 'node:crypto';
6
+ /** 流程表单选项配色(对齐浏览器抓包 styleConfig.OptionBackground,循环取用)。 */
7
+ const OPTION_COLORS = ['#FFBD4F', '#FF716E', '#3BD8C3', '#5B8FF9', '#5AD8A6', '#F6BD16', '#E86452', '#6DC8EC', '#945FB9', '#FF99C3'];
8
+ /**
9
+ * wf-form — 流程表单(工作流程表单)表单管理。与 eb 表 `form` 命令对齐,但走流程表单引擎:
10
+ *
11
+ * 路由前缀: /api/workflow/core/form/... (eb 表为 /api/ebuilder/form/...)
12
+ * body: { module:'workflow', customParam:{} } (eb 表 module:'ebuilderform', customParam:{appId,objId})
13
+ * 无 apid 查询参 / 无 ebbusinessid header(eb 表带);generatorIds 无需 apid。
14
+ * 默认布局: FLOW(eb 表为 EXCEL)→ initLayout 模板按 layoutType 选。
15
+ *
16
+ * 探针实测(mulinquan formId=1307197331401359360,2026-08-24):
17
+ * - getFormLayout /api/workflow/core/form/builder/getFormLayout ✅(module=workflow)
18
+ * - getConfig /api/workflow/core/form/view/relate/getConfig ✅(返回 configs 含系统字段 REQ_xxx / REQUEST_xxx)
19
+ * - saveFormFieldNew /api/workflow/core/form/field/manage/saveFormFieldNew ✅ 落库(formTableId 省略)
20
+ * - saveSubForm /api/workflow/core/form/field/manage/saveSubForm ✅(首字段 omit formTableId,同 eb 机制)
21
+ * - initLayout /api/workflow/core/form/view/relate/initLayout ✅
22
+ * 回读用 getConfig(getFormFieldPage 返回结构不同,count 不可靠)。
23
+ *
24
+ * 不改 form.ts/form-layout-init.ts/form-add-detail-table.ts;老 eb 命令保持原样。
25
+ */
26
+ const ROUTE = '/api/workflow/core/form';
27
+ const MODULE = 'workflow';
28
+ /** 类型别名归一(小写/口语变体 → 平台 componentKey),内联小表。 */
29
+ function canon(ck) {
30
+ const m = {
31
+ textarea: 'TextArea', text: 'Text', number: 'Number', money: 'Money',
32
+ date: 'Date', datetime: 'DateTime', select: 'Select', selectmultiple: 'SelectMultiple',
33
+ employee: 'Employee', department: 'Department', browser: 'Browser',
34
+ };
35
+ return m[ck.toLowerCase()] || ck;
36
+ }
37
+ async function getLayoutMeta(client, baseUrl, formId) {
38
+ const res = await client.post(`${baseUrl}${ROUTE}/builder/getFormLayout`, {
39
+ formId, module: MODULE, customParam: {}, formTableIdList: [],
40
+ designerParam: { onlyLayout: false, convert: false }, form: { id: formId },
41
+ }, { ebbusinessid: formId });
42
+ if (res?.code !== 200 || !res?.status)
43
+ throw new Error(`getFormLayout 失败: ${res?.msg}`);
44
+ const fl = res.data?.formLayout || {}, flm = res.data?.formLayoutMultiple || {};
45
+ return { layoutId: fl.id || '', layoutMultiId: flm.id || '', layoutName: flm.name || '', layoutType: flm.layoutType || 'FLOW' };
46
+ }
47
+ async function readConfigs(client, baseUrl, formId) {
48
+ const meta = await getLayoutMeta(client, baseUrl, formId);
49
+ const cfg = await client.post(`${baseUrl}${ROUTE}/view/relate/getConfig`, {
50
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
51
+ customParam: { terminal: '', objId: formId }, initMode: 5, groupFieldIds: [],
52
+ designerParam: { sourceModule: MODULE },
53
+ }, { ebbusinessid: formId });
54
+ if (cfg?.code !== 200 || !cfg?.status)
55
+ throw new Error(`getConfig 失败: ${cfg?.msg}`);
56
+ return { ...meta, configs: normalizeDetailTableChildren(cfg?.data?.configs || []) };
57
+ }
58
+ async function genIds(client, baseUrl, n) {
59
+ const r = await client.post(`${baseUrl}/api/form/local/generatorIds?size=${n}`, {});
60
+ return (r?.data?.generatorIds || []).map(String);
61
+ }
62
+ export function registerWorkflowFormCommands(program) {
63
+ const wfForm = program.command('wf-form').description('流程表单(工作流程表单)管理:read / add-field / add-detail-table / layout init');
64
+ // ---------- wf-form read ----------
65
+ wfForm.command('read')
66
+ .description('读取流程表单字段(getConfig)')
67
+ .argument('<formId>', '流程表单 formId(workflow create-workflow 回吐)')
68
+ .action(async (formId) => {
69
+ try {
70
+ const opts = program.opts();
71
+ const auth = await AuthSession.fromOptions(opts);
72
+ if (!auth)
73
+ throw new Error('无法加载认证');
74
+ const client = new ApiClient(auth);
75
+ const baseUrl = auth.getBaseUrl();
76
+ const { layoutType, configs } = await readConfigs(client, baseUrl, formId);
77
+ const rows = configs.map((c) => ({
78
+ id: c.id, name: c.name, componentKey: c.componentKey || c.type,
79
+ system: c.system ? '系统' : '业务',
80
+ children: c.children?.length || undefined,
81
+ }));
82
+ if (opts.json) {
83
+ console.log(JSON.stringify({ formId, layoutType, count: rows.length, fields: rows }, null, 2));
84
+ }
85
+ else {
86
+ console.error(`流程表单 ${formId} 布局=${layoutType} 字段=${rows.length}`);
87
+ for (const r of rows)
88
+ console.log(` ${r.system} ${String(r.id).padEnd(20)} ${String(r.componentKey).padEnd(14)} ${r.name}${r.children ? ` (明细×${r.children})` : ''}`);
89
+ }
90
+ }
91
+ catch (e) {
92
+ console.error(`读取失败: ${e.message}`);
93
+ process.exitCode = 1;
94
+ }
95
+ });
96
+ // ---------- wf-form add-field ----------
97
+ wfForm.command('add-field')
98
+ .description('给流程表单加字段(Text/TextArea/Number/Money/Date/DateTime/Select/Employee/Department/Browser)')
99
+ .argument('<formId>', '流程表单 formId')
100
+ .argument('<fieldName>', '字段名')
101
+ .argument('<fieldType>', '字段类型')
102
+ .argument('[title]', '字段显示名(不传同 fieldName)')
103
+ .option('--sub-form-id <id>', '在指定明细子表里加字段(明细表 subFormId)')
104
+ .option('--multiple', '多选(Employee/Department/Browser/下拉Select→SelectMultiple)')
105
+ .option('--precision <n>', '小数位(Number 缺省 0 / Money 缺省 2 / 百分比缺省 1)')
106
+ .option('--percent', 'Number 百分比显示(numberType=percentage)')
107
+ .option('--default <value>', 'Number/Money 默认值')
108
+ .option('--format <fmt>', '日期精度(Date 默认 yyyy-MM-dd / DateTime 默认 yyyy-MM-dd HH:mm)')
109
+ .action(async (formId, fieldName, fieldType, title, o) => {
110
+ try {
111
+ const opts = program.opts();
112
+ const auth = await AuthSession.fromOptions(opts);
113
+ if (!auth)
114
+ throw new Error('无法加载认证');
115
+ const client = new ApiClient(auth);
116
+ const baseUrl = auth.getBaseUrl();
117
+ const ebb = { ebbusinessid: formId };
118
+ const ck = canon(fieldType);
119
+ const colName = toColumnName(fieldName);
120
+ const [fieldId] = await genIds(client, baseUrl, 1);
121
+ if (!fieldId)
122
+ throw new Error('生成 fieldId 失败');
123
+ const jsonObj = {
124
+ type: ck, title: title || fieldName, fieldId, groupId: '-1', formId, showOrder: '0.00',
125
+ dataKey: colName, columnName: colName,
126
+ };
127
+ if (o.subFormId)
128
+ jsonObj.subFormId = o.subFormId; // 明细字段带 subFormId;formTableId 一律省略(流程表单无独立 formTableId)
129
+ if (ck === 'Number' || ck === 'Money') {
130
+ const isPercent = ck === 'Number' && o.percent === true;
131
+ const hasDefault = o.default !== undefined && o.default !== null && o.default !== '';
132
+ Object.assign(jsonObj, {
133
+ pointSize: o.precision !== undefined ? o.precision : (isPercent ? '1' : (ck === 'Money' ? '2' : '0')),
134
+ isDefault: hasDefault, isDelimiter: ck === 'Money', isCapital: false,
135
+ capitalFormat: 'onlyUpperCase', capitalPosition: 'outside', capitalSetting: 'system',
136
+ capitalPrecision: 'yuan', isExchangeRates: false,
137
+ ...(ck === 'Number' ? { numberType: isPercent ? 'percentage' : 'number', content: hasDefault ? String(o.default) : '' } : {}),
138
+ ...(hasDefault ? { stepMode: false, step: 1 } : {}),
139
+ });
140
+ }
141
+ else if (ck === 'TextArea') {
142
+ Object.assign(jsonObj, { content: '', maxLen: 4000 });
143
+ }
144
+ else if (ck === 'Date' || ck === 'DateTime') {
145
+ const fmt = o.format || (ck === 'DateTime' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd');
146
+ Object.assign(jsonObj, { type: 'Date', format: fmt, placeholder: placeholderFromFormat(fmt), timePanelStyle: { type: 'system' }, isDefault: false });
147
+ }
148
+ else if (ck === 'Employee' || ck === 'Department' || ck === 'Browser') {
149
+ jsonObj.isSingle = o.multiple ? false : true;
150
+ }
151
+ else if (ck === 'Select') {
152
+ if (o.multiple) {
153
+ jsonObj.type = 'SelectMultiple';
154
+ jsonObj.componentKey = 'SelectMultiple';
155
+ }
156
+ // 选项需后续单独配(流程表单暂无 field-options 命令)
157
+ }
158
+ if (!jsonObj.componentKey)
159
+ jsonObj.componentKey = jsonObj.type;
160
+ const r = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(jsonObj) }, ebb);
161
+ if (r?.code !== 200 || !r?.status)
162
+ throw new Error(`加字段失败: ${r?.msg || '未知错误'}`);
163
+ // 回读 getConfig 确认落库(主表字段查顶层 configs;明细字段查子表 children)
164
+ const { configs } = await readConfigs(client, baseUrl, formId);
165
+ const found = o.subFormId
166
+ ? (configs.find((c) => String(c.id) === String(o.subFormId))?.children || []).find((ch) => String(ch.id) === String(fieldId))
167
+ : configs.find((c) => String(c.id) === String(fieldId));
168
+ if (!found) {
169
+ console.error(` ⚠ 接口返回成功但 getConfig 未回读到(可能未落库)`);
170
+ process.exitCode = 1;
171
+ return;
172
+ }
173
+ console.log(opts.json
174
+ ? JSON.stringify({ success: true, formId, fieldId, fieldName, componentKey: jsonObj.componentKey, subFormId: o.subFormId || undefined }, null, 2)
175
+ : `字段已加: ${fieldName} (${jsonObj.componentKey}) fieldId=${fieldId}${o.subFormId ? ` (明细 ${o.subFormId})` : ''}${ck === 'Select' ? ' 注:选项需单独配' : ''}`);
176
+ }
177
+ catch (e) {
178
+ console.error(`加字段失败: ${e.message}`);
179
+ process.exitCode = 1;
180
+ }
181
+ });
182
+ // ---------- wf-form permission ----------
183
+ // 表单级字段权限:getFieldPermission 读可靠;saveFieldPermission 写在流程表单上平台多返 500(不可靠,
184
+ // 因流程表单字段权限属流程域——见 scaffold form-field-permission.md 第 139 行:工作流节点字段权限应用对应工作流接口)。
185
+ // 故本命令只提供 get(读);写字段权限用 `workflow node field-permission`(节点级,已有)。
186
+ const permissionCmd = wfForm.command('permission').description('表单级字段权限查询(display/readOnly/required/disableEdit)。写权限用 workflow node field-permission(节点级)');
187
+ /** 递归找字段(configs 树,含明细 children)。 */
188
+ function findInTree(configs, ref) {
189
+ for (const c of configs) {
190
+ if (String(c.id) === ref || c.name === ref || c.dataKey === ref)
191
+ return c;
192
+ if (c.children?.length) {
193
+ const f = findInTree(c.children, ref);
194
+ if (f)
195
+ return f;
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ permissionCmd.command('get')
201
+ .description('查看流程表单字段权限(display/readOnly/required/disableEdit)')
202
+ .argument('<formId>', '流程表单 formId')
203
+ .action(async (formId) => {
204
+ try {
205
+ const opts = program.opts();
206
+ const auth = await AuthSession.fromOptions(opts);
207
+ if (!auth)
208
+ throw new Error('无法加载认证');
209
+ const client = new ApiClient(auth);
210
+ const baseUrl = auth.getBaseUrl();
211
+ const meta = await getLayoutMeta(client, baseUrl, formId);
212
+ const r = await client.post(`${baseUrl}${ROUTE}/view/relate/getFieldPermission`, { module: MODULE, formId, permissionId: formId, showDelete: 0, customParam: {}, layoutId: meta.layoutId }, { ebbusinessid: formId });
213
+ if (r?.code !== 200 || !r?.status)
214
+ throw new Error(`getFieldPermission 失败: ${r?.msg}`);
215
+ const rows = [];
216
+ const walk = (nodes, depth = 0) => { for (const c of nodes) {
217
+ rows.push({ ...c, _d: depth });
218
+ if (c.children?.length)
219
+ walk(c.children, depth + 1);
220
+ } };
221
+ walk(r?.data?.configs || []);
222
+ if (opts.json) {
223
+ console.log(JSON.stringify(rows, null, 2));
224
+ return;
225
+ }
226
+ for (const row of rows) {
227
+ const ind = ' '.repeat(row._d);
228
+ const flags = [row.display ? '' : '隐藏', row.readOnly ? '只读' : '', row.required ? '必填' : '', row.disableEdit ? '禁编辑' : ''].filter(Boolean).join('/') || '默认';
229
+ console.log(`${ind}${String(row.name).padEnd(10)} ${flags}`);
230
+ }
231
+ }
232
+ catch (e) {
233
+ console.error(`查询字段权限失败: ${e.message}`);
234
+ process.exitCode = 1;
235
+ }
236
+ });
237
+ // ---------- wf-form relate ----------
238
+ wfForm.command('relate')
239
+ .description('值带出联动:换表内 Employee/Department 字段自动填目标字段(如 换申请人→填邮箱/工号;换所属部门→填部门编号/上级部门)')
240
+ .argument('<formId>', '流程表单 formId')
241
+ .argument('<targetField>', '目标字段(被填充),用名称或 fieldId')
242
+ .requiredOption('--from <field>', '源字段(触发,Employee 或 Department 类型),用名称或 fieldId')
243
+ .option('--key <key>', '带出属性 key(Employee 源如 email/department/superior;Department 源如 code/name/parent)。默认 Employee=department、Department=name')
244
+ .option('--title <title>', '属性显示名(默认取属性清单中文名)')
245
+ .action(async (formId, targetField, cmdOpts) => {
246
+ try {
247
+ const opts = program.opts();
248
+ const auth = await AuthSession.fromOptions(opts);
249
+ if (!auth)
250
+ throw new Error('无法加载认证');
251
+ const client = new ApiClient(auth);
252
+ const baseUrl = auth.getBaseUrl();
253
+ const ebb = { ebbusinessid: formId };
254
+ // getConfig 解析字段(主表 + 明细 children)
255
+ const { configs } = await readConfigs(client, baseUrl, formId);
256
+ const allFields = [];
257
+ for (const c of configs) {
258
+ if (c.system)
259
+ continue;
260
+ if (c.componentKey === 'DataTable' || c.type === 'DetailTable') {
261
+ for (const ch of (c.children || []))
262
+ allFields.push({ ...ch, isDetail: true });
263
+ }
264
+ else
265
+ allFields.push({ ...c, isDetail: false });
266
+ }
267
+ const findField = (ref) => allFields.find((f) => f.name === ref || String(f.id) === ref || f.dataKey === ref);
268
+ const target = findField(targetField);
269
+ const source = findField(cmdOpts.from);
270
+ if (!target || !source)
271
+ throw new Error(`找不到字段: target="${targetField}" from="${cmdOpts.from}"。可用: ${allFields.map((f) => f.name).join(', ')}`);
272
+ const sourceKey = String(source.componentKey || source.type || '');
273
+ let sourceType;
274
+ if (/employee|user/i.test(sourceKey))
275
+ sourceType = 'Employee';
276
+ else if (/department|dept/i.test(sourceKey))
277
+ sourceType = 'Department';
278
+ else
279
+ throw new Error(`源字段「${source.name}」类型 ${sourceKey} 不支持值带出(仅 Employee/Department)`);
280
+ // 拉属性清单 → 校验 key/默认 key/title
281
+ const attrsRes = await client.post(`${baseUrl}${ROUTE}/business/listBusinessField`, { module: 'hrm', scenario: 'BROWSER_DATA_RELATE', browserParam: { module: 'hrm', type: sourceType === 'Employee' ? 'resource' : 'department' } }, ebb);
282
+ const attrs = Array.isArray(attrsRes?.data) ? attrsRes.data : [];
283
+ const defaultKey = sourceType === 'Employee' ? 'department' : 'name';
284
+ const key = cmdOpts.key || defaultKey;
285
+ let title = cmdOpts.title || '';
286
+ if (attrs.length) {
287
+ const attr = attrs.find((a) => a.valueKey === key);
288
+ if (!attr)
289
+ throw new Error(`--key "${key}" 不是 ${sourceType} 可带出属性。可用: ${attrs.map((a) => a.valueKey).join(', ')}`);
290
+ title = title || attr.name;
291
+ }
292
+ else {
293
+ console.error(' 警告: 拉取属性清单失败,key/title 未校验');
294
+ }
295
+ // 读目标字段全对象 → 注入 ffRelateField(Employee 源带 module:'hrm',Department 不带)
296
+ const fData = await client.post(`${baseUrl}${ROUTE}/field/manage/getFormField`, { module: MODULE, formTableField: { formField: { id: String(target.id), form: { id: formId } } } }, ebb);
297
+ const jsonObj = { ...(fData?.data || {}) };
298
+ const relate = {
299
+ component: { key: jsonObj.type || 'Text' }, title, key, isMultiple: false, fieldId: String(source.id),
300
+ };
301
+ if (sourceType === 'Employee')
302
+ relate.module = 'hrm';
303
+ jsonObj.ffRelateField = relate;
304
+ const r = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(jsonObj) }, ebb);
305
+ if (r?.code !== 200 || !r?.status)
306
+ throw new Error(`保存失败: ${r?.msg || '未知错误'}`);
307
+ console.log(opts.json
308
+ ? JSON.stringify({ success: true, formId, source: source.name, sourceType, target: target.name, key, title }, null, 2)
309
+ : `值带出已设置: 换「${source.name}」(${sourceType}) → 自动填「${target.name}」(属性 ${key}/${title})`);
310
+ }
311
+ catch (e) {
312
+ console.error(`设置值带出失败: ${e.message}`);
313
+ process.exitCode = 1;
314
+ }
315
+ });
316
+ // ---------- wf-form validate ----------
317
+ const validateCmd = wfForm.command('validate').description('字段查重校验规则(多字段 AND,重复禁止提交/提醒)');
318
+ validateCmd.command('list')
319
+ .description('查询流程表单的查重规则')
320
+ .argument('<formId>', '流程表单 formId')
321
+ .action(async (formId) => {
322
+ try {
323
+ const opts = program.opts();
324
+ const auth = await AuthSession.fromOptions(opts);
325
+ if (!auth)
326
+ throw new Error('无法加载认证');
327
+ const client = new ApiClient(auth);
328
+ const baseUrl = auth.getBaseUrl();
329
+ const res = await client.post(`${baseUrl}${ROUTE}/validate/pageFormDataValidate`, { module: MODULE, formId, pageNo: 1, pageSize: 100 }, { ebbusinessid: formId });
330
+ const rules = res?.data?.records || [];
331
+ if (opts.json) {
332
+ console.log(JSON.stringify(rules, null, 2));
333
+ return;
334
+ }
335
+ if (!rules.length) {
336
+ console.log('当前流程表单无查重规则');
337
+ return;
338
+ }
339
+ for (const r of rules) {
340
+ const method = r.treatmentMethod === 'REMINDER' ? '提醒' : '禁止提交';
341
+ const fields = (r.checkFields || []).map((c) => c.fieldName).join(' + ');
342
+ console.log(`[${r.id}] ${method} | ${fields} | ${r.helpContent || ''}`);
343
+ }
344
+ }
345
+ catch (e) {
346
+ console.error(`查询查重规则失败: ${e.message}`);
347
+ process.exitCode = 1;
348
+ }
349
+ });
350
+ validateCmd.command('add')
351
+ .description('新增查重规则(多字段 AND:所有字段值都重复才命中)')
352
+ .argument('<formId>', '流程表单 formId')
353
+ .argument('<fields...>', '查重字段名(可多个,如 "请假人" "开始时间")')
354
+ .option('--help <text>', '重复时的提示内容', '字段值不允许重复')
355
+ .option('--remind', '重复时仅提醒允许提交(缺省禁止提交 NOSUBMISSION)')
356
+ .action(async (formId, fieldNames, cmdOpts) => {
357
+ try {
358
+ const opts = program.opts();
359
+ const auth = await AuthSession.fromOptions(opts);
360
+ if (!auth)
361
+ throw new Error('无法加载认证');
362
+ const client = new ApiClient(auth);
363
+ const baseUrl = auth.getBaseUrl();
364
+ const ebb = { ebbusinessid: formId };
365
+ // getConfig 解析字段(主表 subFormId=formId;明细 subFormId=子表 id)
366
+ const { configs } = await readConfigs(client, baseUrl, formId);
367
+ const candidates = [];
368
+ for (const c of configs) {
369
+ if (c.system)
370
+ continue;
371
+ if (c.componentKey === 'DataTable' || c.type === 'DetailTable') {
372
+ for (const ch of (c.children || []))
373
+ candidates.push({ fieldName: ch.name, fieldId: String(ch.id), subFormId: String(c.id) });
374
+ }
375
+ else
376
+ candidates.push({ fieldName: c.name, fieldId: String(c.id), subFormId: formId });
377
+ }
378
+ const checkFields = fieldNames.map((n) => {
379
+ const c = candidates.find((x) => x.fieldName === n || String(x.fieldId) === n);
380
+ if (!c)
381
+ throw new Error(`未找到字段: ${n},可用: ${candidates.map((x) => x.fieldName).join(', ')}`);
382
+ return c;
383
+ });
384
+ const payload = {
385
+ module: MODULE, formId,
386
+ treatmentMethod: cmdOpts.remind ? 'REMINDER' : 'NOSUBMISSION',
387
+ remindTime: 'SUB_REMIND', taggingColour: '#000000', subFieldCheckType: 'ALL_DATA',
388
+ helpContent: cmdOpts.help, checkFields,
389
+ };
390
+ const res = await client.post(`${baseUrl}${ROUTE}/validate/save`, payload, ebb);
391
+ if (res?.code !== 200 || !res?.status)
392
+ throw new Error(`保存失败: ${res?.msg || '未知错误'}`);
393
+ console.log(opts.json
394
+ ? JSON.stringify({ success: true, ruleId: res?.data?.id || '', fields: fieldNames, method: cmdOpts.remind ? 'REMINDER' : 'NOSUBMISSION' }, null, 2)
395
+ : `已保存查重规则: ${fieldNames.join(' + ')}(${cmdOpts.remind ? '提醒' : '禁止提交'})ruleId=${res?.data?.id || ''}`);
396
+ }
397
+ catch (e) {
398
+ console.error(`新增查重规则失败: ${e.message}`);
399
+ process.exitCode = 1;
400
+ }
401
+ });
402
+ validateCmd.command('delete')
403
+ .description('删除查重规则')
404
+ .argument('<formId>', '流程表单 formId')
405
+ .argument('<ruleId>', '规则 ID(wf-form validate list 查看)')
406
+ .action(async (formId, ruleId) => {
407
+ try {
408
+ const opts = program.opts();
409
+ const auth = await AuthSession.fromOptions(opts);
410
+ if (!auth)
411
+ throw new Error('无法加载认证');
412
+ const client = new ApiClient(auth);
413
+ const baseUrl = auth.getBaseUrl();
414
+ const res = await client.post(`${baseUrl}${ROUTE}/validate/batchDelete`, { module: MODULE, formId, idList: [ruleId] }, { ebbusinessid: formId });
415
+ if (res?.code !== 200 || !res?.status)
416
+ throw new Error(`删除失败: ${res?.msg || '未知错误'}`);
417
+ console.log(`已删除查重规则 ${ruleId}`);
418
+ }
419
+ catch (e) {
420
+ console.error(`删除查重规则失败: ${e.message}`);
421
+ process.exitCode = 1;
422
+ }
423
+ });
424
+ // ---------- wf-form linkage ----------
425
+ wfForm.command('linkage')
426
+ .description('显隐联动管理(/api/workflow/core/form/setting/viewlinkage)')
427
+ .argument('<formId>', '流程表单 formId')
428
+ .argument('<name>', '联动名称')
429
+ .requiredOption('--config <json>', '联动配置 JSON:{"conditions":[{"field":"请假天数","operator":"EQUAL","value":"3"}],"actions":[{"prop":"hide","fields":["请假事由"]}]}。prop: hide/show/edit/readonly/required/hideLine;operator: EQUAL/NOT_EQUAL/GREATER/LESS/GREATER_EQUAL/LESS_EQUAL/EMPTY/NOT_EMPTY(默认 EQUAL)')
430
+ .action(async (formId, name, cmdOpts) => {
431
+ try {
432
+ const opts = program.opts();
433
+ const auth = await AuthSession.fromOptions(opts);
434
+ if (!auth)
435
+ throw new Error('无法加载认证');
436
+ const client = new ApiClient(auth);
437
+ const baseUrl = auth.getBaseUrl();
438
+ const ebb = { ebbusinessid: formId };
439
+ let cfg;
440
+ try {
441
+ cfg = JSON.parse(cmdOpts.config);
442
+ }
443
+ catch {
444
+ throw new Error('config JSON 解析失败');
445
+ }
446
+ if (!Array.isArray(cfg.conditions) || !Array.isArray(cfg.actions) || !cfg.actions.length) {
447
+ throw new Error('config 需含 conditions[] 和非空 actions[]');
448
+ }
449
+ const SUPPORTED = ['hide', 'show', 'edit', 'readonly', 'required', 'hideLine', 'hideRow'];
450
+ const badProp = cfg.actions.find((a) => !a.prop || !SUPPORTED.includes(String(a.prop)));
451
+ if (badProp)
452
+ throw new Error(`action prop 不支持(仅 ${SUPPORTED.join(',')})`);
453
+ // getConfig 解析字段名 → fieldId/dataKey/componentKey(主表 + 明细 children)
454
+ const { configs } = await readConfigs(client, baseUrl, formId);
455
+ const allFields = [];
456
+ for (const c of configs) {
457
+ if (c.system)
458
+ continue;
459
+ if (c.componentKey === 'DataTable' || c.type === 'DetailTable') {
460
+ for (const ch of (c.children || []))
461
+ allFields.push({ ...ch, isDetail: true, detailId: c.id, detailName: c.name });
462
+ }
463
+ else
464
+ allFields.push({ ...c, isDetail: false });
465
+ }
466
+ const findField = (name) => allFields.find((f) => f.name === name || String(f.id) === name || f.dataKey === name);
467
+ const formName = configs.find((c) => !c.system)?.name ? '流程表单' : '流程表单';
468
+ // conditions
469
+ const conditionDetails = cfg.conditions.map((c) => {
470
+ const f = findField(c.field);
471
+ if (!f)
472
+ throw new Error(`条件字段未找到: "${c.field}",可用: ${allFields.map((x) => x.name).join(', ')}`);
473
+ return {
474
+ targetId: String(f.id), detailSymbol: c.operator || 'EQUAL', sourceType: 'CONSTANT',
475
+ sourceContent: JSON.stringify(c.options ? { formField: { id: String(f.id) }, dataOptions: c.options } : { formField: { id: String(f.id) }, content: c.value !== undefined ? String(c.value) : '' }),
476
+ };
477
+ });
478
+ // actions
479
+ const linkageDetails = cfg.actions.map((a) => ({
480
+ formId, id: '0', linkageObj: 'field', linkageProp: a.prop,
481
+ linkageShowValue: a.fields.map((fname) => {
482
+ const f = findField(fname);
483
+ if (!f)
484
+ throw new Error(`联动目标字段未找到: "${fname}",可用: ${allFields.map((x) => x.name).join(', ')}`);
485
+ return { dataKey: f.dataKey || '', groupName: '表单字段', address: f.isDetail ? '明细表' : '主表', addressType: f.isDetail ? 'detail' : 'form', groupId: 'formField', name: f.name, descType: f.componentKey || '', id: String(f.id), content: f.name, desc: '' };
486
+ }),
487
+ detailOptions: [],
488
+ }));
489
+ const payload = {
490
+ fdEffect: { formId, effectModule: MODULE, effectType: 'form', effectName: '当前表单', optionList: [{ effectValue: formId, effectName: '当前表单' }] },
491
+ condition: { targetId: formId, targetName: formName, targetModule: MODULE, conditionType: 'FIELD', conditionDetails, formulaId: '', formulaName: '' },
492
+ linkageDetails,
493
+ module: MODULE, formId, refId: formId, enableState: '1', linkageName: name, customParam: {},
494
+ };
495
+ const r = await client.post(`${baseUrl}${ROUTE}/setting/viewlinkage/add`, payload, ebb);
496
+ if (r?.code !== 200 || !r?.status)
497
+ throw new Error(`联动添加失败: ${r?.msg || '未知错误'}`);
498
+ console.log(opts.json
499
+ ? JSON.stringify({ success: true, formId, linkageName: name, conditions: cfg.conditions.length, actions: cfg.actions.length }, null, 2)
500
+ : `联动已添加: ${name}(${cfg.conditions.length} 条件 → ${cfg.actions.length} 动作)`);
501
+ }
502
+ catch (e) {
503
+ console.error(`添加联动失败: ${e.message}`);
504
+ process.exitCode = 1;
505
+ }
506
+ });
507
+ // ---------- wf-form formula ----------
508
+ const formulaCmd = wfForm.command('formula').description('字段函数公式(/api/excel/formula/*,module=workflow)');
509
+ /** 解析 {主表.字段} / {明细表名.字段} 引用 → FormulaParam(用 getConfig 解析字段)。 */
510
+ async function buildWfFormulaParams(client, baseUrl, formId, expression) {
511
+ const errors = [];
512
+ const parameters = [];
513
+ const refNames = [];
514
+ const refRegex = /\{(?:主表|([^.}]+))\.([^}]+)\}/g;
515
+ const refs = [];
516
+ let m;
517
+ while ((m = refRegex.exec(expression)) !== null) {
518
+ const t = m[1] ? m[1].trim() : '主表';
519
+ const f = m[2].trim();
520
+ if (!refs.some(r => r.table === t && r.field === f))
521
+ refs.push({ table: t, field: f });
522
+ }
523
+ if (refs.length === 0) {
524
+ errors.push('表达式中未找到 {主表.字段名} 或 {明细表名.字段名} 引用');
525
+ return { parameters, errors, refNames };
526
+ }
527
+ const { configs } = await readConfigs(client, baseUrl, formId);
528
+ const mainFind = (name) => configs.find((c) => String(c.id) === name || c.name === name || c.dataKey === name);
529
+ for (const ref of refs) {
530
+ refNames.push(`${ref.table}.${ref.field}`);
531
+ if (ref.table === '主表') {
532
+ const f = mainFind(ref.field);
533
+ if (!f) {
534
+ errors.push(`未找到主表字段: "${ref.field}",可用: ${configs.filter((c) => !c.system).map((x) => x.name).join(', ')}`);
535
+ continue;
536
+ }
537
+ parameters.push({ dataTemplateId: '', fieldId: String(f.id), formId, module: MODULE, properKey: f.componentKey || 'Text', title: f.name || ref.field, name: f.name || ref.field, appId: '', id: null });
538
+ }
539
+ else {
540
+ const dt = configs.find((c) => (c.componentKey === 'DataTable' || c.type === 'DetailTable') && (c.name === ref.table || String(c.id) === ref.table));
541
+ if (!dt) {
542
+ errors.push(`未找到明细表: "${ref.table}"`);
543
+ continue;
544
+ }
545
+ const ch = (dt.children || []).find((x) => String(x.id) === ref.field || x.name === ref.field || x.dataKey === ref.field);
546
+ if (!ch) {
547
+ errors.push(`明细表「${ref.table}」中未找到字段: "${ref.field}"`);
548
+ continue;
549
+ }
550
+ parameters.push({ dataTemplateId: '', fieldId: String(ch.id), formId, module: MODULE, properKey: ch.componentKey || 'Text', sFormId: String(dt.id), sFormName: dt.name, title: ch.name || ref.field, name: ch.name || ref.field, appId: '', id: null });
551
+ }
552
+ }
553
+ return { parameters, errors, refNames };
554
+ }
555
+ formulaCmd.command('set')
556
+ .description('给流程表单字段绑定计算公式')
557
+ .argument('<formId>', '流程表单 formId')
558
+ .requiredOption('--expression <expr>', '公式表达式,如 "DAYS({主表.开始时间},{主表.结束时间})"')
559
+ .requiredOption('--target <fieldId>', '目标字段 ID(公式结果写入此字段)')
560
+ .option('--name <name>', '公式名称(默认自动生成)')
561
+ .option('--return-type <type>', '返回类型: Number / String / Date', 'Number')
562
+ .action(async (formId, cmdOpts) => {
563
+ try {
564
+ const opts = program.opts();
565
+ const auth = await AuthSession.fromOptions(opts);
566
+ if (!auth)
567
+ throw new Error('无法加载认证');
568
+ const client = new ApiClient(auth);
569
+ const baseUrl = auth.getBaseUrl();
570
+ const ebb = { ebbusinessid: formId };
571
+ const parsed = await buildWfFormulaParams(client, baseUrl, formId, cmdOpts.expression);
572
+ if (parsed.errors.length) {
573
+ for (const e of parsed.errors)
574
+ console.error(` ❌ ${e}`);
575
+ process.exitCode = 1;
576
+ return;
577
+ }
578
+ const formulaName = cmdOpts.name || `公式_${Date.now()}`;
579
+ const savePayload = {
580
+ expressSql: cmdOpts.expression, name: formulaName, parameters: parsed.parameters,
581
+ returnType: cmdOpts.returnType, validateType: 'normal', referenceType: 'assign',
582
+ extendParam: JSON.stringify({ exceptIds: cmdOpts.target, createType: 'once' }),
583
+ formulaId: null, dataSources: [], formulaType: 'normal', mainDataSource: [null],
584
+ tempVariableList: [], nameAuto: false, module: MODULE, multiFuncSepType: 1,
585
+ };
586
+ const saveRes = await client.post(`${baseUrl}/api/excel/formula/save`, savePayload);
587
+ if (!saveRes?.resultJson?.excute)
588
+ throw new Error(`公式保存失败: ${saveRes?.resultJson?.error || saveRes?.msg || '未知错误'}`);
589
+ const expressId = saveRes.resultJson.id;
590
+ console.error(` 公式已创建: ${expressId} (${saveRes.resultJson.formula})`);
591
+ // ffExpress 绑字段(saveFormFieldNew 重存带 ffExpress)
592
+ try {
593
+ const gf = await client.post(`${baseUrl}${ROUTE}/field/manage/getFormField`, { module: MODULE, formTableField: { formField: { id: cmdOpts.target, form: { id: formId } } } }, ebb);
594
+ const cur = gf?.data || {};
595
+ if (cur.fieldId) {
596
+ await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify({ ...cur, ffExpress: { id: expressId, name: formulaName } }) }, ebb);
597
+ }
598
+ }
599
+ catch { /* 不阻断 */ }
600
+ // saveSourceForm 注册源
601
+ await client.post(`${baseUrl}/api/excel/formula/library/saveSourceForm`, { sourceForms: [{ id: formId, module: MODULE }], expressId, filter: { fieldId: cmdOpts.target } });
602
+ console.log(opts.json
603
+ ? JSON.stringify({ success: true, expressId, formula: saveRes.resultJson.formula, targetField: cmdOpts.target }, null, 2)
604
+ : `公式已绑定到字段 ${cmdOpts.target}: ${saveRes.resultJson.formula}`);
605
+ }
606
+ catch (e) {
607
+ console.error(`绑定公式失败: ${e.message}`);
608
+ process.exitCode = 1;
609
+ }
610
+ });
611
+ formulaCmd.command('get')
612
+ .description('查看公式详情')
613
+ .argument('<formulaId>', '公式 ID (expressId)')
614
+ .action(async (formulaId) => {
615
+ try {
616
+ const opts = program.opts();
617
+ const auth = await AuthSession.fromOptions(opts);
618
+ if (!auth)
619
+ throw new Error('无法加载认证');
620
+ const client = new ApiClient(auth);
621
+ const baseUrl = auth.getBaseUrl();
622
+ const res = await client.post(`${baseUrl}/api/excel/formula/detail`, { formulaId });
623
+ const data = res?.resultJson?.data;
624
+ if (!data || !res?.resultJson?.excute)
625
+ throw new Error(`查询失败: ${res?.resultJson?.error || res?.msg || '未找到公式'}`);
626
+ if (opts.json)
627
+ console.log(JSON.stringify(data, null, 2));
628
+ else {
629
+ console.log(`公式 ID: ${data.id}\n名称: ${data.name}\n表达式: ${data.expressSql || '-'}\n返回类型: ${data.returnType || '-'}`);
630
+ console.log(`参数 (${(data.parameters || []).length}):`);
631
+ for (const p of (data.parameters || []))
632
+ console.log(` ${p.fieldId} ${p.title || p.name} [${p.properKey}]`);
633
+ }
634
+ }
635
+ catch (e) {
636
+ console.error(`查询公式失败: ${e.message}`);
637
+ process.exitCode = 1;
638
+ }
639
+ });
640
+ formulaCmd.command('list')
641
+ .description('列出流程表单的所有公式')
642
+ .argument('<formId>', '流程表单 formId')
643
+ .action(async (formId) => {
644
+ try {
645
+ const opts = program.opts();
646
+ const auth = await AuthSession.fromOptions(opts);
647
+ if (!auth)
648
+ throw new Error('无法加载认证');
649
+ const client = new ApiClient(auth);
650
+ const baseUrl = auth.getBaseUrl();
651
+ const searchRes = await client.post(`${baseUrl}/api/excel/formula/library/search`, { sourceForms: [{ id: formId, module: MODULE }] });
652
+ const all = searchRes?.page?.result || [];
653
+ const formulas = all.filter((f) => {
654
+ try {
655
+ const p = JSON.parse(f.parameter || '{}');
656
+ const vars = JSON.parse(p.formulavars || '[]');
657
+ return vars.some((v) => v.formId === formId);
658
+ }
659
+ catch {
660
+ return false;
661
+ }
662
+ });
663
+ if (opts.json)
664
+ console.log(JSON.stringify(formulas, null, 2));
665
+ else {
666
+ console.log(`共 ${formulas.length} 个公式 (全租户 ${all.length} 个):`);
667
+ for (const f of formulas) {
668
+ let expr = '';
669
+ try {
670
+ const d = await client.post(`${baseUrl}/api/excel/formula/detail`, { formulaId: f.id });
671
+ expr = d?.resultJson?.data?.expressSql || '';
672
+ }
673
+ catch { }
674
+ console.log(` [${f.id}] ${f.name} → ${expr || f.formula || '-'}`);
675
+ }
676
+ }
677
+ }
678
+ catch (e) {
679
+ console.error(`列出公式失败: ${e.message}`);
680
+ process.exitCode = 1;
681
+ }
682
+ });
683
+ formulaCmd.command('functions')
684
+ .description('查看可用函数列表')
685
+ .option('--category <name>', '按分类名或中文名过滤')
686
+ .action(async (cmdOpts) => {
687
+ try {
688
+ const opts = program.opts();
689
+ const auth = await AuthSession.fromOptions(opts);
690
+ if (!auth)
691
+ throw new Error('无法加载认证');
692
+ const client = new ApiClient(auth);
693
+ const baseUrl = auth.getBaseUrl();
694
+ const res = await client.post(`${baseUrl}/api/excel/formula/funcList`, { extendParam: JSON.stringify({ createType: 'once' }) });
695
+ const cats = res?.data || [];
696
+ if (opts.json)
697
+ console.log(JSON.stringify(cats, null, 2));
698
+ else
699
+ for (const cat of cats) {
700
+ const cn = cat.chineseName || cat.name || '未分类';
701
+ if (cmdOpts.category && !cn.includes(cmdOpts.category) && cn !== cmdOpts.category)
702
+ continue;
703
+ console.log(`\n📁 ${cn} (${(cat.children || []).length})`);
704
+ for (const fn of (cat.children || []))
705
+ console.log(` ${fn.name.padEnd(20)} ${fn.chineseName || ''} → ${fn.returnType || '?'}`);
706
+ }
707
+ }
708
+ catch (e) {
709
+ console.error(`查询函数失败: ${e.message}`);
710
+ process.exitCode = 1;
711
+ }
712
+ });
713
+ // ---------- wf-form edit-field ----------
714
+ wfForm.command('edit-field')
715
+ .description('编辑流程表单已有字段(read-modify-write:getFormField 读全字段 → 改 → saveFormFieldNew 重存)')
716
+ .argument('<formId>', '流程表单 formId')
717
+ .argument('<fieldId>', '字段 fieldId')
718
+ .option('--title <title>', '修改字段标题')
719
+ .option('--multiple', '设为多选(Employee/Department/Browser isSingle=false;Select→SelectMultiple)')
720
+ .option('--single', '设为单选(反向)')
721
+ .action(async (formId, fieldId, o) => {
722
+ try {
723
+ const opts = program.opts();
724
+ const auth = await AuthSession.fromOptions(opts);
725
+ if (!auth)
726
+ throw new Error('无法加载认证');
727
+ const client = new ApiClient(auth);
728
+ const baseUrl = auth.getBaseUrl();
729
+ const ebb = { ebbusinessid: formId };
730
+ // 读全字段
731
+ const fData = await client.post(`${baseUrl}${ROUTE}/field/manage/getFormField`, { module: MODULE, formTableField: { formField: { id: fieldId, form: { id: formId } } } }, ebb);
732
+ const jsonObj = fData?.data || {};
733
+ if (!jsonObj.fieldId)
734
+ throw new Error(`getFormField 未读到字段 fieldId=${fieldId}`);
735
+ const changes = [];
736
+ if (o.title !== undefined) {
737
+ jsonObj.title = o.title;
738
+ changes.push(`title="${o.title}"`);
739
+ }
740
+ if (o.multiple === true || o.single === true) {
741
+ const multi = o.multiple === true;
742
+ const t = jsonObj.type;
743
+ if (t === 'Employee' || t === 'Department' || t === 'Browser') {
744
+ jsonObj.isSingle = !multi;
745
+ changes.push(`isSingle=${!multi}`);
746
+ }
747
+ else if (t === 'Select' || t === 'SelectMultiple') {
748
+ jsonObj.type = multi ? 'SelectMultiple' : 'Select';
749
+ jsonObj.componentKey = jsonObj.type;
750
+ changes.push(`type=${jsonObj.type}`);
751
+ }
752
+ else {
753
+ throw new Error(`字段类型 ${t} 不支持多选切换(仅 Employee/Department/Browser/Select)`);
754
+ }
755
+ }
756
+ if (changes.length === 0)
757
+ throw new Error('未指定要修改的属性(--title / --multiple / --single)');
758
+ const r = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(jsonObj) }, ebb);
759
+ if (r?.code !== 200 || !r?.status)
760
+ throw new Error(`编辑失败: ${r?.msg || '未知错误'}`);
761
+ // 回读确认
762
+ const r2 = await client.post(`${baseUrl}${ROUTE}/field/manage/getFormField`, { module: MODULE, formTableField: { formField: { id: fieldId, form: { id: formId } } } }, ebb);
763
+ const after = r2?.data || {};
764
+ console.log(opts.json
765
+ ? JSON.stringify({ success: true, formId, fieldId, title: after.title, type: after.type, isSingle: after.isSingle }, null, 2)
766
+ : `字段已编辑: ${after.title} (${after.type})${after.isSingle !== undefined ? ` isSingle=${after.isSingle}` : ''} 改了 ${changes.join(', ')}`);
767
+ }
768
+ catch (e) {
769
+ console.error(`编辑字段失败: ${e.message}`);
770
+ process.exitCode = 1;
771
+ }
772
+ });
773
+ // ---------- wf-form field-options ----------
774
+ wfForm.command('field-options')
775
+ .description('给流程表单 Select/SelectMultiple 字段配选项(read-modify-write:getFormField 读全字段 → 改 options → saveFormFieldNew 重存)')
776
+ .argument('<formId>', '流程表单 formId')
777
+ .argument('<fieldId>', 'Select/SelectMultiple 字段 fieldId')
778
+ .requiredOption('--options <json>', '选项列表 [{"name":"事假"},...] 或 @文件路径')
779
+ .option('--default <valueKey>', '默认选项 valueKey(序号字符串,如 "0")')
780
+ .action(async (formId, fieldId, cmdOpts) => {
781
+ try {
782
+ const opts = program.opts();
783
+ const auth = await AuthSession.fromOptions(opts);
784
+ if (!auth)
785
+ throw new Error('无法加载认证');
786
+ const client = new ApiClient(auth);
787
+ const baseUrl = auth.getBaseUrl();
788
+ const ebb = { ebbusinessid: formId };
789
+ // 解析选项
790
+ let optStr = cmdOpts.options;
791
+ if (optStr.startsWith('@')) {
792
+ const { readFileSync, existsSync } = await import('node:fs');
793
+ if (!existsSync(optStr.slice(1)))
794
+ throw new Error('选项文件不存在');
795
+ optStr = readFileSync(optStr.slice(1), 'utf-8');
796
+ }
797
+ let optionDefs;
798
+ try {
799
+ optionDefs = JSON.parse(optStr);
800
+ }
801
+ catch {
802
+ throw new Error('选项 JSON 解析失败');
803
+ }
804
+ if (!Array.isArray(optionDefs) || optionDefs.length === 0)
805
+ throw new Error('--options 必须是非空数组');
806
+ // 读全字段对象(read-modify-write)
807
+ const fData = await client.post(`${baseUrl}${ROUTE}/field/manage/getFormField`, { module: MODULE, formTableField: { formField: { id: fieldId, form: { id: formId } } } }, ebb);
808
+ const currentField = fData?.data || {};
809
+ if (!currentField.fieldId)
810
+ throw new Error(`getFormField 未读到字段 fieldId=${fieldId}`);
811
+ if (currentField.type !== 'Select' && currentField.type !== 'SelectMultiple') {
812
+ throw new Error(`字段类型 ${currentField.type} 不支持配选项(仅 Select/SelectMultiple)`);
813
+ }
814
+ // 构造 options(对齐浏览器抓包形状:selectionId 空让平台自动生成雪花,valueKey 用序号字符串)
815
+ const options = optionDefs.map((o, i) => {
816
+ const vk = String(o.valueKey ?? i);
817
+ return {
818
+ componentKey: 'Option', name: o.name, order: i,
819
+ selectionId: '', defOption: cmdOpts.default === vk,
820
+ uid: randomUUID(), other: false, archive: false, isLimit: false,
821
+ optionNum: '1000000', score: '0',
822
+ styleConfig: { OptionBackground: OPTION_COLORS[i % OPTION_COLORS.length] },
823
+ valueKey: vk,
824
+ };
825
+ });
826
+ // 合并重存(保留 currentField 全部属性,只替换 options)
827
+ const jsonObj = { ...currentField, options };
828
+ const r = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(jsonObj) }, ebb);
829
+ if (r?.code !== 200 || !r?.status)
830
+ throw new Error(`配选项失败: ${r?.msg || '未知错误'}`);
831
+ // 回读确认
832
+ const r2 = await client.post(`${baseUrl}${ROUTE}/field/manage/getFormField`, { module: MODULE, formTableField: { formField: { id: fieldId, form: { id: formId } } } }, ebb);
833
+ const saved = (r2?.data?.options || []).map((o) => `${o.name}(vk=${o.valueKey}${o.defOption ? ',默认' : ''})`);
834
+ console.log(opts.json
835
+ ? JSON.stringify({ success: true, formId, fieldId, options: saved }, null, 2)
836
+ : `选项已配置: ${currentField.title} → ${saved.join(', ')}`);
837
+ }
838
+ catch (e) {
839
+ console.error(`配选项失败: ${e.message}`);
840
+ process.exitCode = 1;
841
+ }
842
+ });
843
+ // ---------- wf-form add-detail-table ----------
844
+ wfForm.command('add-detail-table')
845
+ .description('给流程表单加明细子表(saveSubForm 建子表 + 加首字段使其可见;--init 放入布局)')
846
+ .argument('<formId>', '流程表单 formId')
847
+ .argument('<name>', '明细表名')
848
+ .argument('[firstFieldName]', '首字段名(强烈建议传;空子表 getConfig 读不到)')
849
+ .option('--data-key <k>', '子表物理表名/dataKey(默认 uf_拼音_随机)')
850
+ .option('--init', '创建后跑 layout init 放入 DetailTable 组件')
851
+ .option('--first-field-type <t>', '首字段类型(默认 Text)')
852
+ .action(async (formId, name, firstFieldName, o) => {
853
+ try {
854
+ const opts = program.opts();
855
+ const auth = await AuthSession.fromOptions(opts);
856
+ if (!auth)
857
+ throw new Error('无法加载认证');
858
+ const client = new ApiClient(auth);
859
+ const baseUrl = auth.getBaseUrl();
860
+ const ebb = { ebbusinessid: formId };
861
+ const [subFormId] = await genIds(client, baseUrl, 1);
862
+ if (!subFormId)
863
+ throw new Error('生成 subFormId 失败');
864
+ const dataKey = o.dataKey || toSubTableName(name);
865
+ const subRes = await client.post(`${baseUrl}${ROUTE}/field/manage/saveSubForm`, {
866
+ physicalTable: true, order: 0, subFormId, type: 'DetailTable',
867
+ customParam: {}, module: MODULE, dataKey, tableName: dataKey,
868
+ formTable: { id: formId, form: { id: subFormId, name, order: '0.00' } },
869
+ }, ebb);
870
+ if (subRes?.code !== 200 || !subRes?.status)
871
+ throw new Error(`saveSubForm 失败: ${subRes?.msg}`);
872
+ console.error(` 子表已建: subFormId=${subFormId} name=${name} dataKey=${dataKey}`);
873
+ let firstFieldId = '';
874
+ if (firstFieldName) {
875
+ const [fid] = await genIds(client, baseUrl, 1);
876
+ firstFieldId = fid;
877
+ const ftype = canon(o.firstFieldType || 'Text');
878
+ const fo = {
879
+ type: ftype, title: firstFieldName, fieldId: firstFieldId, groupId: '-1', formId, showOrder: '0.00',
880
+ dataKey: toSubTableName(firstFieldName).replace(/^uf_/, ''), columnName: toSubTableName(firstFieldName).replace(/^uf_/, ''),
881
+ componentKey: ftype, subFormId,
882
+ };
883
+ const fRes = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(fo) }, ebb);
884
+ if (fRes?.code !== 200 || !fRes?.status)
885
+ throw new Error(`加首字段失败: ${fRes?.msg}`);
886
+ console.error(` 首字段已加: ${firstFieldName} (${firstFieldId})`);
887
+ }
888
+ else {
889
+ console.error(' ⚠ 未传首字段:子表已建但空子表 getConfig 读不到,需手动加字段后才可 init 放置');
890
+ }
891
+ let compCount = 0;
892
+ if (o.init) {
893
+ if (!firstFieldName)
894
+ throw new Error('--init 需配合首字段名(空子表放不了 DetailTable)');
895
+ const meta = await getLayoutMeta(client, baseUrl, formId);
896
+ const cfg = await client.post(`${baseUrl}${ROUTE}/view/relate/getConfig`, {
897
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
898
+ customParam: { terminal: '', objId: formId }, initMode: 5, groupFieldIds: [],
899
+ designerParam: { sourceModule: MODULE },
900
+ }, ebb);
901
+ const configs = normalizeDetailTableChildren((cfg?.data?.configs || []).map((c) => {
902
+ const isBusiness = !c.system && c.componentKey !== 'DataTable' && c.type !== 'DetailTable';
903
+ const out = { ...c, display: c.system ? c.display : true, show: c.system ? c.show : true };
904
+ if (isBusiness) {
905
+ out.readOnly = false;
906
+ out.disableEdit = false;
907
+ } // FLOW 默认锁,强制可编辑
908
+ return out;
909
+ }));
910
+ const styleTemplateId = meta.layoutType === 'FLOW' ? '820000000000000001' : '700000000000000008';
911
+ const initRes = await client.post(`${baseUrl}${ROUTE}/view/relate/initLayout`, {
912
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId, generateCollapse: true,
913
+ styleTemplateId, styleTemplateType: 1, configs, addLayout: false, ecodeType: ['js', 'css'],
914
+ initMode: '1', customParam: { terminal: '', objId: formId },
915
+ formLayoutMultiple: { isSys: true, id: meta.layoutMultiId, name: meta.layoutName, terminalType: 'PC', layoutType: meta.layoutType },
916
+ designerParam: { onlyLayout: false },
917
+ }, ebb);
918
+ if (initRes?.code !== 200 || !initRes?.status)
919
+ throw new Error(`initLayout 失败: ${initRes?.msg}`);
920
+ compCount = initRes?.data?.pageLayouts?.[0]?.comps?.length || initRes?.data?.formLayout?.pageLayouts?.[0]?.comps?.length || 0;
921
+ console.error(` 布局已 init: 组件 ${compCount} 个(含 DetailTable)`);
922
+ }
923
+ console.log(opts.json
924
+ ? JSON.stringify({ success: true, formId, subFormId, name, dataKey, firstFieldId, initComps: compCount || undefined }, null, 2)
925
+ : `明细子表已建: ${name} subFormId=${subFormId}${firstFieldName ? ` 首字段=${firstFieldName}` : ''}${o.init ? ` 已 init 放入布局` : ' (未 init,跑 wf-form layout init 放置)'}`);
926
+ if (!opts.json && !o.init) {
927
+ console.log(' 后续加字段: e10-cli wf-form add-field <formId> <字段名> <类型> --sub-form-id ' + subFormId);
928
+ }
929
+ }
930
+ catch (e) {
931
+ console.error(`加明细子表失败: ${e.message}`);
932
+ process.exitCode = 1;
933
+ }
934
+ });
935
+ // ---------- wf-form layout init ----------
936
+ const layoutCmd = wfForm.command('layout').description('布局管理');
937
+ layoutCmd.command('init')
938
+ .description('初始化/重建流程表单布局(initLayout):按 layoutType 选模板(FLOW 默认)+ 设字段 readOnly/required/singleRow')
939
+ .argument('<formId>', '流程表单 formId')
940
+ .option('--style-template <name|id>', '样式模板 id(默认按 layoutType:FLOW=820...01 / EXCEL=700...08)')
941
+ .option('--read-only', '字段只读')
942
+ .option('--required', '字段必填')
943
+ .option('--single-row', '独占一行')
944
+ .option('--generate-collapse', '生成折叠分组')
945
+ .action(async (formId, o) => {
946
+ try {
947
+ const opts = program.opts();
948
+ const auth = await AuthSession.fromOptions(opts);
949
+ if (!auth)
950
+ throw new Error('无法加载认证');
951
+ const client = new ApiClient(auth);
952
+ const baseUrl = auth.getBaseUrl();
953
+ const ebb = { ebbusinessid: formId };
954
+ const meta = await getLayoutMeta(client, baseUrl, formId);
955
+ if (!meta.layoutId)
956
+ throw new Error('无法获取 layoutId');
957
+ const cfgRes = await client.post(`${baseUrl}${ROUTE}/view/relate/getConfig`, {
958
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
959
+ customParam: { terminal: '', objId: formId }, initMode: 5, groupFieldIds: [],
960
+ designerParam: { sourceModule: MODULE },
961
+ }, ebb);
962
+ // 流程表单 FLOW 布局 getConfig 默认把业务字段 readOnly=true(锁死),init 时强制可编辑:
963
+ // 业务字段 readOnly=false / disableEdit=false(除非 --read-only 显式锁);明细/系统字段保留。
964
+ const configs = normalizeDetailTableChildren((cfgRes?.data?.configs || []).map((c) => {
965
+ const isBusiness = !c.system && c.componentKey !== 'DataTable' && c.type !== 'DetailTable';
966
+ const out = { ...c, display: c.system ? c.display : true, show: c.system ? c.show : true };
967
+ if (isBusiness) {
968
+ out.readOnly = o.readOnly === true;
969
+ out.disableEdit = false;
970
+ if (o.required !== undefined)
971
+ out.required = o.required;
972
+ if (o.singleRow !== undefined)
973
+ out.singleRow = o.singleRow;
974
+ }
975
+ return out;
976
+ }));
977
+ const styleTemplateId = o.styleTemplate
978
+ || (meta.layoutType === 'FLOW' ? '820000000000000001' : '700000000000000008');
979
+ const initRes = await client.post(`${baseUrl}${ROUTE}/view/relate/initLayout`, {
980
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
981
+ generateCollapse: o.generateCollapse !== false,
982
+ styleTemplateId: /^\d+$/.test(styleTemplateId) ? styleTemplateId : styleTemplateId, styleTemplateType: 1,
983
+ configs, addLayout: false, ecodeType: ['js', 'css'], initMode: '1',
984
+ customParam: { terminal: '', objId: formId },
985
+ formLayoutMultiple: { isSys: true, id: meta.layoutMultiId, name: meta.layoutName, terminalType: 'PC', layoutType: meta.layoutType },
986
+ designerParam: { onlyLayout: false },
987
+ }, ebb);
988
+ if (initRes?.code !== 200 || !initRes?.status)
989
+ throw new Error(`initLayout 失败: ${initRes?.msg}`);
990
+ const comps = initRes?.data?.pageLayouts?.[0]?.comps || initRes?.data?.formLayout?.pageLayouts?.[0]?.comps || [];
991
+ console.log(opts.json
992
+ ? JSON.stringify({ success: true, formId, layoutType: meta.layoutType, styleTemplateId, comps: comps.length }, null, 2)
993
+ : `布局已初始化: 类型=${meta.layoutType} 模板=${styleTemplateId} 组件=${comps.length}`);
994
+ }
995
+ catch (e) {
996
+ console.error(`初始化布局失败: ${e.message}`);
997
+ process.exitCode = 1;
998
+ }
999
+ });
1000
+ return wfForm;
1001
+ }
1002
+ //# sourceMappingURL=wf-form.js.map