lan-low-code-cli 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,541 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTemplateCommand = registerTemplateCommand;
4
+ const formDefine_1 = require("../api/formDefine");
5
+ const template_1 = require("../api/template");
6
+ const client_1 = require("../client");
7
+ const config_1 = require("../config");
8
+ const constants_1 = require("../constants");
9
+ const generator_1 = require("../generator");
10
+ const types_1 = require("../types");
11
+ const logger_1 = require("../utils/logger");
12
+ const widgets_1 = require("../widgets");
13
+ const form_1 = require("./form");
14
+ function createClient(opts) {
15
+ return new client_1.LcodeClient((0, config_1.resolveRuntime)({ baseUrl: opts.baseUrl, token: opts.token }));
16
+ }
17
+ /** 控件元数据注册表(站点根不变时复用磁盘缓存) */
18
+ function createRegistry(opts) {
19
+ const runtime = (0, config_1.resolveRuntime)({ baseUrl: opts.baseUrl, token: opts.token });
20
+ return new widgets_1.WidgetRegistry({ baseUrl: runtime.baseUrl, token: runtime.token });
21
+ }
22
+ /** commander 可重复选项收集器 */
23
+ function collect(value, previous) {
24
+ return previous.concat([value]);
25
+ }
26
+ /** 生成 34 位字母数字 id(与平台 fdId 形态一致) */
27
+ function genId() {
28
+ const ts = Date.now().toString(36);
29
+ let rand = '';
30
+ while (rand.length < 26)
31
+ rand += Math.random().toString(36).slice(2);
32
+ return `1k6g${ts}${rand}`.slice(0, 34);
33
+ }
34
+ function deepClone(value) {
35
+ return JSON.parse(JSON.stringify(value));
36
+ }
37
+ function toList(result) {
38
+ if (Array.isArray(result))
39
+ return result;
40
+ const content = result?.content;
41
+ return Array.isArray(content) ? content : [];
42
+ }
43
+ /** 把源表单的业务字段复制到模板模型(重生成字段 id,改归属模型) */
44
+ function cloneFields(srcMain, tplMain, keepNames) {
45
+ const keep = keepNames && keepNames.length ? new Set(keepNames) : undefined;
46
+ return (srcMain.fdFields || [])
47
+ .filter((f) => !f.fdIsSystem)
48
+ .filter((f) => !keep || keep.has(f.fdName))
49
+ .map((f) => ({
50
+ ...deepClone(f),
51
+ fdId: genId(),
52
+ fdDataModel: { fdId: tplMain.fdId, fdName: tplMain.fdName },
53
+ }));
54
+ }
55
+ /**
56
+ * 克隆源表单视图:替换表名、剔除明细节点与未保留字段。
57
+ * 返回可直接写入模板 fdConfig 的 viewModel 数组。
58
+ */
59
+ function cloneViewModel(srcCfg, options) {
60
+ const source = deepClone((srcCfg.viewModel || [])[0] || null);
61
+ if (!source)
62
+ return [];
63
+ const dropFields = new Set(options.dropFieldNames || []);
64
+ const viewCfg = typeof source.fdConfig === 'string' ? JSON.parse(source.fdConfig) : source.fdConfig;
65
+ const desktop = viewCfg?.view?.render?.desktop;
66
+ if (!Array.isArray(desktop))
67
+ return [];
68
+ const clean = (node) => {
69
+ if (!node)
70
+ return null;
71
+ if (options.detailTable && node.key === options.detailTable)
72
+ return null;
73
+ if (dropFields.has(String(node.key)))
74
+ return null;
75
+ const props = node.controlProps;
76
+ if (props) {
77
+ if (props.$$tableName === options.srcTable)
78
+ props.$$tableName = options.tplTable;
79
+ if (props.tableName === options.srcTable)
80
+ props.tableName = options.tplTable;
81
+ }
82
+ if (Array.isArray(node.children)) {
83
+ node.children = node.children.map(clean).filter(Boolean);
84
+ }
85
+ return node;
86
+ };
87
+ viewCfg.view.render.desktop = desktop.map(clean).filter(Boolean);
88
+ source.fdConfig = typeof source.fdConfig === 'string' ? JSON.stringify(viewCfg) : viewCfg;
89
+ source.fdId = genId();
90
+ source.fdName = options.tplName;
91
+ source.fdXForm = { fdId: options.tplId, fdName: options.tplName };
92
+ return [source];
93
+ }
94
+ /** 克隆权限配置:替换表名,剔除明细表相关配置 */
95
+ function cloneAuth(srcCfg, srcTable, tplTable, detailTable) {
96
+ let text = JSON.stringify(srcCfg.auth || []).split(srcTable).join(tplTable);
97
+ if (detailTable) {
98
+ const replaced = detailTable.split(srcTable).join(tplTable);
99
+ text = text.split(replaced).join(`${tplTable}__DETAIL_REMOVED`);
100
+ }
101
+ const prune = (value) => {
102
+ if (Array.isArray(value))
103
+ return value.map(prune);
104
+ if (value && typeof value === 'object') {
105
+ const out = {};
106
+ Object.keys(value).forEach((key) => {
107
+ if (key.indexOf('__DETAIL_REMOVED') > -1)
108
+ return;
109
+ out[key] = prune(value[key]);
110
+ });
111
+ return out;
112
+ }
113
+ return value;
114
+ };
115
+ return prune(JSON.parse(text));
116
+ }
117
+ /**
118
+ * 克隆源表单 fdConfig 中「数据模型 / 视图 / 权限」以外的配置(attribute / sign / authFilter / extendMap 等)。
119
+ *
120
+ * 设计器加载时会读取 `sign.formula`、`attribute.formAttr` 等结构,缺失会直接抛
121
+ * `Cannot read properties of undefined (reading 'formula')`,因此不能只写 dataModel/viewModel/auth。
122
+ */
123
+ function cloneBaseConfig(srcCfg, srcTable, tplTable, detailTable) {
124
+ const clone = deepClone(srcCfg || {});
125
+ let text = JSON.stringify(clone).split(srcTable).join(tplTable);
126
+ if (detailTable) {
127
+ const replaced = detailTable.split(srcTable).join(tplTable);
128
+ text = text.split(replaced).join(`${tplTable}__DETAIL_REMOVED`);
129
+ }
130
+ const prune = (value) => {
131
+ if (Array.isArray(value))
132
+ return value.map(prune);
133
+ if (value && typeof value === 'object') {
134
+ const out = {};
135
+ Object.keys(value).forEach((key) => {
136
+ if (key.indexOf('__DETAIL_REMOVED') > -1)
137
+ return;
138
+ out[key] = prune(value[key]);
139
+ });
140
+ return out;
141
+ }
142
+ return value;
143
+ };
144
+ return prune(JSON.parse(text));
145
+ }
146
+ /**
147
+ * 分类表单的「发布配置」若缺 `sign` 等结构,模板设计器合并数据时会报
148
+ * `Cannot read properties of undefined (reading 'formula')`(mergeNewData 里 `sign: { formula }` 解构)。
149
+ *
150
+ * 这里用分类表单自身设计重新保存并发布一次,把发布配置补齐。返回是否执行了补齐。
151
+ */
152
+ async function ensureCategoryDesignPublished(client, categoryId, categoryCode, opts) {
153
+ try {
154
+ const rel = await client.post('sys-modeling/categoryModel/getReleaseConfig', { sourceFormCode: categoryCode, currentFormId: categoryId });
155
+ const source = JSON.parse(rel?.sourceDesignConfig || '{}');
156
+ if (source.sign)
157
+ return false;
158
+ logger_1.logger.step('分类表单发布配置缺少 sign,自动补齐(saveDesignAndPublish)');
159
+ const design = await template_1.templateApi.loadDesign(client, categoryId);
160
+ const cfg = JSON.parse(String(design.fdConfig || '{}'));
161
+ const models = cfg.dataModel || [];
162
+ if (!models.length)
163
+ return false;
164
+ const payload = await (0, generator_1.buildFormDefinePayload)(models, {
165
+ base: { ...design, fdId: categoryId },
166
+ formId: categoryId,
167
+ formName: String(design.fdName ?? ''),
168
+ formDefineType: types_1.EFormDefineType.Common,
169
+ entityName: constants_1.MODELING_ENTITY_NAME,
170
+ entityCode: constants_1.MODELING_ENTITY_CODE,
171
+ controlMap: (0, config_1.resolveControlMap)(),
172
+ registry: createRegistry(opts),
173
+ includeSystem: false,
174
+ prevViewModel: cfg.viewModel,
175
+ prevLang: cfg.lang,
176
+ prevAttribute: cfg.attribute,
177
+ });
178
+ await client.post('sys-modeling/sysModelingXForm/saveDesignAndPublish', payload);
179
+ logger_1.logger.success('分类表单发布配置已补齐');
180
+ return true;
181
+ }
182
+ catch (err) {
183
+ logger_1.logger.warn(`补齐分类表单发布配置失败:${err.message}`);
184
+ return false;
185
+ }
186
+ }
187
+ function registerTemplateCommand(program) {
188
+ const template = program
189
+ .command('template')
190
+ .description('分类表单模板(发起页模板):list | create | add-fields | delete');
191
+ // -------------------------------------------------------------------- list
192
+ template
193
+ .command('list')
194
+ .description('列出分类表单下的模板(运行时分类树)')
195
+ .requiredOption('--category-code <code>', '分类表单编码')
196
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
197
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
198
+ .option('--json', '以 JSON 输出')
199
+ .action(async (opts) => {
200
+ const client = createClient(opts);
201
+ const code = String(opts.categoryCode ?? '').trim();
202
+ if (!code)
203
+ throw new Error('--category-code 不能为空');
204
+ const result = await template_1.templateApi.tree(client, code);
205
+ const list = result?.categoryList ?? [];
206
+ if (opts.json) {
207
+ logger_1.logger.json(list);
208
+ return;
209
+ }
210
+ logger_1.logger.table(list.map((item) => ({
211
+ fdId: String(item.fdId ?? ''),
212
+ 名称: String(item.fdName ?? ''),
213
+ 允许选择: item.allowSelect === false ? '否' : '是',
214
+ })));
215
+ logger_1.logger.info(logger_1.color.gray(`共 ${list.length} 个模板`));
216
+ });
217
+ // ------------------------------------------------------------------ create
218
+ template
219
+ .command('create')
220
+ .description('在分类表单下创建模板,并可把既有表单的字段/视图/权限移植进来')
221
+ .requiredOption('--category-code <code>', '分类表单编码(categoryModel 的 fdCode)')
222
+ .requiredOption('--name <name>', '模板名称')
223
+ .option('--code <code>', '模板编码,默认自动生成')
224
+ .option('--from-form <fdId>', '来源表单 id:克隆其业务字段、视图布局、字段权限')
225
+ .option('--fields <names>', '仅移植指定业务字段(逗号分隔,配合 --from-form)')
226
+ .option('--table-name <name>', '模板表名(默认取分类表单的模板表/聚合表)')
227
+ .option('--category-node <fdId>', '挂载到已有分类节点 id(分类表单;不传则自动新建)')
228
+ .option('--category-name <name>', '自动新建的分类节点名称(默认取模板名)')
229
+ .option('--publish', '创建后发布模板')
230
+ .option('--dry-run', '只打印将要提交的数据,不实际请求')
231
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
232
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
233
+ .option('--json', '以 JSON 输出结果')
234
+ .action(async (opts) => {
235
+ const client = createClient(opts);
236
+ const categoryCode = String(opts.categoryCode ?? '').trim();
237
+ const name = String(opts.name ?? '').trim();
238
+ if (!categoryCode)
239
+ throw new Error('--category-code 不能为空');
240
+ if (!name)
241
+ throw new Error('--name 不能为空');
242
+ // 1. 分类表单
243
+ logger_1.logger.step(`查询分类表单:${categoryCode}`);
244
+ const category = toList(await template_1.templateApi.findCategoryByCode(client, categoryCode))[0];
245
+ if (!category?.fdId)
246
+ throw new Error(`分类表单不存在:${categoryCode}`);
247
+ // 模板编辑器会合并分类表单的发布配置,缺 sign 会直接报 "reading 'formula'",先确保配置完整
248
+ await ensureCategoryDesignPublished(client, String(category.fdId), categoryCode, opts);
249
+ const isSimple = Number(category.fdType) === 2;
250
+ const tableName = String(opts.tableName ?? '').trim() ||
251
+ String(isSimple ? category.fdPolymersTableName : category.fdTemplateTableName || '').trim();
252
+ if (!tableName)
253
+ throw new Error('未能确定模板表名,请用 --table-name 指定');
254
+ // 2. 应用 id(模板创建需要 fdAppId)
255
+ let appId = '';
256
+ const appCode = String(category.fdAppCode ?? '').trim();
257
+ if (appCode) {
258
+ const apps = toList(await formDefine_1.modelingAppApi.list(client, { conditions: { fdAppCode: appCode }, pageSize: 1 }));
259
+ appId = String(apps[0]?.fdId ?? '');
260
+ }
261
+ logger_1.logger.info(logger_1.color.gray(`分类表单:${category.fdName}(${isSimple ? '简单分类表单' : '分类表单'}) ` +
262
+ `模板表:${tableName}${appCode ? ` 应用:${appCode}` : ''}`));
263
+ // 3. 分类表单:模板必须挂在分类节点下才会出现在发起页(简单分类的聚合表本身就是节点)
264
+ let nodeId = '';
265
+ if (!isSimple) {
266
+ nodeId = String(opts.categoryNode ?? '').trim();
267
+ if (!nodeId) {
268
+ logger_1.logger.step('创建分类节点');
269
+ const node = await template_1.templateApi.addCategoryNode(client, {
270
+ fdCategoryModelCode: categoryCode,
271
+ fdName: String(opts.categoryName ?? name).trim() || name,
272
+ });
273
+ nodeId = String(node?.fdId ?? '');
274
+ }
275
+ if (!nodeId)
276
+ throw new Error('分类节点创建失败,请用 --category-node 指定已有节点');
277
+ }
278
+ // 4. 模板创建 payload(跳过 init,直接 add + fdBuildNew)
279
+ const payload = {
280
+ fdCategoryModelCode: categoryCode,
281
+ ...(nodeId ? { fdCategory: { fdId: nodeId } } : {}),
282
+ ...(appId ? { fdAppId: appId } : {}),
283
+ fdName: name,
284
+ fdSimpleName: name,
285
+ fdCode: String(opts.code ?? '').trim() || `tpl_${Date.now().toString(36).slice(-6)}`,
286
+ fdStatus: 'draft',
287
+ fdEnable: true,
288
+ fdBuildNew: true,
289
+ mechanisms: {
290
+ load: '*',
291
+ 'sys-xform': {
292
+ fdName: name,
293
+ fdTableName: tableName,
294
+ fdStatus: 'draft',
295
+ fdBuildNew: true,
296
+ fdEntityName: `sys-modeling-tmpl#${categoryCode}`,
297
+ fdEntityId: '',
298
+ fdEntityKey: 'sys-xform',
299
+ },
300
+ },
301
+ };
302
+ if (opts.dryRun) {
303
+ logger_1.logger.title('【dry-run】将提交(模板 add)');
304
+ logger_1.logger.json(payload);
305
+ return;
306
+ }
307
+ logger_1.logger.step(`创建模板:${name}`);
308
+ const created = await template_1.templateApi.create(client, isSimple ? 'simple' : 'category', payload);
309
+ const fdId = String(created?.fdId ?? '');
310
+ if (!fdId)
311
+ throw new Error('模板创建失败:接口未返回 fdId');
312
+ logger_1.logger.success(`模板创建成功,fdId = ${fdId}`);
313
+ // 分类节点挂载放在设计就绪之后(见发布前)
314
+ // 5. 从来源表单移植字段 / 视图 / 权限
315
+ let fieldCount = 0;
316
+ let viewCount = 0;
317
+ if (opts.fromForm) {
318
+ const fromForm = String(opts.fromForm).trim();
319
+ logger_1.logger.step(`加载来源表单设计:${fromForm}`);
320
+ const src = await template_1.templateApi.loadDesign(client, fromForm);
321
+ const srcCfg = JSON.parse(String(src.fdConfig || '{}'));
322
+ const srcMain = (srcCfg.dataModel || []).find((m) => m.fdType === 'main');
323
+ const srcDetail = (srcCfg.dataModel || []).find((m) => m.fdType === 'detail');
324
+ if (!srcMain)
325
+ throw new Error(`来源表单缺少主模型:${fromForm}`);
326
+ logger_1.logger.step(`加载模板设计:${fdId}`);
327
+ const tpl = await template_1.templateApi.loadDesign(client, fdId);
328
+ const tplCfg = JSON.parse(String(tpl.fdConfig || '{}'));
329
+ const tplMain = (tplCfg.dataModel || []).find((m) => m.fdType === 'main');
330
+ if (!tplMain)
331
+ throw new Error('模板缺少主模型,无法移植字段');
332
+ const keepNames = opts.fields
333
+ ? String(opts.fields)
334
+ .split(',')
335
+ .map((s) => s.trim())
336
+ .filter(Boolean)
337
+ : undefined;
338
+ const fields = cloneFields(srcMain, tplMain, keepNames);
339
+ if (!fields.length)
340
+ throw new Error('来源表单没有可移植的业务字段');
341
+ tplMain.fdFields = [...(tplMain.fdFields || []), ...fields];
342
+ fieldCount = fields.length;
343
+ const srcBizNames = (srcMain.fdFields || [])
344
+ .filter((f) => !f.fdIsSystem)
345
+ .map((f) => f.fdName);
346
+ const keptNames = new Set(fields.map((f) => f.fdName));
347
+ const viewModel = cloneViewModel(srcCfg, {
348
+ srcTable: srcMain.fdTableName,
349
+ tplTable: tplMain.fdTableName,
350
+ detailTable: srcDetail?.fdTableName,
351
+ tplId: fdId,
352
+ tplName: name,
353
+ dropFieldNames: srcBizNames.filter((n) => !keptNames.has(n)),
354
+ });
355
+ viewCount = viewModel.length;
356
+ const auth = cloneAuth(srcCfg, srcMain.fdTableName, tplMain.fdTableName, srcDetail?.fdTableName);
357
+ const baseCfg = cloneBaseConfig(srcCfg, srcMain.fdTableName, tplMain.fdTableName, srcDetail?.fdTableName);
358
+ const newCfg = {
359
+ ...baseCfg,
360
+ dataModel: tplCfg.dataModel,
361
+ viewModel,
362
+ auth,
363
+ lang: srcCfg.lang || '{}',
364
+ error: srcCfg.error || '{}',
365
+ };
366
+ // 设计器加载/合并时需要的结构,缺失会报 "reading 'formula'"
367
+ if (!newCfg.sign)
368
+ newCfg.sign = { formula: {} };
369
+ if (!newCfg.attribute)
370
+ newCfg.attribute = { formAttr: '{}' };
371
+ if (!newCfg.authFilter)
372
+ newCfg.authFilter = { detailAuthDefine: {} };
373
+ if (!newCfg.extendMap)
374
+ newCfg.extendMap = {};
375
+ await template_1.templateApi.saveDesign(client, {
376
+ ...tpl,
377
+ // 模板设计的实体 id 必须指向模板自身,为空会导致设计器合并数据时取不到元数据
378
+ fdEntityId: fdId,
379
+ fdEntityKey: 'sys-xform',
380
+ fdEntityName: `sys-modeling-tmpl#${categoryCode}`,
381
+ fdConfig: JSON.stringify(newCfg),
382
+ });
383
+ logger_1.logger.success(`已移植 ${fieldCount} 个业务字段、${viewCount} 个视图`);
384
+ }
385
+ // 6. 兜底挂载分类节点(add 时后端可能忽略 fdCategory,设计就绪后再 update 一次)
386
+ if (nodeId) {
387
+ try {
388
+ const current = await template_1.templateApi.get(client, fdId, categoryCode);
389
+ await template_1.templateApi.update(client, { ...current, fdCategory: { fdId: nodeId } });
390
+ logger_1.logger.info(logger_1.color.gray(`模板已挂到分类节点:${nodeId}`));
391
+ }
392
+ catch (err) {
393
+ logger_1.logger.warn(`挂载分类节点失败(不影响模板本身):${err.message}`);
394
+ }
395
+ }
396
+ // 7. 发布
397
+ if (opts.publish) {
398
+ logger_1.logger.step('发布模板');
399
+ const detail = await template_1.templateApi.get(client, fdId, categoryCode);
400
+ await template_1.templateApi.publish(client, { ...detail, fdStatus: 'official' });
401
+ logger_1.logger.success('模板已发布');
402
+ }
403
+ if (opts.json) {
404
+ logger_1.logger.json({ fdId, categoryCode, tableName, fields: fieldCount, views: viewCount });
405
+ return;
406
+ }
407
+ logger_1.logger.info(logger_1.color.gray(`完成。模板 id:${fdId} 可在平台「${category.fdName}」发起页看到该模板;` +
408
+ `后续加字段可用 llc form autofill --id <来源表单> 生成后再移植。`));
409
+ });
410
+ // -------------------------------------------------------------- add-fields
411
+ template
412
+ .command('add-fields')
413
+ .description('向已有模板追加字段(默认重建栅格布局让字段可见,--keep-layout 保留原布局)')
414
+ .requiredOption('--template-id <fdId>', '模板 id(可用 llc template list 查询)')
415
+ .option('--category-code <code>', '分类表单编码(--publish 时必填)')
416
+ .option('--add-field <spec>', '新增字段,格式 name:label[:dataType|controlType[:controlType]],可重复', collect, [])
417
+ .option('--per-row <n>', '每行控件数(重建布局时生效,默认 2)', (v) => Number(v), 2)
418
+ .option('--keep-layout', '保留现有布局(新字段可能不在画布上,需到设计器拖入)')
419
+ .option('--publish', '保存后发布模板')
420
+ .option('--dry-run', '只打印将新增的字段,不提交')
421
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
422
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
423
+ .option('--json', '以 JSON 输出结果')
424
+ .action(async (opts) => {
425
+ const client = createClient(opts);
426
+ const fdId = String(opts.templateId ?? '').trim();
427
+ const specs = opts.addField ?? [];
428
+ if (!fdId)
429
+ throw new Error('--template-id 不能为空');
430
+ if (!specs.length)
431
+ throw new Error('请用 --add-field 指定要新增的字段');
432
+ logger_1.logger.step(`加载模板设计:${fdId}`);
433
+ const tpl = await template_1.templateApi.loadDesign(client, fdId);
434
+ const cfg = JSON.parse(String(tpl.fdConfig || '{}'));
435
+ const models = cfg.dataModel || [];
436
+ if (!models.length)
437
+ throw new Error('模板数据模型为空,无法新增字段');
438
+ const controlMap = (0, config_1.resolveControlMap)();
439
+ const registry = createRegistry(opts);
440
+ const withNew = await (0, form_1.injectNewFields)(models, specs, controlMap, registry);
441
+ const mainBefore = (models.find((m) => m.fdType === 'main')?.fdFields) || [];
442
+ const beforeNames = new Set(mainBefore.map((f) => f.fdName));
443
+ const mainAfter = (withNew.find((m) => m.fdType === 'main')?.fdFields) || [];
444
+ const added = mainAfter
445
+ .filter((f) => !beforeNames.has(f.fdName))
446
+ .map((f) => f.fdName);
447
+ if (!added.length)
448
+ throw new Error('没有新增字段(--add-field 指定的字段可能已存在)');
449
+ const payload = await (0, generator_1.buildFormDefinePayload)(withNew, {
450
+ base: { ...tpl, fdId },
451
+ formId: fdId,
452
+ formName: String(tpl.fdName ?? ''),
453
+ formDefineType: types_1.EFormDefineType.Common,
454
+ entityName: constants_1.MODELING_ENTITY_NAME,
455
+ entityCode: constants_1.MODELING_ENTITY_CODE,
456
+ controlMap,
457
+ registry,
458
+ perRow: opts.perRow,
459
+ includeSystem: false,
460
+ layout: opts.keepLayout ? undefined : 'grid',
461
+ prevViewModel: cfg.viewModel,
462
+ prevLang: cfg.lang,
463
+ prevAttribute: cfg.attribute,
464
+ });
465
+ const nextCfg = JSON.parse(String(payload.fdConfig || '{}'));
466
+ // 保留模板已有的字段权限(重建布局时 buildAuth 会生成新权限配置)
467
+ if (cfg.auth)
468
+ nextCfg.auth = cfg.auth;
469
+ logger_1.logger.info(logger_1.color.gray(`新增字段:${added.join('、')} 重建布局:${opts.keepLayout ? '否' : '是'}`));
470
+ if (opts.dryRun) {
471
+ logger_1.logger.json({ fdId, added, keepLayout: Boolean(opts.keepLayout) });
472
+ return;
473
+ }
474
+ await template_1.templateApi.saveDesign(client, { ...tpl, fdConfig: JSON.stringify(nextCfg) });
475
+ logger_1.logger.success(`模板设计已保存(新增 ${added.length} 个字段)`);
476
+ if (opts.publish) {
477
+ const categoryCode = String(opts.categoryCode ?? '').trim();
478
+ if (!categoryCode)
479
+ throw new Error('--publish 需要同时提供 --category-code');
480
+ logger_1.logger.step('发布模板');
481
+ const detail = await template_1.templateApi.get(client, fdId, categoryCode);
482
+ await template_1.templateApi.publish(client, { ...detail, fdStatus: 'official' });
483
+ logger_1.logger.success('模板已发布');
484
+ }
485
+ if (opts.json) {
486
+ logger_1.logger.json({ fdId, added, published: Boolean(opts.publish) });
487
+ }
488
+ });
489
+ // ------------------------------------------------------------------ delete
490
+ template
491
+ .command('delete')
492
+ .description('删除分类表单下的模板(--all 删除该分类表单下的全部模板)')
493
+ .requiredOption('--category-code <code>', '分类表单编码')
494
+ .option('--template-id <fdId>', '模板 id,可重复', collect, [])
495
+ .option('--all', '删除该分类表单下的全部模板')
496
+ .option('--yes', '确认执行删除')
497
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
498
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
499
+ .option('--json', '以 JSON 输出结果')
500
+ .action(async (opts) => {
501
+ const client = createClient(opts);
502
+ const code = String(opts.categoryCode ?? '').trim();
503
+ const explicit = (opts.templateId ?? [])
504
+ .map((s) => String(s).trim())
505
+ .filter(Boolean);
506
+ if (!code)
507
+ throw new Error('--category-code 不能为空');
508
+ if (!explicit.length && !opts.all) {
509
+ throw new Error('请用 --template-id 指定模板,或用 --all 删除全部');
510
+ }
511
+ logger_1.logger.step(`查询分类表单:${code}`);
512
+ const category = toList(await template_1.templateApi.findCategoryByCode(client, code))[0];
513
+ if (!category?.fdId)
514
+ throw new Error(`分类表单不存在:${code}`);
515
+ const isSimple = Number(category.fdType) === 2;
516
+ const kind = isSimple ? 'simple' : 'category';
517
+ let ids = explicit;
518
+ if (opts.all) {
519
+ const listed = await template_1.templateApi.listByCategory(client, kind, code);
520
+ const rows = isSimple
521
+ ? (listed?.categoryList ?? [])
522
+ : (listed ?? []);
523
+ ids = rows.map((row) => String(row.fdId ?? '')).filter(Boolean);
524
+ }
525
+ ids = [...new Set(ids)];
526
+ if (!ids.length) {
527
+ logger_1.logger.warn('没有可删除的模板');
528
+ return;
529
+ }
530
+ logger_1.logger.info(logger_1.color.gray(`待删除模板 ${ids.length} 个:${ids.join(', ')}`));
531
+ if (!opts.yes) {
532
+ logger_1.logger.warn('已取消:删除模板不可恢复,请追加 --yes 确认执行');
533
+ return;
534
+ }
535
+ await template_1.templateApi.deleteTemplates(client, kind, code, ids);
536
+ logger_1.logger.success(`已删除 ${ids.length} 个模板`);
537
+ if (opts.json) {
538
+ logger_1.logger.json({ categoryCode: code, deleted: ids });
539
+ }
540
+ });
541
+ }
package/dist/index.js CHANGED
@@ -8,8 +8,10 @@ const control_1 = require("./commands/control");
8
8
  const field_1 = require("./commands/field");
9
9
  const form_1 = require("./commands/form");
10
10
  const locale_1 = require("./commands/locale");
11
+ const operation_1 = require("./commands/operation");
12
+ const template_1 = require("./commands/template");
11
13
  const logger_1 = require("./utils/logger");
12
- const VERSION = '2.5.0';
14
+ const VERSION = '2.6.0';
13
15
  const program = new commander_1.Command();
14
16
  exports.program = program;
15
17
  program
@@ -34,6 +36,8 @@ program.hook('preAction', (thisCommand, actionCommand) => {
34
36
  (0, app_1.registerAppCommand)(program);
35
37
  (0, form_1.registerFormCommand)(program);
36
38
  (0, field_1.registerFieldCommand)(program);
39
+ (0, operation_1.registerOperationCommand)(program);
40
+ (0, template_1.registerTemplateCommand)(program);
37
41
  (0, locale_1.registerLocaleCommand)(program);
38
42
  (0, control_1.registerControlCommand)(program);
39
43
  /**
@@ -106,6 +110,10 @@ ${logger_1.color.bold('常用查询')}
106
110
  llc control list 环境可用控件(含高级控件;明细/分页为容器类)
107
111
  llc locale list 环境提供了哪些语言译文
108
112
  llc field library [--keyword 关键词] 字段库
113
+ llc operation list --form-id <fdId> 表单的业务操作(列表页/查看页按钮)
114
+ llc operation create --form-id <fdId> --name 打开帮助 --code openHelp --type link --url https://example.com
115
+ llc operation delete --id <operationId> --yes 删除业务操作(被引用时会拒绝)
116
+ llc operation attach --form-id <fdId> --operation-id <opId> 挂到列表视图(--target view 挂查看视图)
109
117
 
110
118
  ${logger_1.color.gray('高级控件(desc / tags / mdm / association / mechnumber …)与普通控件同样写法:')}
111
119
  llc form autofill --id <fdId> --apply --add-field "字段名:标题:控件类型"
package/package.json CHANGED
@@ -1,49 +1,49 @@
1
- {
2
- "name": "lan-low-code-cli",
3
- "version": "2.5.0",
4
- "description": "lan-low-code cli:低代码表单引擎命令行工具(创建表单、获取可用字段、自动完善表单字段)",
5
- "keywords": [
6
- "low-code",
7
- "lan-low-code",
8
- "llc",
9
- "form",
10
- "cli"
11
- ],
12
- "license": "MIT",
13
- "packageManager": "pnpm@10.26.2",
14
- "bin": {
15
- "llc": "bin/llc.js"
16
- },
17
- "main": "dist/index.js",
18
- "scripts": {
19
- "build": "tsc -p tsconfig.json",
20
- "clean": "rimraf dist",
21
- "rebuild": "pnpm run clean && pnpm run build",
22
- "start": "pnpm run build && node bin/llc.js",
23
- "typecheck": "tsc -p tsconfig.json --noEmit",
24
- "prepare": "pnpm run build",
25
- "bump": "node scripts/bump-version.js",
26
- "bump:check": "node scripts/bump-version.js --check",
27
- "pack": "node scripts/pack.js",
28
- "pack:cli": "node scripts/pack.js --target cli",
29
- "pack:skill": "node scripts/pack.js --target skill"
30
- },
31
- "dependencies": {
32
- "commander": "^11.1.0"
33
- },
34
- "devDependencies": {
35
- "@types/node": "^20.11.30",
36
- "rimraf": "^5.0.5",
37
- "typescript": "^5.4.5"
38
- },
39
- "engines": {
40
- "node": ">=18",
41
- "pnpm": ">=8"
42
- },
43
- "files": [
44
- "bin",
45
- "dist",
46
- "skills",
47
- "README.md"
48
- ]
49
- }
1
+ {
2
+ "name": "lan-low-code-cli",
3
+ "version": "2.6.0",
4
+ "description": "lan-low-code cli:低代码表单引擎命令行工具(创建表单、获取可用字段、自动完善表单字段)",
5
+ "keywords": [
6
+ "low-code",
7
+ "lan-low-code",
8
+ "llc",
9
+ "form",
10
+ "cli"
11
+ ],
12
+ "license": "MIT",
13
+ "packageManager": "pnpm@10.26.2",
14
+ "bin": {
15
+ "llc": "bin/llc.js"
16
+ },
17
+ "main": "dist/index.js",
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "clean": "rimraf dist",
21
+ "rebuild": "pnpm run clean && pnpm run build",
22
+ "start": "pnpm run build && node bin/llc.js",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "prepare": "pnpm run build",
25
+ "bump": "node scripts/bump-version.js",
26
+ "bump:check": "node scripts/bump-version.js --check",
27
+ "pack": "node scripts/pack.js",
28
+ "pack:cli": "node scripts/pack.js --target cli",
29
+ "pack:skill": "node scripts/pack.js --target skill"
30
+ },
31
+ "dependencies": {
32
+ "commander": "^11.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.11.30",
36
+ "rimraf": "^5.0.5",
37
+ "typescript": "^5.4.5"
38
+ },
39
+ "engines": {
40
+ "node": ">=18",
41
+ "pnpm": ">=8"
42
+ },
43
+ "files": [
44
+ "bin",
45
+ "dist",
46
+ "skills",
47
+ "README.md"
48
+ ]
49
+ }