weaver-e10-builder 1.0.7-beta → 1.0.8-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 (31) hide show
  1. package/dist/commands/wf-form.d.ts.map +1 -1
  2. package/dist/commands/wf-form.js +824 -42
  3. package/dist/commands/wf-form.js.map +1 -1
  4. package/dist/commands/workflow-action.d.ts.map +1 -1
  5. package/dist/commands/workflow-action.js +34 -28
  6. package/dist/commands/workflow-action.js.map +1 -1
  7. package/dist/commands/workflow-create-workflow.d.ts.map +1 -1
  8. package/dist/commands/workflow-create-workflow.js +75 -25
  9. package/dist/commands/workflow-create-workflow.js.map +1 -1
  10. package/dist/commands/workflow-node-robot.js +1 -1
  11. package/dist/commands/workflow-node-robot.js.map +1 -1
  12. package/dist/commands/workflow-node-validate.d.ts.map +1 -1
  13. package/dist/commands/workflow-node-validate.js +56 -45
  14. package/dist/commands/workflow-node-validate.js.map +1 -1
  15. package/dist/commands/workflow-numbering.d.ts.map +1 -1
  16. package/dist/commands/workflow-numbering.js +98 -2
  17. package/dist/commands/workflow-numbering.js.map +1 -1
  18. package/dist/commands/workflow-operator-sign.d.ts +3 -0
  19. package/dist/commands/workflow-operator-sign.d.ts.map +1 -0
  20. package/dist/commands/workflow-operator-sign.js +118 -0
  21. package/dist/commands/workflow-operator-sign.js.map +1 -0
  22. package/dist/commands/workflow.d.ts.map +1 -1
  23. package/dist/commands/workflow.js +83 -6
  24. package/dist/commands/workflow.js.map +1 -1
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/services/workflow-initiator.d.ts +15 -0
  28. package/dist/services/workflow-initiator.d.ts.map +1 -0
  29. package/dist/services/workflow-initiator.js +77 -0
  30. package/dist/services/workflow-initiator.js.map +1 -0
  31. package/package.json +1 -1
@@ -31,6 +31,8 @@ function canon(ck) {
31
31
  textarea: 'TextArea', text: 'Text', number: 'Number', money: 'Money',
32
32
  date: 'Date', datetime: 'DateTime', select: 'Select', selectmultiple: 'SelectMultiple',
33
33
  employee: 'Employee', department: 'Department', browser: 'Browser',
34
+ file: 'FileComponent', filecomponent: 'FileComponent', upload: 'FileComponent', 附件: 'FileComponent',
35
+ ebuilder: 'Ebuilder', eb: 'Ebuilder', flow: 'Flow',
34
36
  };
35
37
  return m[ck.toLowerCase()] || ck;
36
38
  }
@@ -55,6 +57,52 @@ async function readConfigs(client, baseUrl, formId) {
55
57
  throw new Error(`getConfig 失败: ${cfg?.msg}`);
56
58
  return { ...meta, configs: normalizeDetailTableChildren(cfg?.data?.configs || []) };
57
59
  }
60
+ /** 读取运行时布局(getFormLayout pageLayouts comps),返回 singleRow=true 的字段 ID 集合。
61
+ * getConfig 的 configs 不带已保存的 singleRow(设计视图默认 false);field-prop/group/layout init
62
+ * 重排前若不从运行时合并回来,独占一行字段会被 initLayout 冲掉(实测:group initLayout 后 客户地址 singleRow 丢失)。 */
63
+ async function fetchRuntimeSingleRows(client, baseUrl, formId) {
64
+ try {
65
+ const res = await client.post(`${baseUrl}${ROUTE}/builder/getFormLayout`, {
66
+ formId, module: MODULE, customParam: {}, formTableIdList: [],
67
+ designerParam: { onlyLayout: false, convert: false }, form: { id: formId },
68
+ }, { ebbusinessid: formId });
69
+ const pls = res?.data?.formLayout?.pageLayouts || res?.data?.pageLayouts || [];
70
+ const comps = pls[0]?.comps || [];
71
+ const set = new Set();
72
+ const walk = (arr) => {
73
+ for (const x of arr) {
74
+ const cfg = x.config || x;
75
+ const fid = cfg.fieldId || cfg.id || x.id;
76
+ if (fid && cfg.singleRow === true)
77
+ set.add(String(fid));
78
+ if (x.components?.length)
79
+ walk(x.components);
80
+ if (x.children?.length)
81
+ walk(x.children);
82
+ }
83
+ };
84
+ walk(comps);
85
+ return set;
86
+ }
87
+ catch {
88
+ return new Set();
89
+ }
90
+ }
91
+ /** 将运行时已保存的 singleRow 合并回 getConfig configs(递归含 Collapse children),保留独占一行不被重排冲掉。 */
92
+ function mergeRuntimeSingleRow(configs, srSet) {
93
+ if (!srSet.size)
94
+ return;
95
+ const walk = (arr) => {
96
+ for (const c of arr) {
97
+ const fid = c.id || c.fieldId;
98
+ if (fid && srSet.has(String(fid)))
99
+ c.singleRow = true;
100
+ if (c.children?.length)
101
+ walk(c.children);
102
+ }
103
+ };
104
+ walk(configs);
105
+ }
58
106
  async function genIds(client, baseUrl, n) {
59
107
  const r = await client.post(`${baseUrl}/api/form/local/generatorIds?size=${n}`, {});
60
108
  return (r?.data?.generatorIds || []).map(String);
@@ -93,9 +141,76 @@ export function registerWorkflowFormCommands(program) {
93
141
  process.exitCode = 1;
94
142
  }
95
143
  });
144
+ /** 构建字段 jsonObj(add-field / add-fields 共用)。返回未带 fieldId 的模板,调用方填 fieldId。 */
145
+ function buildFieldJsonObj(fieldName, fieldType, title, o, formId, fieldId) {
146
+ const ck = canon(fieldType);
147
+ const colName = toColumnName(fieldName);
148
+ const jsonObj = {
149
+ type: ck, title: title || fieldName, fieldId, groupId: '-1', formId, showOrder: '0.00',
150
+ dataKey: colName, columnName: colName,
151
+ };
152
+ if (o.singleRow === true)
153
+ jsonObj.singleRow = true;
154
+ if (o.subFormId)
155
+ jsonObj.subFormId = o.subFormId;
156
+ if (ck === 'Number' || ck === 'Money') {
157
+ const isPercent = ck === 'Number' && o.percent === true;
158
+ const hasDefault = o.default !== undefined && o.default !== null && o.default !== '';
159
+ Object.assign(jsonObj, {
160
+ pointSize: o.precision !== undefined ? o.precision : (isPercent ? '1' : (ck === 'Money' ? '2' : '0')),
161
+ isDefault: hasDefault, isDelimiter: ck === 'Money', isCapital: false,
162
+ capitalFormat: 'onlyUpperCase', capitalPosition: 'outside', capitalSetting: 'system',
163
+ capitalPrecision: 'yuan', isExchangeRates: false,
164
+ ...(ck === 'Number' ? { numberType: isPercent ? 'percentage' : 'number', content: hasDefault ? String(o.default) : '' } : {}),
165
+ ...(hasDefault ? { stepMode: false, step: 1 } : {}),
166
+ });
167
+ }
168
+ else if (ck === 'TextArea') {
169
+ Object.assign(jsonObj, { content: '', maxLen: 4000 });
170
+ }
171
+ else if (ck === 'Date' || ck === 'DateTime') {
172
+ const fmt = o.format || (ck === 'DateTime' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd');
173
+ Object.assign(jsonObj, { type: 'Date', format: fmt, placeholder: placeholderFromFormat(fmt), timePanelStyle: { type: 'system' }, isDefault: false, earlyType: 'currentDay', lateType: 'currentDay' });
174
+ if (o.defaultCurrent) {
175
+ jsonObj.isDefault = true;
176
+ jsonObj.defaultExtend = { date: { type: 'current' }, time: { customValue: '00:00', type: 'custom' } };
177
+ }
178
+ }
179
+ else if (ck === 'Employee' || ck === 'Department' || ck === 'Browser') {
180
+ jsonObj.isSingle = o.multiple ? false : true;
181
+ if (o.defaultCurrent && ck === 'Employee') {
182
+ jsonObj.defaultExtend = { type: 'current', relation: 'current', virtualId: '1' };
183
+ }
184
+ }
185
+ else if (ck === 'Select') {
186
+ if (o.multiple) {
187
+ jsonObj.type = 'SelectMultiple';
188
+ jsonObj.componentKey = 'SelectMultiple';
189
+ }
190
+ }
191
+ else if (ck === 'EBuilder' || ck === 'Flow') {
192
+ // EBuilder 关联字段(浏览器抓包 saveFormFieldNew 实测格式):type=EBuilder(大写EB),
193
+ // 标准字段元数据(isAllFormData/hasDisplayFormat/openRelateView/dataCount/currentLevel/columnType/
194
+ // columnTypeSwitch/singleEdit/typeLen)+ displayType=space + isAddForRelation=false + sformId/dataTitle/formName
195
+ // + content/formEntity/filterQueryParams/filterParams(抓包含,filterParams 可能承载字段层筛选)。
196
+ Object.assign(jsonObj, {
197
+ isAllFormData: false, viewDataDetail: true, quickAddParams: [],
198
+ hasDisplayFormat: false, openRelateView: false,
199
+ dataCount: 0, currentLevel: '1', columnType: 'TEXT', columnTypeSwitch: false,
200
+ singleEdit: true, typeLen: 16, isAddForRelation: false, displayType: 'space',
201
+ sformId: o.sformId || '', dataTitle: o.dataTitle || 'name',
202
+ relateViewData: [], formName: o.formName || '', relateField: null,
203
+ content: [], formEntity: {}, filterQueryParams: '{}', filterParams: '{}',
204
+ virtualForm: false, appId: o.appId || '', virtualTable: false,
205
+ });
206
+ }
207
+ if (!jsonObj.componentKey)
208
+ jsonObj.componentKey = jsonObj.type;
209
+ return jsonObj;
210
+ }
96
211
  // ---------- wf-form add-field ----------
97
212
  wfForm.command('add-field')
98
- .description('给流程表单加字段(Text/TextArea/Number/Money/Date/DateTime/Select/Employee/Department/Browser)')
213
+ .description('给流程表单加字段(Text/TextArea/Number/Money/Date/DateTime/Select/Employee/Department/Browser/EBuilder/Flow)')
99
214
  .argument('<formId>', '流程表单 formId')
100
215
  .argument('<fieldName>', '字段名')
101
216
  .argument('<fieldType>', '字段类型')
@@ -106,6 +221,12 @@ export function registerWorkflowFormCommands(program) {
106
221
  .option('--percent', 'Number 百分比显示(numberType=percentage)')
107
222
  .option('--default <value>', 'Number/Money 默认值')
108
223
  .option('--format <fmt>', '日期精度(Date 默认 yyyy-MM-dd / DateTime 默认 yyyy-MM-dd HH:mm)')
224
+ .option('--single-row', '独占一行显示(布局 singleRow:true,其余字段仍两列;对齐 eb 表 form add-field --single-row)')
225
+ .option('--default-current', '系统默认值:Date=当前日期 / Employee=创建人(对齐 eb 表 fields.json defaultValue:true)')
226
+ .option('--sform-id <objId>', 'EBuilder/Flow 关联的目标表单 objId(EBuilder 必填)')
227
+ .option('--app-id <appId>', '目标表单所属应用 appId(EBuilder 建议传,不传 formName 自动解析需要)')
228
+ .option('--data-title <field>', 'EBuilder 关联显示字段(默认 name;目标表含"名称"的字段或 fieldId)')
229
+ .option('--form-name <name>', '目标表单名称(不传自动解析)')
109
230
  .action(async (formId, fieldName, fieldType, title, o) => {
110
231
  try {
111
232
  const opts = program.opts();
@@ -120,43 +241,23 @@ export function registerWorkflowFormCommands(program) {
120
241
  const [fieldId] = await genIds(client, baseUrl, 1);
121
242
  if (!fieldId)
122
243
  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';
244
+ // EBuilder 关联:校验 sformId,解析 formName(getList by appId)
245
+ if (ck === 'EBuilder' || ck === 'Flow') {
246
+ if (!o.sformId)
247
+ throw new Error('EBuilder/Flow 类型必须传 --sform-id <目标表单 objId>');
248
+ if (!o.formName && o.appId) {
249
+ try {
250
+ const fl = await client.get(`${baseUrl}/api/bs/ebuilder/form/obj/getList/${o.appId}?apid=${o.appId}`, { ebbusinessid: o.appId });
251
+ const tf = (fl?.data || []).find((f) => String(f.id) === String(o.sformId));
252
+ if (tf?.formName || tf?.name)
253
+ o.formName = tf.formName || tf.name;
254
+ }
255
+ catch { /* 留空兜底 */ }
155
256
  }
156
- // 选项需后续单独配(流程表单暂无 field-options 命令)
257
+ if (!o.formName)
258
+ o.formName = o.sformId; // 兜底
157
259
  }
158
- if (!jsonObj.componentKey)
159
- jsonObj.componentKey = jsonObj.type;
260
+ const jsonObj = buildFieldJsonObj(fieldName, fieldType, title, o, formId, fieldId);
160
261
  const r = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(jsonObj) }, ebb);
161
262
  if (r?.code !== 200 || !r?.status)
162
263
  throw new Error(`加字段失败: ${r?.msg || '未知错误'}`);
@@ -170,6 +271,52 @@ export function registerWorkflowFormCommands(program) {
170
271
  process.exitCode = 1;
171
272
  return;
172
273
  }
274
+ // EBuilder/Flow 关联字段 follow-up:流程表单平台对从零构造的 jsonObj 不落 sformId/formName/appId/
275
+ // dataTitle/displayType(getFormField 读不到),需 getFormField 完整配置 + 补全关联信息 + ffDisplayColumns
276
+ // 二次 save 才落库(mulinquan/eteams 实测:full + sformId 二次 save 后回读 sformId 正确)。
277
+ // 对齐浏览器 saveFormFieldNew 格式:type=EBuilder / componentKey=Ebuilder(小写b) / displayType=space / isAddForRelation=false。
278
+ if (ck === 'EBuilder' || ck === 'Flow') {
279
+ try {
280
+ const sform = o.sformId || jsonObj.sformId;
281
+ if (sform) {
282
+ const tfd = await client.post(`${baseUrl}/api/ebuilder/form/dataset/v1/getFields`, { objId: sform, userid: auth.userId, tenantKey: auth.tenantKey, isReturnDetail: 'y' });
283
+ const displayCols = (tfd?.data?.mainFields || [])
284
+ .filter((f) => /^\d+$/.test(String(f.fieldId)))
285
+ .map((f) => ({ id: String(f.fieldId), name: String(f.fieldName || f.title || '') }))
286
+ .slice(0, 5);
287
+ const gf = await client.post(`${baseUrl}${ROUTE}/field/manage/getFormField`, { module: MODULE, formTableField: { formField: { id: fieldId, form: { id: formId } } } }, ebb);
288
+ const full = gf?.data || {};
289
+ if (full.fieldId) {
290
+ const fullWithEb = {
291
+ ...full,
292
+ sformId: sform,
293
+ appId: o.appId || full.appId || '',
294
+ formName: o.formName || full.formName || '',
295
+ dataTitle: o.dataTitle || 'name',
296
+ displayType: 'space',
297
+ isAddForRelation: false,
298
+ componentKey: 'Ebuilder',
299
+ type: 'EBuilder',
300
+ content: full.content || [],
301
+ formEntity: full.formEntity || {},
302
+ filterQueryParams: full.filterQueryParams || '{}',
303
+ filterParams: full.filterParams || '{}',
304
+ virtualForm: false,
305
+ virtualTable: false,
306
+ ...(displayCols.length ? { ffDisplayColumns: displayCols } : {}),
307
+ };
308
+ await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(fullWithEb) }, ebb);
309
+ if (displayCols.length)
310
+ console.error(` EBuilder 关联已配: ${o.formName || sform} → 显示列 ${displayCols.map((c) => c.name).join('/')}`);
311
+ else
312
+ console.error(` EBuilder 关联已配: ${o.formName || sform}`);
313
+ }
314
+ }
315
+ }
316
+ catch (e) {
317
+ console.error(` ⚠ EBuilder 关联配置失败: ${e.message}`);
318
+ }
319
+ }
173
320
  console.log(opts.json
174
321
  ? JSON.stringify({ success: true, formId, fieldId, fieldName, componentKey: jsonObj.componentKey, subFormId: o.subFormId || undefined }, null, 2)
175
322
  : `字段已加: ${fieldName} (${jsonObj.componentKey}) fieldId=${fieldId}${o.subFormId ? ` (明细 ${o.subFormId})` : ''}${ck === 'Select' ? ' 注:选项需单独配' : ''}`);
@@ -710,14 +857,97 @@ export function registerWorkflowFormCommands(program) {
710
857
  process.exitCode = 1;
711
858
  }
712
859
  });
860
+ // ---------- wf-form add-fields(批量)----------
861
+ wfForm.command('add-fields')
862
+ .description('批量加字段(@fields.json,末尾自动 layout init 重新布局)。JSON 数组,每项 {name,type,title?,subFormId?,multiple?,precision?,percent?,default?,format?,singleRow?,defaultCurrent?}')
863
+ .argument('<formId>', '流程表单 formId')
864
+ .argument('<fieldsJson>', '字段定义 JSON / @文件路径')
865
+ .option('--no-init', '跳过末尾自动 layout init(默认加完自动重排布局)')
866
+ .action(async (formId, fieldsJson, cmdOpts) => {
867
+ try {
868
+ const opts = program.opts();
869
+ const auth = await AuthSession.fromOptions(opts);
870
+ if (!auth)
871
+ throw new Error('无法加载认证');
872
+ const client = new ApiClient(auth);
873
+ const baseUrl = auth.getBaseUrl();
874
+ const ebb = { ebbusinessid: formId };
875
+ let str = fieldsJson;
876
+ if (str.startsWith('@')) {
877
+ const { readFileSync, existsSync } = await import('node:fs');
878
+ if (!existsSync(str.slice(1)))
879
+ throw new Error('文件不存在');
880
+ str = readFileSync(str.slice(1), 'utf-8');
881
+ }
882
+ let defs;
883
+ try {
884
+ defs = JSON.parse(str);
885
+ }
886
+ catch {
887
+ throw new Error('fieldsJson 解析失败');
888
+ }
889
+ if (!Array.isArray(defs) || !defs.length)
890
+ throw new Error('fieldsJson 需为非空数组');
891
+ const ids = await genIds(client, baseUrl, defs.length);
892
+ let added = 0;
893
+ for (let i = 0; i < defs.length; i++) {
894
+ const d = defs[i];
895
+ if (!d.name || !d.type) {
896
+ console.error(` ⚠ 第 ${i + 1} 项缺 name/type,跳过`);
897
+ continue;
898
+ }
899
+ const fieldId = ids[i];
900
+ const jsonObj = buildFieldJsonObj(d.name, d.type, d.title, d, formId, fieldId);
901
+ const r = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(jsonObj) }, ebb);
902
+ if (r?.code === 200 && r?.status) {
903
+ added++;
904
+ console.error(` + ${d.name} (${d.type}) fieldId=${fieldId}`);
905
+ }
906
+ else
907
+ console.error(` ✗ ${d.name} 失败: ${r?.msg || '未知错误'}`);
908
+ }
909
+ console.error(`已加 ${added}/${defs.length} 个字段`);
910
+ // 末尾自动 layout init(解决 add-field 不自动重新布局的缺口)
911
+ let compCount = 0;
912
+ if (cmdOpts.init !== false && added > 0) {
913
+ const meta = await getLayoutMeta(client, baseUrl, formId);
914
+ const cfg = await client.post(`${baseUrl}${ROUTE}/view/relate/getConfig`, {
915
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
916
+ customParam: { terminal: '', objId: formId }, initMode: 5, groupFieldIds: [],
917
+ designerParam: { sourceModule: MODULE },
918
+ }, ebb);
919
+ const configs = normalizeDetailTableChildren((cfg?.data?.configs || []).map((c) => ({ ...c, display: c.system ? c.display : true, show: c.system ? c.show : true })));
920
+ const styleTemplateId = meta.layoutType === 'FLOW' ? '820000000000000001' : '700000000000000008';
921
+ const initRes = await client.post(`${baseUrl}${ROUTE}/view/relate/initLayout`, {
922
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId, generateCollapse: true,
923
+ styleTemplateId, styleTemplateType: 1, configs, addLayout: false, ecodeType: ['js', 'css'],
924
+ initMode: '1', customParam: { terminal: '', objId: formId },
925
+ formLayoutMultiple: { isSys: true, id: meta.layoutMultiId, name: meta.layoutName, terminalType: 'PC', layoutType: meta.layoutType },
926
+ designerParam: { onlyLayout: false },
927
+ }, ebb);
928
+ compCount = initRes?.data?.pageLayouts?.[0]?.comps?.length || initRes?.data?.formLayout?.pageLayouts?.[0]?.comps?.length || 0;
929
+ console.error(` 布局已 init: 组件 ${compCount} 个`);
930
+ }
931
+ console.log(opts.json
932
+ ? JSON.stringify({ success: true, formId, added, total: defs.length, initComps: compCount || undefined }, null, 2)
933
+ : `批量加字段完成: ${added}/${defs.length} 个${cmdOpts.init !== false ? ` + 自动布局 ${compCount} 组件` : ''}`);
934
+ }
935
+ catch (e) {
936
+ console.error(`批量加字段失败: ${e.message}`);
937
+ process.exitCode = 1;
938
+ }
939
+ });
713
940
  // ---------- wf-form edit-field ----------
714
941
  wfForm.command('edit-field')
715
942
  .description('编辑流程表单已有字段(read-modify-write:getFormField 读全字段 → 改 → saveFormFieldNew 重存)')
716
943
  .argument('<formId>', '流程表单 formId')
717
- .argument('<fieldId>', '字段 fieldId')
944
+ .argument('<fieldId>', '字段 fieldId(被带出的目标字段,如 联系方式)')
718
945
  .option('--title <title>', '修改字段标题')
719
946
  .option('--multiple', '设为多选(Employee/Department/Browser isSingle=false;Select→SelectMultiple)')
720
947
  .option('--single', '设为单选(反向)')
948
+ .option('--relate-from <关联字段>', 'EB关联带出:选 EBuilder 关联记录(如关联客户)后自动带出本字段值。传关联字段名/fieldId')
949
+ .option('--relate-from-source <源字段>', '目标表单的源字段(如客户信息表的联系方式)fieldId 或字段名,--relate-from 必填。传字段名需 --sform-id 解析')
950
+ .option('--sform-id <objId>', 'EBuilder 关联字段的目标表单 objId(流程表单 getConfig/getFormField 不返回 sformId;source 传字段名解析用)')
721
951
  .action(async (formId, fieldId, o) => {
722
952
  try {
723
953
  const opts = program.opts();
@@ -753,8 +983,58 @@ export function registerWorkflowFormCommands(program) {
753
983
  throw new Error(`字段类型 ${t} 不支持多选切换(仅 Employee/Department/Browser/Select)`);
754
984
  }
755
985
  }
986
+ // EB关联带出(ffRelateField):选关联记录后自动带出目标表字段值
987
+ if (o.relateFrom) {
988
+ if (!o.relateFromSource)
989
+ throw new Error('--relate-from 需配合 --relate-from-source <源字段名/fieldId>');
990
+ const { configs } = await readConfigs(client, baseUrl, formId);
991
+ const parseLayout = (f) => { try {
992
+ return typeof f.layoutDetail === 'string' ? JSON.parse(f.layoutDetail) : (f.layoutDetail || {});
993
+ }
994
+ catch {
995
+ return {};
996
+ } };
997
+ // 找 EBuilder 关联字段(关联客户)→ fieldId + sformId(目标表 objId)
998
+ const ebField = configs.find((c) => {
999
+ const ld = parseLayout(c);
1000
+ const ck = c.componentKey || ld.componentKey || '';
1001
+ return /ebuilder|eb|flow/i.test(ck) && (c.name === o.relateFrom || String(c.id) === o.relateFrom || ld.dataKey === o.relateFrom);
1002
+ });
1003
+ if (!ebField)
1004
+ throw new Error(`未找到 EBuilder 关联字段 "${o.relateFrom}"`);
1005
+ const ebLd = parseLayout(ebField);
1006
+ // 目标表 objId:流程表单的 EBuilder 字段配置不返回 sformId(getConfig/getFormField 都不返),
1007
+ // 所以需 --sform-id 显式传(与 ebuilder-scope 同款限制);config 里有则优先用。
1008
+ const sformId = o.sformId || ebField.sformId || ebLd.sformId || ebLd.virtualFormId || '';
1009
+ // 从目标表查源字段(联系方式)→ fieldId + fieldName;source 传的是 fieldId 则免查直接可用
1010
+ let srcFieldId = o.relateFromSource;
1011
+ let srcTitle = o.relateFromSource;
1012
+ const isId = /^\d+$/.test(String(o.relateFromSource));
1013
+ if (!isId || sformId) {
1014
+ try {
1015
+ const tfd = await client.post(`${baseUrl}/api/ebuilder/form/dataset/v1/getFields`, { objId: sformId, userid: auth.userId, tenantKey: auth.tenantKey, isReturnDetail: 'y' });
1016
+ const tf = [...(tfd?.data?.mainFields || []), ...(tfd?.data?.detailFields || []).flatMap((d) => d.fields || [])];
1017
+ const sf = tf.find((f) => f.fieldName === o.relateFromSource || String(f.fieldId) === o.relateFromSource);
1018
+ if (sf) {
1019
+ srcFieldId = String(sf.fieldId);
1020
+ srcTitle = sf.fieldName || srcFieldId;
1021
+ }
1022
+ }
1023
+ catch { /* 留原值兜底 */ }
1024
+ }
1025
+ if (!/^\d+$/.test(String(srcFieldId))) {
1026
+ throw new Error(`源字段 "${o.relateFromSource}" 解析不到 fieldId(需 --sform-id <目标表单objId> 或直接传 fieldId)`);
1027
+ }
1028
+ if (!sformId)
1029
+ console.error(` 警告: 未提供 --sform-id,源字段按 fieldId 直传(title 用原值)`);
1030
+ jsonObj.ffRelateField = {
1031
+ component: { key: jsonObj.type || 'Text' },
1032
+ isField: true, title: srcTitle, key: srcFieldId, isMultiple: false, fieldId: String(ebField.id),
1033
+ };
1034
+ changes.push(`relate-from=${o.relateFrom}→${srcTitle}(${srcFieldId})`);
1035
+ }
756
1036
  if (changes.length === 0)
757
- throw new Error('未指定要修改的属性(--title / --multiple / --single)');
1037
+ throw new Error('未指定要修改的属性(--title / --multiple / --single / --relate-from)');
758
1038
  const r = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFormFieldNew`, { formId, module: MODULE, customParam: {}, jsonObj: JSON.stringify(jsonObj) }, ebb);
759
1039
  if (r?.code !== 200 || !r?.status)
760
1040
  throw new Error(`编辑失败: ${r?.msg || '未知错误'}`);
@@ -840,6 +1120,127 @@ export function registerWorkflowFormCommands(program) {
840
1120
  process.exitCode = 1;
841
1121
  }
842
1122
  });
1123
+ // ---------- wf-form ebuilder-scope(关联浏览数据范围)----------
1124
+ // 流程表单 EBuilder 关联字段的浏览数据范围(如:关联客户时只选客户状态=启用的)。
1125
+ // 抓包(mulinquan 2026-08-25):businessModule="ebuildercard"(非 eb 表的 ebuilderform),
1126
+ // businessId=workflowId(非 formId),formId=流程表单 formId,controlId=EBuilder 字段 fieldId,
1127
+ // browserType="ebuilder/form_ebuilder",scopeType="scopeSql",scopeInfoRender=条件树 JSON 字符串。
1128
+ wfForm.command('ebuilder-scope')
1129
+ .description('关联浏览数据范围(如关联客户只选客户状态=启用)。--rule "目标表字段:操作:值"')
1130
+ .argument('<formId>', '流程表单 formId')
1131
+ .argument('<ebuilderFieldId>', 'EBuilder 关联字段 fieldId(controlId)')
1132
+ .requiredOption('--workflow-id <wfId>', '工作流程 ID(businessId)')
1133
+ .requiredOption('--sform-id <objId>', '目标表单 objId(EBuilder 关联的目标表,建字段时传的 --sform-id)')
1134
+ .requiredOption('--rule <rule>', '筛选规则:目标表字段名:操作:值。操作 eq/neq/like/isnull/notnull。Select 值用选项名(如 客户状态:eq:启用)')
1135
+ .action(async (formId, ebuilderFieldId, cmdOpts) => {
1136
+ try {
1137
+ const opts = program.opts();
1138
+ const auth = await AuthSession.fromOptions(opts);
1139
+ if (!auth)
1140
+ throw new Error('无法加载认证');
1141
+ const client = new ApiClient(auth);
1142
+ const baseUrl = auth.getBaseUrl();
1143
+ const sformId = cmdOpts.sformId;
1144
+ const ebb = { ebbusinessid: formId };
1145
+ // 1. 解析 rule:目标表字段名:操作:值
1146
+ const [tgtFieldName, func, rawVal] = cmdOpts.rule.split(':');
1147
+ if (!tgtFieldName || !func)
1148
+ throw new Error('--rule 格式:字段名:操作:值(如 客户状态:eq:启用)');
1149
+ // 2. 查目标表字段 → fieldId + fieldType + options(Select 解析选项 selectionId)
1150
+ const tfd = await client.post(`${baseUrl}/api/ebuilder/form/dataset/v1/getFields`, { objId: sformId, userid: auth.userId, tenantKey: auth.tenantKey, isReturnDetail: 'y' });
1151
+ const tgtFields = [...(tfd?.data?.mainFields || []), ...(tfd?.data?.detailFields || []).flatMap((d) => d.fields || [])];
1152
+ const tf = tgtFields.find((f) => f.fieldName === tgtFieldName || String(f.fieldId) === tgtFieldName);
1153
+ if (!tf)
1154
+ throw new Error(`目标表 ${sformId} 未找到字段 "${tgtFieldName}"`);
1155
+ const tgtFieldId = String(tf.fieldId);
1156
+ const tgtFieldType = tf.fieldType || tf.componentKey || 'Text';
1157
+ const tgtTitle = tf.fieldName || tgtFieldName;
1158
+ // 目标表 formId:getFormField 拿选项需 formId(不是 objId)。从 EBuilder 字段配置读 appId → getList 解析
1159
+ let tgtAppId = '';
1160
+ try {
1161
+ const ef = await client.post(`${baseUrl}${ROUTE}/field/manage/getFormField`, { module: MODULE, formTableField: { formField: { id: ebuilderFieldId, form: { id: formId } } } }, ebb);
1162
+ tgtAppId = ef?.data?.appId || '';
1163
+ }
1164
+ catch { /* 无则跳过 */ }
1165
+ let tgtFormId = sformId;
1166
+ if (tgtAppId) {
1167
+ try {
1168
+ const fl = await client.get(`${baseUrl}/api/bs/ebuilder/form/obj/getList/${tgtAppId}?apid=${tgtAppId}`, { ebbusinessid: tgtAppId });
1169
+ const fi = (fl?.data || []).find((f) => String(f.id) === String(sformId));
1170
+ if (fi?.formId)
1171
+ tgtFormId = String(fi.formId);
1172
+ }
1173
+ catch { /* 保持 sformId 兜底 */ }
1174
+ }
1175
+ // 3. 解析值:Select 用选项名(浏览器抓包:value=选项名 + config.options 名→id 映射,selectValue.id=selectionId);
1176
+ // 文本用字面值。businessModule/module 必须是 'workflow'(之前用 ebuildercard 平台不认)。
1177
+ let selectValue = null;
1178
+ let valueStr = rawVal || '';
1179
+ let configOptions = [];
1180
+ if (/Select|RadioBox|CheckBox/i.test(tgtFieldType) && rawVal) {
1181
+ const gfOpts = await client.post(`${baseUrl}/api/ebuilder/form/form/field/manage/getFormField`, { module: 'ebuilderform', formTableField: { formField: { id: tgtFieldId, form: { id: tgtFormId } } } }, { ebbusinessid: tgtFormId });
1182
+ const fopts = Array.isArray(gfOpts?.data?.options) ? gfOpts.data.options : [];
1183
+ configOptions = fopts.map((o) => ({ name: o.name, id: String(o.selectionId || o.valueKey || o.id) }));
1184
+ const opt = fopts.find((o) => o.name === rawVal || o.valueKey === rawVal || String(o.selectionId || o.id) === rawVal);
1185
+ if (opt) {
1186
+ const selId = String(opt.selectionId || opt.valueKey || opt.id);
1187
+ selectValue = { id: selId, content: opt.name, type: 'SELECTION_OPTION' };
1188
+ valueStr = opt.name; // 存选项名,平台按 config.options 映射回 selectionId
1189
+ }
1190
+ }
1191
+ // 4. 构建 scopeInfoRender 条件树(对齐浏览器抓包:conditions.type/text、config.options)
1192
+ const rootKey = String(Date.now());
1193
+ const nodeKey = String(Date.now() + 1);
1194
+ const tree = {
1195
+ key: rootKey, relationship: '1', isParent: true, isNewFilter: true, parentNode: '',
1196
+ datas: [{
1197
+ isParent: false, parentNode: rootKey,
1198
+ id: `field_${tgtFieldId}`, fieldName: `field_${tgtFieldId}`,
1199
+ fieldType: tgtFieldType, text: tgtTitle, compType: tgtFieldType, type: 'condition',
1200
+ conditions: {
1201
+ func, value: valueStr, type: '', text: '',
1202
+ ...(selectValue ? { selectValue, customBrowserValue: [selectValue] } : {}),
1203
+ customValue: '',
1204
+ },
1205
+ config: configOptions.length ? { name: tgtTitle, options: configOptions, title: tgtTitle } : { name: tgtTitle, title: tgtTitle },
1206
+ subTableId: '', key: nodeKey,
1207
+ }],
1208
+ version: 1, showFilterIsNull: false,
1209
+ };
1210
+ // 5. batchSave(module/businessModule 用 workflow,非 ebuildercard)。
1211
+ // 更新须带现有 scope 记录 id(浏览器抓包实测:selectPage 读 → 拿 id → batchSave 带 id 更新;
1212
+ // 不带 id 会新建/覆盖不对,平台读不到)。selectPage 读接口 POST /api/bcw/browserDataScope/selectPage。
1213
+ let scopeId = '';
1214
+ try {
1215
+ const rd = await client.post(`${baseUrl}/api/bcw/browserDataScope/selectPage`, {
1216
+ businessId: cmdOpts.workflowId, businessModule: 'workflow',
1217
+ controlId: ebuilderFieldId, browserType: 'ebuilder/form_ebuilder',
1218
+ formId, module: 'workflow',
1219
+ });
1220
+ const rec = rd?.data?.records?.[0] || rd?.data?.data?.[0] || (Array.isArray(rd?.data) ? rd.data[0] : undefined);
1221
+ if (rec?.id)
1222
+ scopeId = String(rec.id);
1223
+ }
1224
+ catch { /* 读失败按新建处理 */ }
1225
+ const payload = [{
1226
+ businessId: cmdOpts.workflowId, businessModule: 'workflow',
1227
+ controlId: ebuilderFieldId, browserType: 'ebuilder/form_ebuilder',
1228
+ formId, module: 'workflow',
1229
+ scopeInfoRender: JSON.stringify(tree), scopeType: 'scopeSql',
1230
+ ...(scopeId ? { id: scopeId } : {}),
1231
+ }];
1232
+ const r = await client.post(`${baseUrl}/api/bcw/browserDataScope/batchSave`, payload);
1233
+ if (r?.code !== 200 && !r?.status)
1234
+ throw new Error(`保存失败: ${r?.msg || JSON.stringify(r).slice(0, 200)}`);
1235
+ console.log(opts.json
1236
+ ? JSON.stringify({ success: true, formId, ebuilderFieldId, workflowId: cmdOpts.workflowId, rule: cmdOpts.rule }, null, 2)
1237
+ : `关联浏览范围已设置: ${tgtTitle} ${func} '${rawVal}'(EBuilder 字段 ${ebuilderFieldId})`);
1238
+ }
1239
+ catch (e) {
1240
+ console.error(`设置关联浏览范围失败: ${e.message}`);
1241
+ process.exitCode = 1;
1242
+ }
1243
+ });
843
1244
  // ---------- wf-form add-detail-table ----------
844
1245
  wfForm.command('add-detail-table')
845
1246
  .description('给流程表单加明细子表(saveSubForm 建子表 + 加首字段使其可见;--init 放入布局)')
@@ -932,8 +1333,284 @@ export function registerWorkflowFormCommands(program) {
932
1333
  process.exitCode = 1;
933
1334
  }
934
1335
  });
1336
+ // 落单/孤字段占满行(对齐 eb 表 form-create.ts:两列排布时「奇数个的最后一个」+「被 singleRow
1337
+ // 挤落单的前一字段」自动 singleRow 独占整行,避免右列留白)。明细表恒 singleRow。
1338
+ // 排布对象仅业务主字段;系统字段与 Fold/Collapse 分组容器原样保留(分组内字段由平台容器排布)。
1339
+ function applyLoneSingleRows(configs) {
1340
+ const isGroup = (c) => c.componentKey === 'Fold' || c.type === 'Collapse';
1341
+ const isDetail = (c) => c.type === 'DetailTable' || c.componentKey === 'DataTable';
1342
+ const isBusiness = (c) => !c.system && !isGroup(c) && !isDetail(c);
1343
+ const mainFields = configs.filter(isBusiness);
1344
+ const details = configs.filter(isDetail);
1345
+ const others = configs.filter((c) => !isBusiness(c) && !isDetail(c));
1346
+ const mainIds = mainFields.map((c) => String(c.id));
1347
+ const detailIds = new Set(details.map((c) => String(c.id)));
1348
+ // 奇数个最后一个落单 → 独占一行
1349
+ const loneRowId = mainIds.length % 2 === 1 ? mainIds[mainIds.length - 1] : null;
1350
+ // 落单兜底:模拟两列流,singleRow 字段位于 col=1 时其前一字段(col0 未配对)也标 singleRow
1351
+ const orphanIds = new Set();
1352
+ let col = 0;
1353
+ let prevId = '';
1354
+ for (const id of [...mainIds, ...details.map((c) => String(c.id))]) {
1355
+ const c = mainFields.find((x) => String(x.id) === id) || details.find((x) => String(x.id) === id);
1356
+ const isSingle = c?.singleRow === true || id === loneRowId || detailIds.has(id);
1357
+ if (isSingle) {
1358
+ if (col === 1 && prevId)
1359
+ orphanIds.add(prevId);
1360
+ col = 0;
1361
+ }
1362
+ else {
1363
+ col = col === 0 ? 1 : 0;
1364
+ }
1365
+ prevId = id;
1366
+ }
1367
+ const biz = mainFields.map((c) => ({
1368
+ ...c,
1369
+ singleRow: String(c.id) === loneRowId || c.singleRow === true || orphanIds.has(String(c.id)),
1370
+ }));
1371
+ const det = details.map((d) => ({ ...d, singleRow: true }));
1372
+ return [...others, ...biz, ...det];
1373
+ }
1374
+ // 读表单现有字段分组 → refFieldIds(initLayout/getConfig 的 groupFieldIds)。
1375
+ // getFieldGroupPage 返回全部主表字段,字段 formField.group 带 groupId+groupName;
1376
+ // refFieldId = groupId - 1(平台先分配 layout config 再分配 formFieldGroup,顺序相连)。
1377
+ // 用于 field-prop / layout init 重跑 initLayout 时保留 Collapse 折叠分组。
1378
+ async function fetchFormGroups(client, baseUrl, formId) {
1379
+ try {
1380
+ const r = await client.post(`${baseUrl}${ROUTE}/field/manage/getFieldGroupPage`, { pageNo: 1, pageSize: 100, module: MODULE, formFieldSearchEntity: { formId }, customParam: {} }, { ebbusinessid: formId });
1381
+ const list = r?.data?.pageDatas?.result || [];
1382
+ const seen = new Set();
1383
+ for (const f of list) {
1384
+ const g = f?.formField?.group;
1385
+ if (g?.id)
1386
+ seen.add(String(g.id));
1387
+ }
1388
+ return [...seen].map((id) => String(BigInt(id) - 1n));
1389
+ }
1390
+ catch {
1391
+ return [];
1392
+ }
1393
+ }
1394
+ // ---------- wf-form group(字段分组,对齐 eb 表)----------
1395
+ // 流程表单字段分组(基本信息/系统设置等折叠容器)。三步接口(module=workflow,实测可用):
1396
+ // ① saveFieldGroup 建分组 → data.group.id(formFieldGroup记录id) + refFieldId(layout Collapse id)
1397
+ // ② updateFormFieldGroup 分配字段(fieldList)
1398
+ // ③ initLayout 布局:configs 包 Collapse{id:refFieldId, componentKey:"Fold", type:"Collapse",
1399
+ // dataKey:"f_"+refFieldId, children:[分组字段]} + 顶层 groupFieldIds:[refFieldId]
1400
+ // (对齐 eb 表 form-create.ts FLOW 分组结构 / 记忆 field-group-saveFieldGroup-api)
1401
+ wfForm.command('group')
1402
+ .description('字段分组(建分组/分配字段/布局 Collapse 折叠,对齐 eb 表)。一次建全部分组')
1403
+ .argument('<formId>', '流程表单 formId')
1404
+ .requiredOption('--groups <json>', '分组定义 JSON:{"分组名":["字段名或ID",...]},可 @文件路径')
1405
+ .action(async (formId, o) => {
1406
+ try {
1407
+ const opts = program.opts();
1408
+ const auth = await AuthSession.fromOptions(opts);
1409
+ if (!auth)
1410
+ throw new Error('无法加载认证');
1411
+ const client = new ApiClient(auth);
1412
+ const baseUrl = auth.getBaseUrl();
1413
+ const ebb = { ebbusinessid: formId };
1414
+ let groupsJson = o.groups;
1415
+ if (groupsJson.startsWith('@')) {
1416
+ const { readFileSync, existsSync } = await import('fs');
1417
+ const p = groupsJson.slice(1);
1418
+ if (!existsSync(p))
1419
+ throw new Error(`文件不存在: ${p}`);
1420
+ groupsJson = readFileSync(p, 'utf-8');
1421
+ }
1422
+ let groups;
1423
+ try {
1424
+ groups = JSON.parse(groupsJson);
1425
+ }
1426
+ catch {
1427
+ throw new Error('--groups JSON 解析失败');
1428
+ }
1429
+ const entries = Object.entries(groups || {});
1430
+ if (entries.length === 0)
1431
+ throw new Error('--groups 为空');
1432
+ // 名字 → 字段列表,无字段的分组也允许(空分组占位,后续 assign)
1433
+ // 解析字段(主表字段名→fieldId;含明细字段名但分组只建议主表)
1434
+ const { configs } = await readConfigs(client, baseUrl, formId);
1435
+ const fieldByName = new Map();
1436
+ const walk = (nodes) => { for (const c of nodes) {
1437
+ if (c.name)
1438
+ fieldByName.set(String(c.name), c);
1439
+ if (String(c.id))
1440
+ fieldByName.set(String(c.id), c);
1441
+ if (c.children?.length)
1442
+ walk(c.children);
1443
+ } };
1444
+ walk(configs);
1445
+ const resolveField = (ref) => fieldByName.get(String(ref)) || fieldByName.get(String(ref)) || null;
1446
+ // ① 建全部分组(saveFieldGroup)
1447
+ const created = [];
1448
+ for (const [gname, fieldRefs] of entries) {
1449
+ const sr = await client.post(`${baseUrl}${ROUTE}/field/manage/saveFieldGroup`, { module: MODULE, formFieldGroup: { groupName: gname, formId }, customParam: {} }, ebb);
1450
+ if (sr?.code !== 200 || !sr?.status)
1451
+ throw new Error(`建分组「${gname}」失败: ${sr?.msg || '未知错误'}`);
1452
+ const g = sr?.data?.group || {};
1453
+ const fieldIds = [];
1454
+ const missing = [];
1455
+ for (const ref of (fieldRefs || [])) {
1456
+ const f = resolveField(ref);
1457
+ if (f)
1458
+ fieldIds.push(String(f.id));
1459
+ else
1460
+ missing.push(String(ref));
1461
+ }
1462
+ if (missing.length)
1463
+ console.error(` ⚠ 分组「${gname}」未找到字段: ${missing.join(', ')}`);
1464
+ created.push({ name: gname, groupId: String(g.id || ''), refFieldId: String(g.refFieldId || ''), fieldIds });
1465
+ console.error(` 分组已建: ${gname} groupId=${g.id} refFieldId=${g.refFieldId}${fieldIds.length ? ` 字段×${fieldIds.length}` : '(空分组)'}`);
1466
+ }
1467
+ // ② 分配字段(updateFormFieldGroup)
1468
+ for (const grp of created) {
1469
+ if (!grp.fieldIds.length)
1470
+ continue;
1471
+ const ur = await client.post(`${baseUrl}${ROUTE}/field/manage/updateFormFieldGroup`, { module: MODULE, formFieldGroup: { id: grp.groupId }, form: { id: formId }, fieldList: grp.fieldIds.map((id) => ({ id })), customParam: {} }, ebb);
1472
+ if (ur?.code !== 200 || !ur?.status)
1473
+ console.error(` ⚠ 分组「${grp.name}」分配字段失败: ${ur?.msg || '未知错误'}`);
1474
+ else
1475
+ console.error(` 已分配: ${grp.name} ← ${grp.fieldIds.length} 字段`);
1476
+ }
1477
+ // ③ 布局:构建 Collapse configs + groupFieldIds → initLayout
1478
+ const meta = await getLayoutMeta(client, baseUrl, formId);
1479
+ if (!meta.layoutId)
1480
+ throw new Error('无法获取 layoutId');
1481
+ const cfgRes = await client.post(`${baseUrl}${ROUTE}/view/relate/getConfig`, {
1482
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
1483
+ customParam: { terminal: '', objId: formId }, initMode: 5, groupFieldIds: [],
1484
+ designerParam: { sourceModule: MODULE },
1485
+ }, ebb);
1486
+ if (cfgRes?.code !== 200 || !cfgRes?.status)
1487
+ throw new Error(`getConfig 失败: ${cfgRes?.msg}`);
1488
+ const allConfigs = JSON.parse(JSON.stringify(cfgRes?.data?.configs || []));
1489
+ // 合并运行时已保存的 singleRow(getConfig 不带,防 initLayout 冲掉独占一行)
1490
+ const allSrSet = await fetchRuntimeSingleRows(client, baseUrl, formId);
1491
+ mergeRuntimeSingleRow(allConfigs, allSrSet);
1492
+ // 找出被分组的主表字段 config(顶层非系统非明细),从平铺抽出包 Collapse
1493
+ const groupFieldIdSet = new Set(created.flatMap((g) => g.fieldIds));
1494
+ const collapsedFields = allConfigs.filter((c) => !c.system && c.type !== 'DetailTable' && c.componentKey !== 'DataTable' && groupFieldIdSet.has(String(c.id)));
1495
+ const rest = allConfigs.filter((c) => collapsedFields.indexOf(c) === -1);
1496
+ const groupFieldIds = [];
1497
+ const collapses = [];
1498
+ for (const grp of created) {
1499
+ const children = collapsedFields.filter((c) => grp.fieldIds.includes(String(c.id)));
1500
+ groupFieldIds.push(grp.refFieldId);
1501
+ collapses.push({
1502
+ id: grp.refFieldId, name: grp.name, componentKey: 'Fold', display: true, show: true,
1503
+ readOnly: true, required: false, disableEdit: false, singleRow: false, isDelete: false,
1504
+ type: 'Collapse', typeDescribe: '分组', dataKey: `f_${grp.refFieldId}`,
1505
+ children,
1506
+ });
1507
+ }
1508
+ // 落单/孤字段占满行(含分组内字段 singleRow 由平台 Collapse 自行排布,只对未分组字段算)
1509
+ const finalConfigs = applyLoneSingleRows([...collapses, ...rest]);
1510
+ const styleTemplateId = meta.layoutType === 'FLOW' ? '820000000000000001' : '700000000000000008';
1511
+ const initRes = await client.post(`${baseUrl}${ROUTE}/view/relate/initLayout`, {
1512
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
1513
+ generateCollapse: true, styleTemplateId, styleTemplateType: 1,
1514
+ configs: finalConfigs, addLayout: false, ecodeType: ['js', 'css'], initMode: '1',
1515
+ groupFieldIds,
1516
+ customParam: { terminal: '', objId: formId },
1517
+ formLayoutMultiple: { isSys: true, id: meta.layoutMultiId, name: meta.layoutName, terminalType: 'PC', layoutType: meta.layoutType },
1518
+ designerParam: { onlyLayout: false },
1519
+ }, ebb);
1520
+ if (initRes?.code !== 200 || !initRes?.status)
1521
+ throw new Error(`initLayout 失败: ${initRes?.msg}`);
1522
+ const comps = initRes?.data?.pageLayouts?.[0]?.comps || initRes?.data?.formLayout?.pageLayouts?.[0]?.comps || [];
1523
+ console.log(opts.json
1524
+ ? JSON.stringify({ success: true, formId, groups: created.map((g) => ({ name: g.name, groupId: g.groupId, refFieldId: g.refFieldId, fieldCount: g.fieldIds.length })), comps: comps.length }, null, 2)
1525
+ : `字段分组完成: ${created.length} 个分组,布局 ${comps.length} 组件`);
1526
+ }
1527
+ catch (e) {
1528
+ console.error(`字段分组失败: ${e.message}`);
1529
+ process.exitCode = 1;
1530
+ }
1531
+ });
935
1532
  // ---------- wf-form layout init ----------
936
1533
  const layoutCmd = wfForm.command('layout').description('布局管理');
1534
+ // ---------- wf-form layout add(添加布局)----------
1535
+ // 给流程表单新增一套布局(formLayoutMultiple 多布局,PC/移动端多套并存不覆盖)。
1536
+ // 抓包(mulinquan 2026-08-25):POST /api/workflow/core/form/layout/save
1537
+ // body: { module:'workflow', customParam:{workflowId, isNodeLayout:true},
1538
+ // formLayoutMultiple:{terminalType:'PC', name, labelObj:'{}', describe, layoutType:'FLOW'|'EXCEL'},
1539
+ // local:true, copyConfig:{formDataLinkage:true, excelLinkage:true}, formId }
1540
+ // → data.formLayoutMultiple.id = 新布局 layoutId。FLOW=流式 EXCEL=表格式(实测两种都成功)。
1541
+ layoutCmd.command('add')
1542
+ .description('添加一套新布局(layout/save):FLOW 流式 / EXCEL 表格式,PC/移动端多套并存不覆盖。返回新布局 ID')
1543
+ .argument('<formId>', '流程表单 formId')
1544
+ .requiredOption('--name <name>', '布局名称(如 "项目立项流程PC1")')
1545
+ .option('--layout-type <type>', '布局类型:FLOW 流式(默认) / EXCEL 表格式', 'FLOW')
1546
+ .option('--terminal-type <t>', '终端类型:PC(默认) / MOBILE', 'PC')
1547
+ .option('--describe <text>', '布局描述', '')
1548
+ .option('--workflow-id <id>', '工作流 ID(customParam.workflowId;不传用 formId 兜底,实测后端接受)')
1549
+ .action(async (formId, o) => {
1550
+ try {
1551
+ const opts = program.opts();
1552
+ const auth = await AuthSession.fromOptions(opts);
1553
+ if (!auth)
1554
+ throw new Error('无法加载认证');
1555
+ const client = new ApiClient(auth);
1556
+ const baseUrl = auth.getBaseUrl();
1557
+ const ebb = { ebbusinessid: formId };
1558
+ const layoutType = String(o.layoutType || 'FLOW').toUpperCase();
1559
+ if (!['FLOW', 'EXCEL'].includes(layoutType))
1560
+ throw new Error(`布局类型仅支持 FLOW(流式)/EXCEL(表格式),收到: ${layoutType}`);
1561
+ const body = {
1562
+ module: MODULE,
1563
+ customParam: { workflowId: o.workflowId || formId, isNodeLayout: true },
1564
+ formLayoutMultiple: { terminalType: o.terminalType || 'PC', name: o.name, labelObj: '{}', describe: o.describe || '', layoutType },
1565
+ local: true,
1566
+ copyConfig: { formDataLinkage: true, excelLinkage: true },
1567
+ formId,
1568
+ };
1569
+ const r = await client.post(`${baseUrl}${ROUTE}/layout/save`, body, ebb);
1570
+ if (r?.code !== 200 || !r?.status)
1571
+ throw new Error(`添加布局失败: ${r?.msg || '未知错误'}`);
1572
+ const flm = r?.data?.formLayoutMultiple || {};
1573
+ console.log(opts.json
1574
+ ? JSON.stringify({ success: true, formId, layoutId: flm.id, layoutType, terminalType: o.terminalType || 'PC', name: o.name, versionId: flm.versionId, isDefault: flm.isDefault }, null, 2)
1575
+ : `布局已添加: ${o.name}(${layoutType === 'FLOW' ? '流式' : '表格式'})layoutId=${flm.id}${flm.isDefault ? ' 已设为默认' : ''}`);
1576
+ }
1577
+ catch (e) {
1578
+ console.error(`添加布局失败: ${e.message}`);
1579
+ process.exitCode = 1;
1580
+ }
1581
+ });
1582
+ // ---------- wf-form layout delete(删除布局)----------
1583
+ // 抓包(mulinquan 2026-08-25):POST /api/bs/workflow/pathdef/form/layout/emptyNodeLayout
1584
+ // body: { layoutId, workflowId, terminal:'PC' } → data=true,**效果=从布局列表删除该布局**
1585
+ // (用户确认:emptyNodeLayout 实际就是把布局删掉,不是"保留布局清空节点")。
1586
+ layoutCmd.command('delete')
1587
+ .description('删除一套布局(emptyNodeLayout:从布局列表移除)。传 layoutId(layout add 返回的 formLayoutMultiple.id)')
1588
+ .argument('<formId>', '流程表单 formId')
1589
+ .argument('<layoutId>', '要删除的布局 ID(wf-form layout add 返回的 layoutId)')
1590
+ .option('--workflow-id <id>', '工作流 ID(不传用 formId 兜底)')
1591
+ .option('--terminal <t>', '终端类型:PC(默认) / MOBILE', 'PC')
1592
+ .action(async (formId, layoutId, o) => {
1593
+ try {
1594
+ const opts = program.opts();
1595
+ const auth = await AuthSession.fromOptions(opts);
1596
+ if (!auth)
1597
+ throw new Error('无法加载认证');
1598
+ const client = new ApiClient(auth);
1599
+ const baseUrl = auth.getBaseUrl();
1600
+ const r = await client.post(`${baseUrl}/api/bs/workflow/pathdef/form/layout/emptyNodeLayout`, {
1601
+ layoutId, workflowId: o.workflowId || formId, terminal: o.terminal || 'PC',
1602
+ });
1603
+ if (r?.code !== 200 || !r?.status)
1604
+ throw new Error(`删除布局失败: ${r?.msg || '未知错误'}`);
1605
+ console.log(opts.json
1606
+ ? JSON.stringify({ success: true, formId, layoutId, terminal: o.terminal || 'PC' }, null, 2)
1607
+ : `布局已删除: ${layoutId}(${o.terminal || 'PC'})`);
1608
+ }
1609
+ catch (e) {
1610
+ console.error(`删除布局失败: ${e.message}`);
1611
+ process.exitCode = 1;
1612
+ }
1613
+ });
937
1614
  layoutCmd.command('init')
938
1615
  .description('初始化/重建流程表单布局(initLayout):按 layoutType 选模板(FLOW 默认)+ 设字段 readOnly/required/singleRow')
939
1616
  .argument('<formId>', '流程表单 formId')
@@ -954,15 +1631,17 @@ export function registerWorkflowFormCommands(program) {
954
1631
  const meta = await getLayoutMeta(client, baseUrl, formId);
955
1632
  if (!meta.layoutId)
956
1633
  throw new Error('无法获取 layoutId');
1634
+ // 保留字段分组:getConfig/initLayout 带 groupFieldIds,否则 Collapse 折叠被冲掉
1635
+ const groupIds = await fetchFormGroups(client, baseUrl, formId);
957
1636
  const cfgRes = await client.post(`${baseUrl}${ROUTE}/view/relate/getConfig`, {
958
1637
  formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
959
- customParam: { terminal: '', objId: formId }, initMode: 5, groupFieldIds: [],
1638
+ customParam: { terminal: '', objId: formId }, initMode: 5, groupFieldIds: groupIds,
960
1639
  designerParam: { sourceModule: MODULE },
961
1640
  }, ebb);
962
1641
  // 流程表单 FLOW 布局 getConfig 默认把业务字段 readOnly=true(锁死),init 时强制可编辑:
963
- // 业务字段 readOnly=false / disableEdit=false(除非 --read-only 显式锁);明细/系统字段保留。
1642
+ // 业务字段 readOnly=false / disableEdit=false(除非 --read-only 显式锁);明细/系统字段/Collapse 分组容器保留。
964
1643
  const configs = normalizeDetailTableChildren((cfgRes?.data?.configs || []).map((c) => {
965
- const isBusiness = !c.system && c.componentKey !== 'DataTable' && c.type !== 'DetailTable';
1644
+ const isBusiness = !c.system && c.componentKey !== 'DataTable' && c.type !== 'DetailTable' && c.type !== 'Collapse' && c.componentKey !== 'Fold';
966
1645
  const out = { ...c, display: c.system ? c.display : true, show: c.system ? c.show : true };
967
1646
  if (isBusiness) {
968
1647
  out.readOnly = o.readOnly === true;
@@ -974,13 +1653,19 @@ export function registerWorkflowFormCommands(program) {
974
1653
  }
975
1654
  return out;
976
1655
  }));
1656
+ // 合并运行时已保存的 singleRow(getConfig 不带,防 initLayout 冲掉独占一行)
1657
+ const initSrSet = await fetchRuntimeSingleRows(client, baseUrl, formId);
1658
+ mergeRuntimeSingleRow(configs, initSrSet);
1659
+ // 落单/孤字段占满行(对齐 eb 表):奇数个最后一个 + 被 singleRow 挤落单的前一字段自动 singleRow
1660
+ const finalConfigs = applyLoneSingleRows(configs);
977
1661
  const styleTemplateId = o.styleTemplate
978
1662
  || (meta.layoutType === 'FLOW' ? '820000000000000001' : '700000000000000008');
979
1663
  const initRes = await client.post(`${baseUrl}${ROUTE}/view/relate/initLayout`, {
980
1664
  formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
981
1665
  generateCollapse: o.generateCollapse !== false,
982
1666
  styleTemplateId: /^\d+$/.test(styleTemplateId) ? styleTemplateId : styleTemplateId, styleTemplateType: 1,
983
- configs, addLayout: false, ecodeType: ['js', 'css'], initMode: '1',
1667
+ configs: finalConfigs, addLayout: false, ecodeType: ['js', 'css'], initMode: '1',
1668
+ groupFieldIds: groupIds,
984
1669
  customParam: { terminal: '', objId: formId },
985
1670
  formLayoutMultiple: { isSys: true, id: meta.layoutMultiId, name: meta.layoutName, terminalType: 'PC', layoutType: meta.layoutType },
986
1671
  designerParam: { onlyLayout: false },
@@ -997,6 +1682,103 @@ export function registerWorkflowFormCommands(program) {
997
1682
  process.exitCode = 1;
998
1683
  }
999
1684
  });
1685
+ // ---------- wf-form field-prop ----------
1686
+ // 流程表单字段属性(必填/只读/可编辑/独占一行)。
1687
+ // getConfig 顶层 configs 的 readOnly/required 只是字段设计视图(FLOW 模板默认全业务字段 readOnly=true);
1688
+ // 运行时布局看 initLayout 保存的 comps.config。initLayout 是唯一保存路径(saveConfig 实测 404)。
1689
+ // 因此:getConfig 读 configs → 未指定业务字段强制可编辑(防 FLOW 锁死)→ 按名/ID 改指定字段 → initLayout 重存。
1690
+ wfForm.command('field-prop')
1691
+ .description('设置流程表单字段属性:必填/只读/可编辑/独占一行(getConfig 读 → 改 → initLayout 保存到运行时布局)')
1692
+ .argument('<formId>', '流程表单 formId')
1693
+ .option('--required <fields...>', '设为必填(required=true)')
1694
+ .option('--no-required <fields...>', '取消必填(required=false)')
1695
+ .option('--readonly <fields...>', '设为只读(readOnly=true, disableEdit=true)')
1696
+ .option('--editable <fields...>', '设为可编辑(readOnly=false, disableEdit=false)')
1697
+ .option('--single-row <fields...>', '独占一行(singleRow=true)')
1698
+ .action(async (formId, o) => {
1699
+ try {
1700
+ const opts = program.opts();
1701
+ const auth = await AuthSession.fromOptions(opts);
1702
+ if (!auth)
1703
+ throw new Error('无法加载认证');
1704
+ const client = new ApiClient(auth);
1705
+ const baseUrl = auth.getBaseUrl();
1706
+ const ebb = { ebbusinessid: formId };
1707
+ const meta = await getLayoutMeta(client, baseUrl, formId);
1708
+ if (!meta.layoutId)
1709
+ throw new Error('无法获取 layoutId');
1710
+ // 保留字段分组:getConfig/initLayout 带 groupFieldIds,否则 Collapse 折叠被冲掉
1711
+ const groupIds = await fetchFormGroups(client, baseUrl, formId);
1712
+ // 读当前布局 configs(含明细 children + Collapse 分组)
1713
+ const cfgRes = await client.post(`${baseUrl}${ROUTE}/view/relate/getConfig`, {
1714
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
1715
+ customParam: { terminal: '', objId: formId }, initMode: 5, groupFieldIds: groupIds,
1716
+ designerParam: { sourceModule: MODULE },
1717
+ }, ebb);
1718
+ if (cfgRes?.code !== 200 || !cfgRes?.status)
1719
+ throw new Error(`getConfig 失败: ${cfgRes?.msg}`);
1720
+ // 深拷贝 + 明细子字段归一为可编辑/非必填(防 DetailTable 子字段被模板锁死)
1721
+ const configs = normalizeDetailTableChildren(JSON.parse(JSON.stringify(cfgRes?.data?.configs || [])));
1722
+ // FLOW 模板默认业务字段 readOnly=true(锁死)→ 未指定的业务字段显式可编辑;
1723
+ // Collapse 分组容器 / 系统字段 / 明细保留原样
1724
+ for (const c of configs) {
1725
+ if (!c.system && c.componentKey !== 'DataTable' && c.type !== 'DetailTable' && c.type !== 'Collapse' && c.componentKey !== 'Fold') {
1726
+ c.readOnly = false;
1727
+ c.disableEdit = false;
1728
+ }
1729
+ }
1730
+ // 合并运行时已保存的 singleRow(getConfig 不带,防 initLayout 冲掉独占一行)
1731
+ const srSet = await fetchRuntimeSingleRows(client, baseUrl, formId);
1732
+ mergeRuntimeSingleRow(configs, srSet);
1733
+ const apply = (refs, fn, label) => {
1734
+ if (!refs || refs.length === 0)
1735
+ return;
1736
+ const hit = [];
1737
+ const walk = (nodes) => {
1738
+ for (const c of nodes) {
1739
+ if (refs.some(r => String(c.id) === r || c.name === r || c.title === r))
1740
+ hit.push(c);
1741
+ if (c.children?.length)
1742
+ walk(c.children);
1743
+ }
1744
+ };
1745
+ walk(configs);
1746
+ for (const c of hit)
1747
+ fn(c);
1748
+ if (hit.length)
1749
+ console.error(` ${label}: ${hit.map((c) => c.name || c.title).join(', ')}`);
1750
+ else
1751
+ console.error(` ⚠ 未找到字段: ${refs.join(', ')}`);
1752
+ };
1753
+ apply(o.required, (c) => { c.required = true; }, '必填');
1754
+ apply(o.noRequired, (c) => { c.required = false; }, '取消必填');
1755
+ apply(o.readonly, (c) => { c.readOnly = true; c.disableEdit = true; }, '只读');
1756
+ apply(o.editable, (c) => { c.readOnly = false; c.disableEdit = false; }, '可编辑');
1757
+ apply(o.singleRow, (c) => { c.singleRow = true; }, '独占一行');
1758
+ // 落单/孤字段占满行(对齐 eb 表):奇数个最后一个 + 被 singleRow 挤落单的前一字段自动 singleRow
1759
+ const finalConfigs = applyLoneSingleRows(configs);
1760
+ const styleTemplateId = meta.layoutType === 'FLOW' ? '820000000000000001' : '700000000000000008';
1761
+ const initRes = await client.post(`${baseUrl}${ROUTE}/view/relate/initLayout`, {
1762
+ formId, module: MODULE, apiModule: 'workflow/form', layoutId: meta.layoutId,
1763
+ generateCollapse: true, styleTemplateId, styleTemplateType: 1,
1764
+ configs: finalConfigs, addLayout: false, ecodeType: ['js', 'css'], initMode: '1',
1765
+ groupFieldIds: groupIds,
1766
+ customParam: { terminal: '', objId: formId },
1767
+ formLayoutMultiple: { isSys: true, id: meta.layoutMultiId, name: meta.layoutName, terminalType: 'PC', layoutType: meta.layoutType },
1768
+ designerParam: { onlyLayout: false },
1769
+ }, ebb);
1770
+ if (initRes?.code !== 200 || !initRes?.status)
1771
+ throw new Error(`initLayout 失败: ${initRes?.msg}`);
1772
+ const comps = initRes?.data?.pageLayouts?.[0]?.comps || initRes?.data?.formLayout?.pageLayouts?.[0]?.comps || [];
1773
+ console.log(opts.json
1774
+ ? JSON.stringify({ success: true, formId, layoutType: meta.layoutType, comps: comps.length }, null, 2)
1775
+ : `字段属性已保存: 布局 ${comps.length} 组件`);
1776
+ }
1777
+ catch (e) {
1778
+ console.error(`设置字段属性失败: ${e.message}`);
1779
+ process.exitCode = 1;
1780
+ }
1781
+ });
1000
1782
  return wfForm;
1001
1783
  }
1002
1784
  //# sourceMappingURL=wf-form.js.map