lan-low-code-cli 2.4.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,730 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerOperationCommand = registerOperationCommand;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const operation_1 = require("../api/operation");
7
+ const view_1 = require("../api/view");
8
+ const client_1 = require("../client");
9
+ const config_1 = require("../config");
10
+ const logger_1 = require("../utils/logger");
11
+ function createClient(opts) {
12
+ return new client_1.LcodeClient((0, config_1.resolveRuntime)({ baseUrl: opts.baseUrl, token: opts.token }));
13
+ }
14
+ const KIND_ALIAS = {
15
+ link: 'link',
16
+ url: 'link',
17
+ script: 'script',
18
+ js: 'script',
19
+ dialog: 'dialog',
20
+ modal: 'dialog',
21
+ };
22
+ const KIND_META = {
23
+ link: { fdType: 2, fdViewType: 4, label: '打开链接' },
24
+ script: { fdType: 3, fdViewType: 5, label: '执行脚本' },
25
+ dialog: { fdType: 3, fdViewType: 1, label: '执行业务操作(对话框)' },
26
+ };
27
+ /** 解析 fdConfig(后端实体不一定有顶层 fdViewType,视图类型存在 fdConfig 中) */
28
+ function readOperationConfig(op) {
29
+ if (!op.fdConfig)
30
+ return {};
31
+ try {
32
+ const parsed = typeof op.fdConfig === 'string' ? JSON.parse(op.fdConfig) : op.fdConfig;
33
+ return parsed && typeof parsed === 'object' ? parsed : {};
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ }
39
+ function operationTypeLabel(op) {
40
+ if (op.fdType === 1)
41
+ return '内置';
42
+ if (op.fdType === 4)
43
+ return '对话框';
44
+ if (op.fdType === 6)
45
+ return 'AI 能力';
46
+ const viewType = Number(readOperationConfig(op).fdViewType ?? op.fdViewType);
47
+ if (op.fdType === 3)
48
+ return viewType === 5 ? '脚本' : '无视图';
49
+ return '有视图';
50
+ }
51
+ function positionLabel(position) {
52
+ if (position === 2)
53
+ return '查看视图';
54
+ if (position === 3)
55
+ return '新建/编辑视图';
56
+ return '列表视图';
57
+ }
58
+ /** `@file` 读取文件内容,其余按字面量返回 */
59
+ function readScriptValue(input) {
60
+ if (!input.startsWith('@'))
61
+ return input;
62
+ const file = (0, path_1.isAbsolute)(input.slice(1)) ? input.slice(1) : (0, path_1.resolve)(process.cwd(), input.slice(1));
63
+ if (!(0, fs_1.existsSync)(file))
64
+ throw new Error(`脚本文件不存在:${file}`);
65
+ return (0, fs_1.readFileSync)(file, 'utf8');
66
+ }
67
+ const TARGET_LABEL = { list: '列表视图', view: '查看视图' };
68
+ const ATTACH_POSITIONS = ['top', 'rightHand', 'rightSide'];
69
+ function toObject(input) {
70
+ if (!input)
71
+ return {};
72
+ if (typeof input === 'object')
73
+ return input;
74
+ try {
75
+ const parsed = JSON.parse(String(input));
76
+ return parsed && typeof parsed === 'object' ? parsed : {};
77
+ }
78
+ catch {
79
+ return {};
80
+ }
81
+ }
82
+ function platformConfig(config, platform) {
83
+ const current = config[platform];
84
+ if (current && typeof current === 'object')
85
+ return current;
86
+ const next = {};
87
+ config[platform] = next;
88
+ return next;
89
+ }
90
+ function readViewConfig(vo, target) {
91
+ if (target === 'list')
92
+ return toObject(vo.fdExtendConfig);
93
+ const view = vo;
94
+ return view.fdConfigVO
95
+ ? view.fdConfigVO
96
+ : toObject(view.fdConfig);
97
+ }
98
+ async function saveViewConfig(client, target, vo, config, formId) {
99
+ if (target === 'list') {
100
+ await view_1.listViewApi.update(client, { ...vo, fdExtendConfig: JSON.stringify(config) });
101
+ return;
102
+ }
103
+ const { fdConfigVO: _ignored, ...rest } = vo;
104
+ await view_1.formViewApi.update(client, {
105
+ ...rest,
106
+ fdXForm: vo.fdXForm ?? { fdId: formId },
107
+ fdConfig: JSON.stringify(config),
108
+ });
109
+ }
110
+ function assertSameForm(actual, expected, label) {
111
+ if (actual && actual !== expected) {
112
+ throw new Error(`${label}不属于该表单(表单 ${actual},期望 ${expected})`);
113
+ }
114
+ }
115
+ async function loadListView(client, formId, viewId) {
116
+ if (viewId) {
117
+ const vo = await view_1.listViewApi.get(client, viewId);
118
+ if (!vo?.fdId)
119
+ throw new Error(`列表视图不存在:${viewId}`);
120
+ assertSameForm(vo.fdXForm?.fdId, formId, '列表视图');
121
+ return vo;
122
+ }
123
+ const res = await view_1.listViewApi.list(client, {
124
+ conditions: { fdXForm: formId },
125
+ pageNo: 1,
126
+ pageSize: 50,
127
+ columns: ['fdId', 'fdName'],
128
+ });
129
+ const views = res?.content ?? [];
130
+ if (!views.length)
131
+ throw new Error('该表单没有列表视图');
132
+ if (views.length > 1) {
133
+ const names = views.map((v, i) => ` ${i + 1}. ${v.fdId} ${v.fdName ?? ''}`).join('\n');
134
+ throw new Error(`该表单有 ${views.length} 个列表视图,请用 --view-id 指定:\n${names}`);
135
+ }
136
+ return view_1.listViewApi.get(client, views[0].fdId);
137
+ }
138
+ async function loadFormView(client, formId, viewId) {
139
+ if (viewId) {
140
+ const vo = await view_1.formViewApi.get(client, viewId);
141
+ if (!vo?.fdId)
142
+ throw new Error(`查看视图不存在:${viewId}`);
143
+ assertSameForm(vo.fdXForm?.fdId, formId, '查看视图');
144
+ return vo;
145
+ }
146
+ let res = await view_1.formViewApi.list(client, {
147
+ conditions: { 'fdXForm.fdId': formId },
148
+ pageNo: 1,
149
+ pageSize: 50,
150
+ columns: ['fdId', 'fdName'],
151
+ });
152
+ if (!res?.content?.length) {
153
+ res = await view_1.formViewApi.list(client, {
154
+ conditions: { fdXForm: formId },
155
+ pageNo: 1,
156
+ pageSize: 50,
157
+ columns: ['fdId', 'fdName'],
158
+ });
159
+ }
160
+ const views = res?.content ?? [];
161
+ if (!views.length)
162
+ throw new Error('该表单没有查看视图');
163
+ if (views.length > 1) {
164
+ const names = views.map((v, i) => ` ${i + 1}. ${v.fdId} ${v.fdName ?? ''}`).join('\n');
165
+ throw new Error(`该表单有 ${views.length} 个查看视图,请用 --view-id 指定:\n${names}`);
166
+ }
167
+ return view_1.formViewApi.get(client, views[0].fdId);
168
+ }
169
+ function normalizePlatform(input) {
170
+ const value = String(input ?? 'desktop').trim().toLowerCase();
171
+ if (value !== 'desktop' && value !== 'mobile') {
172
+ throw new Error(`未知 --platform:${input}(可选 desktop | mobile)`);
173
+ }
174
+ return value;
175
+ }
176
+ function upsertOperationItem(list, opId, position, extra = {}) {
177
+ const index = list.findIndex((item) => item.id === opId);
178
+ if (index < 0) {
179
+ list.push({ id: opId, position, ...extra });
180
+ return 'added';
181
+ }
182
+ if (list[index].position === position)
183
+ return 'unchanged';
184
+ list[index] = { ...list[index], position };
185
+ return 'updated';
186
+ }
187
+ /** 查看视图的 operationList 项带有展示条件等字段,补全可减少运行时差异 */
188
+ function operationItemExtra(op) {
189
+ return {
190
+ fdId: op.fdId,
191
+ fdName: op.fdName,
192
+ name: op.fdName,
193
+ code: op.fdType === 1 ? op.fdCode : op.fdUniqueCode || op.fdCode,
194
+ fdType: op.fdType,
195
+ fdPosition: op.fdPosition,
196
+ logic: 'none',
197
+ ...(op.isModal ? { isModal: true } : {}),
198
+ };
199
+ }
200
+ function assertOperationName(name) {
201
+ if (!name)
202
+ throw new Error('--name 不能为空');
203
+ if (name.length > 30)
204
+ throw new Error('--name 最长 30 个字符');
205
+ }
206
+ function assertOperationCode(code) {
207
+ if (!code)
208
+ throw new Error('--code 不能为空(自定义操作编码,最长 15)');
209
+ if (code.length > 15)
210
+ throw new Error('--code 最长 15 个字符');
211
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(code)) {
212
+ throw new Error('--code 需以字母开头,仅允许字母、数字、下划线、连字符');
213
+ }
214
+ }
215
+ /** 从已有操作推导类型(fdType=2 为链接;fdType=3 + fdViewType=5 为脚本,其余按对话框) */
216
+ function detectKind(op) {
217
+ if (op.fdType === 2)
218
+ return 'link';
219
+ const viewType = Number(readOperationConfig(op).fdViewType ?? op.fdViewType);
220
+ return op.fdType === 3 && viewType === 5 ? 'script' : 'dialog';
221
+ }
222
+ function buildCreatePayload(opts) {
223
+ const formId = String(opts.formId ?? '').trim();
224
+ const name = String(opts.name ?? '').trim();
225
+ const code = String(opts.code ?? '').trim();
226
+ const rawKind = String(opts.type ?? '').trim().toLowerCase();
227
+ const kind = KIND_ALIAS[rawKind];
228
+ const positionRaw = String(opts.position ?? 'list').trim().toLowerCase();
229
+ const fdPosition = positionRaw === 'view' ? 2 : positionRaw === 'list' ? 1 : 0;
230
+ if (!formId)
231
+ throw new Error('--form-id 不能为空');
232
+ assertOperationName(name);
233
+ assertOperationCode(code);
234
+ if (!kind)
235
+ throw new Error(`未知 --type:${opts.type}(可选 link | script | dialog)`);
236
+ if (!fdPosition)
237
+ throw new Error(`未知 --position:${opts.position}(可选 list | view)`);
238
+ const desc = opts.desc === undefined ? '' : String(opts.desc);
239
+ if (desc.length > 500)
240
+ throw new Error('--desc 最长 500 个字符');
241
+ const meta = KIND_META[kind];
242
+ const config = {
243
+ authFieldList: [],
244
+ createInitDataFieldList: [],
245
+ listViewInitDataFieldList: [],
246
+ fdViewFormModel: { fdId: '', fdName: '' },
247
+ fdViewModel: { fdId: '', fdName: '', fdModelName: '' },
248
+ fdViewType: meta.fdViewType,
249
+ fdIdField: null,
250
+ // 「勾选数据」:仅列表视图的自定义操作可配置;默认不要求勾选
251
+ fdListViewIdValid: fdPosition === 1 && opts.needSelect === true,
252
+ };
253
+ if (kind === 'link') {
254
+ const url = String(opts.url ?? '').trim();
255
+ if (!url)
256
+ throw new Error('--type link 需指定 --url');
257
+ // 勾选数据时,链接按接口调用(与平台弹窗的推导一致)
258
+ config.fdUrlType = config.fdListViewIdValid ? '2' : '1';
259
+ config.fdLinkAddress = url;
260
+ }
261
+ else if (kind === 'script') {
262
+ const script = String(opts.script ?? '');
263
+ if (!script.trim())
264
+ throw new Error('--type script 需指定 --script <代码|@文件>');
265
+ config.fdJavaScript = readScriptValue(script);
266
+ }
267
+ else {
268
+ const dialogId = String(opts.dialogId ?? '').trim();
269
+ if (!dialogId)
270
+ throw new Error('--type dialog 需指定 --dialog-id');
271
+ config.fdModal = { fdId: dialogId };
272
+ config.fdModalType = 1;
273
+ config.fdModalSizeType = 2;
274
+ config.fdModalDetail = { modalId: dialogId };
275
+ }
276
+ const payload = {
277
+ fdName: name,
278
+ fdUniqueCode: code,
279
+ fdDesc: desc,
280
+ fdType: meta.fdType,
281
+ fdPosition,
282
+ fdXForm: { fdId: formId },
283
+ fdConfig: JSON.stringify(config),
284
+ };
285
+ if (opts.mobileIcon)
286
+ payload.fdMobileIcon = String(opts.mobileIcon);
287
+ if (kind === 'dialog')
288
+ payload.isModal = true;
289
+ return { payload, kind };
290
+ }
291
+ /**
292
+ * 基于现有操作构造 update 请求体。
293
+ * 显式切换类型(--type)时会重建 fdConfig 的类型相关字段,
294
+ * 但保留权限、默认值等与类型无关的配置。
295
+ */
296
+ function buildUpdatePayload(current, opts) {
297
+ if (current.fdType !== 2 && current.fdType !== 3) {
298
+ throw new Error(`该操作类型为「${operationTypeLabel(current)}」,仅支持修改自定义的链接 / 脚本 / 对话框操作`);
299
+ }
300
+ const currentKind = detectKind(current);
301
+ const rawKind = String(opts.type ?? '').trim().toLowerCase();
302
+ let kind = currentKind;
303
+ if (rawKind) {
304
+ const resolved = KIND_ALIAS[rawKind];
305
+ if (!resolved)
306
+ throw new Error(`未知 --type:${opts.type}(可选 link | script | dialog)`);
307
+ kind = resolved;
308
+ }
309
+ else {
310
+ // 未指定 --type 时,不允许通过其它类型的参数隐式换类型
311
+ const conflicts = [
312
+ [opts.script !== undefined, 'script'],
313
+ [opts.url !== undefined, 'link'],
314
+ [opts.dialogId !== undefined, 'dialog'],
315
+ ];
316
+ conflicts.forEach(([given, expected]) => {
317
+ if (given && expected !== currentKind) {
318
+ throw new Error(`该操作当前是「${KIND_META[currentKind].label}」,改成「${KIND_META[expected].label}」请加 --type ${expected}`);
319
+ }
320
+ });
321
+ }
322
+ const oldConfig = readOperationConfig(current);
323
+ const switching = kind !== currentKind;
324
+ const config = switching
325
+ ? {
326
+ authFieldList: oldConfig.authFieldList ?? [],
327
+ createInitDataFieldList: oldConfig.createInitDataFieldList ?? [],
328
+ listViewInitDataFieldList: oldConfig.listViewInitDataFieldList ?? [],
329
+ fdViewFormModel: oldConfig.fdViewFormModel ?? { fdId: '', fdName: '' },
330
+ fdViewModel: oldConfig.fdViewModel ?? { fdId: '', fdName: '', fdModelName: '' },
331
+ fdViewType: KIND_META[kind].fdViewType,
332
+ fdIdField: null,
333
+ fdListViewIdValid: oldConfig.fdListViewIdValid === true,
334
+ }
335
+ : { ...oldConfig };
336
+ if (config.fdViewType === undefined)
337
+ config.fdViewType = KIND_META[kind].fdViewType;
338
+ const needSelect = opts.needSelect;
339
+ if (needSelect !== undefined)
340
+ config.fdListViewIdValid = needSelect;
341
+ if (kind === 'link') {
342
+ const url = opts.url !== undefined ? String(opts.url).trim() : String(config.fdLinkAddress ?? '');
343
+ if (!url)
344
+ throw new Error('链接操作需指定 --url');
345
+ config.fdUrlType = config.fdListViewIdValid === true ? '2' : '1';
346
+ config.fdLinkAddress = url;
347
+ }
348
+ else if (kind === 'script') {
349
+ const script = opts.script !== undefined
350
+ ? readScriptValue(String(opts.script))
351
+ : String(config.fdJavaScript ?? '');
352
+ if (!script.trim())
353
+ throw new Error('脚本操作需指定 --script <代码|@文件>');
354
+ config.fdJavaScript = script;
355
+ }
356
+ else {
357
+ const dialogId = opts.dialogId !== undefined
358
+ ? String(opts.dialogId).trim()
359
+ : String(config.fdModal?.fdId ?? '');
360
+ if (!dialogId)
361
+ throw new Error('对话框操作需指定 --dialog-id');
362
+ config.fdModal = { fdId: dialogId };
363
+ config.fdModalType = 1;
364
+ config.fdModalSizeType = 2;
365
+ config.fdModalDetail = { modalId: dialogId };
366
+ }
367
+ const name = opts.name !== undefined ? String(opts.name).trim() : current.fdName;
368
+ assertOperationName(name);
369
+ const code = opts.code !== undefined
370
+ ? String(opts.code).trim()
371
+ : String(current.fdUniqueCode || current.fdCode || '');
372
+ if (opts.code !== undefined)
373
+ assertOperationCode(code);
374
+ let fdPosition = current.fdPosition;
375
+ if (opts.position !== undefined) {
376
+ const positionRaw = String(opts.position).trim().toLowerCase();
377
+ if (positionRaw !== 'list' && positionRaw !== 'view') {
378
+ throw new Error(`未知 --position:${opts.position}(可选 list | view)`);
379
+ }
380
+ fdPosition = positionRaw === 'view' ? 2 : 1;
381
+ }
382
+ const desc = opts.desc !== undefined ? String(opts.desc) : String(current.fdDesc ?? '');
383
+ if (desc.length > 500)
384
+ throw new Error('--desc 最长 500 个字符');
385
+ const payload = {
386
+ ...current,
387
+ fdName: name,
388
+ fdUniqueCode: code,
389
+ fdDesc: desc,
390
+ fdType: KIND_META[kind].fdType,
391
+ fdPosition,
392
+ fdConfig: JSON.stringify(config),
393
+ };
394
+ // 只读视图字段,回传可能导致更新失败
395
+ delete payload.fdConfigVO;
396
+ if (kind === 'dialog')
397
+ payload.isModal = true;
398
+ else
399
+ delete payload.isModal;
400
+ if (opts.mobileIcon !== undefined)
401
+ payload.fdMobileIcon = String(opts.mobileIcon);
402
+ if (opts.enable === true)
403
+ payload.fdEnabled = true;
404
+ if (opts.disable === true)
405
+ payload.fdEnabled = false;
406
+ return { payload, kind };
407
+ }
408
+ function registerOperationCommand(program) {
409
+ const operation = program
410
+ .command('operation')
411
+ .description('表单业务操作(列表页/查看页按钮):list | get | create | update | delete | attach | detach');
412
+ // ------------------------------------------------------------------ list
413
+ operation
414
+ .command('list')
415
+ .description('列出表单的业务操作')
416
+ .requiredOption('--form-id <fdId>', '表单 id')
417
+ .option('--keyword <keyword>', '按名称模糊搜索')
418
+ .option('--page-no <n>', '页码', (v) => Number(v), 1)
419
+ .option('--page-size <n>', '每页数量', (v) => Number(v), 100)
420
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
421
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
422
+ .option('--json', '以 JSON 输出')
423
+ .action(async (opts) => {
424
+ const formId = String(opts.formId ?? '').trim();
425
+ if (!formId)
426
+ throw new Error('表单 id 不能为空');
427
+ const client = createClient(opts);
428
+ const conditions = { fdXForm: formId };
429
+ if (opts.keyword)
430
+ conditions.fdName = { $contains: String(opts.keyword).trim() };
431
+ const result = await operation_1.operationApi.list(client, {
432
+ pageNo: opts.pageNo,
433
+ pageSize: opts.pageSize,
434
+ conditions,
435
+ fdXFormId: formId,
436
+ sorts: { fdCreateTime: 'desc' },
437
+ // 仅请求平台实体确认存在的列(fdViewType 在 fdConfig 内,不是顶层字段)
438
+ columns: [
439
+ 'fdId',
440
+ 'fdName',
441
+ 'fdCode',
442
+ 'fdUniqueCode',
443
+ 'fdType',
444
+ 'fdPosition',
445
+ 'fdConfig',
446
+ 'fdEnabled',
447
+ 'fdMobileIcon',
448
+ 'fdDesc',
449
+ 'fdCreateTime',
450
+ ],
451
+ });
452
+ const content = result?.content ?? [];
453
+ if (opts.json) {
454
+ logger_1.logger.json(result);
455
+ return;
456
+ }
457
+ logger_1.logger.table(content.map((item) => ({
458
+ fdId: item.fdId,
459
+ 名称: item.fdName ?? '',
460
+ // 自定义操作的 fdCode 统一为 other,真正的编码在 fdUniqueCode
461
+ 编码: (item.fdType === 1 ? item.fdCode : item.fdUniqueCode || item.fdCode) || '',
462
+ 类型: operationTypeLabel(item),
463
+ 位置: positionLabel(item.fdPosition),
464
+ 状态: item.fdEnabled === false ? '未启用' : '启用',
465
+ })));
466
+ logger_1.logger.info(logger_1.color.gray(`共 ${content.length} 条`));
467
+ });
468
+ // ------------------------------------------------------------------- get
469
+ operation
470
+ .command('get')
471
+ .description('查看业务操作详情(含 fdConfig)')
472
+ .requiredOption('--id <fdId>', '业务操作 id')
473
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
474
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
475
+ .option('--json', '以 JSON 输出')
476
+ .action(async (opts) => {
477
+ const fdId = String(opts.id ?? '').trim();
478
+ if (!fdId)
479
+ throw new Error('业务操作 id 不能为空');
480
+ const client = createClient(opts);
481
+ const vo = await operation_1.operationApi.get(client, fdId);
482
+ if (opts.json) {
483
+ logger_1.logger.json(vo);
484
+ return;
485
+ }
486
+ const displayCode = vo.fdType === 1 ? vo.fdCode : vo.fdUniqueCode || vo.fdCode;
487
+ logger_1.logger.title(`\n${vo.fdName} (${displayCode || vo.fdId})`);
488
+ logger_1.logger.info(` fdId ${vo.fdId}`);
489
+ logger_1.logger.info(` 类型 ${operationTypeLabel(vo)}`);
490
+ logger_1.logger.info(` 位置 ${positionLabel(vo.fdPosition)}`);
491
+ logger_1.logger.info(` 视图类型 ${String(readOperationConfig(vo).fdViewType ?? '')}`);
492
+ logger_1.logger.info(` 状态 ${vo.fdEnabled === false ? '未启用' : '启用'}`);
493
+ logger_1.logger.info(` 描述 ${vo.fdDesc ?? ''}`);
494
+ if (vo.fdConfig) {
495
+ logger_1.logger.info('\nfdConfig:');
496
+ try {
497
+ logger_1.logger.json(JSON.parse(vo.fdConfig));
498
+ }
499
+ catch {
500
+ logger_1.logger.info(vo.fdConfig);
501
+ }
502
+ }
503
+ });
504
+ // ---------------------------------------------------------------- create
505
+ operation
506
+ .command('create')
507
+ .description('新建业务操作(支持打开链接 / 执行脚本 / 执行对话框)')
508
+ .requiredOption('--form-id <fdId>', '表单 id')
509
+ .requiredOption('--name <name>', '操作名称(≤30)')
510
+ .requiredOption('--code <code>', '自定义操作编码(≤15,字母开头,可含数字/下划线/连字符)')
511
+ .requiredOption('--type <type>', '类型:link | script | dialog')
512
+ .option('--position <position>', '使用位置:list(列表视图)| view(查看视图)', 'list')
513
+ .option('--need-select', '点击前需勾选数据(默认否;仅列表视图的自定义操作有效)')
514
+ .option('--url <url>', '打开链接地址(--type link 必填)')
515
+ .option('--script <code|@file>', '脚本内容或 @文件(--type script 必填)')
516
+ .option('--dialog-id <fdId>', '对话框 id(--type dialog 必填,可在平台对话框设计中获取)')
517
+ .option('--desc <text>', '描述(≤500)')
518
+ .option('--mobile-icon <name>', '移动端图标名')
519
+ .option('--dry-run', '只打印请求体,不提交')
520
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
521
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
522
+ .option('--json', '以 JSON 输出结果')
523
+ .action(async (opts) => {
524
+ const { payload, kind } = buildCreatePayload(opts);
525
+ if (opts.dryRun) {
526
+ logger_1.logger.info(logger_1.color.gray(`# ${KIND_META[kind].label} · 表单 ${String(opts.formId)}(dry-run)`));
527
+ logger_1.logger.json(payload);
528
+ return;
529
+ }
530
+ const client = createClient(opts);
531
+ logger_1.logger.step(`新建业务操作:${String(opts.name)}(${KIND_META[kind].label})`);
532
+ const result = await operation_1.operationApi.add(client, payload);
533
+ const fdId = typeof result === 'string' ? result : result?.fdId;
534
+ if (opts.json) {
535
+ logger_1.logger.json({ fdId: fdId ?? null });
536
+ return;
537
+ }
538
+ logger_1.logger.success(fdId ? `已创建:${fdId}` : '已创建');
539
+ logger_1.logger.info(logger_1.color.gray('默认仅管理员可用;需要授权请到平台「业务操作」中设置可访问者。'));
540
+ });
541
+ // ---------------------------------------------------------------- update
542
+ operation
543
+ .command('update')
544
+ .description('修改业务操作(可把已有按钮切换为脚本模式并替换脚本)')
545
+ .requiredOption('--id <fdId>', '业务操作 id')
546
+ .option('--name <name>', '操作名称(≤30)')
547
+ .option('--code <code>', '自定义操作编码(≤15)')
548
+ .option('--desc <text>', '描述(≤500)')
549
+ .option('--position <position>', '使用位置:list(列表视图)| view(查看视图)')
550
+ .option('--type <type>', '切换类型:link | script | dialog(省略时保持现有类型)')
551
+ .option('--url <url>', '打开链接地址(link)')
552
+ .option('--script <code|@file>', '脚本内容或 @文件(script)')
553
+ .option('--dialog-id <fdId>', '对话框 id(dialog)')
554
+ .option('--need-select', '点击前需勾选数据')
555
+ .option('--no-need-select', '不需要勾选数据')
556
+ .option('--enable', '启用该操作')
557
+ .option('--disable', '停用该操作')
558
+ .option('--mobile-icon <name>', '移动端图标名')
559
+ .option('--dry-run', '只打印请求体,不提交')
560
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
561
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
562
+ .option('--json', '以 JSON 输出结果')
563
+ .action(async (opts) => {
564
+ const fdId = String(opts.id ?? '').trim();
565
+ if (!fdId)
566
+ throw new Error('业务操作 id 不能为空');
567
+ if (opts.enable && opts.disable)
568
+ throw new Error('--enable 与 --disable 不能同时使用');
569
+ const client = createClient(opts);
570
+ const current = await operation_1.operationApi.get(client, fdId);
571
+ if (!current?.fdId)
572
+ throw new Error(`业务操作不存在:${fdId}`);
573
+ const { payload, kind } = buildUpdatePayload(current, opts);
574
+ if (opts.dryRun) {
575
+ logger_1.logger.info(logger_1.color.gray(`# ${KIND_META[kind].label} · ${current.fdName}(dry-run)`));
576
+ logger_1.logger.json(payload);
577
+ return;
578
+ }
579
+ logger_1.logger.step(`修改业务操作:${String(payload.fdName)}(${KIND_META[kind].label})`);
580
+ await operation_1.operationApi.update(client, payload);
581
+ if (opts.json) {
582
+ logger_1.logger.json({ fdId, type: kind });
583
+ return;
584
+ }
585
+ logger_1.logger.success(`已更新:${fdId}`);
586
+ logger_1.logger.info(logger_1.color.gray('切换到脚本模式后,需在视图中挂载该操作(llc operation attach)才会显示为按钮。'));
587
+ });
588
+ // ---------------------------------------------------------------- delete
589
+ operation
590
+ .command('delete')
591
+ .description('删除业务操作(被页面引用时会拒绝;需 --yes 确认)')
592
+ .requiredOption('--id <fdId>', '业务操作 id')
593
+ .option('--yes', '确认执行删除')
594
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
595
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
596
+ .action(async (opts) => {
597
+ const fdId = String(opts.id ?? '').trim();
598
+ if (!fdId)
599
+ throw new Error('业务操作 id 不能为空');
600
+ if (!opts.yes) {
601
+ logger_1.logger.warn('已取消:删除业务操作不可恢复,请追加 --yes 确认执行');
602
+ return;
603
+ }
604
+ const client = createClient(opts);
605
+ logger_1.logger.step(`检查引用:${fdId}`);
606
+ let referContent = [];
607
+ try {
608
+ const refer = await operation_1.operationApi.listByRefer(client, fdId);
609
+ referContent = refer?.content ?? [];
610
+ }
611
+ catch (err) {
612
+ logger_1.logger.warn(`引用检查失败,继续删除:${err.message}`);
613
+ }
614
+ if (referContent.length > 0) {
615
+ referContent.forEach((item, index) => {
616
+ const vo = item;
617
+ logger_1.logger.info(` ${index + 1}. ${String(vo.fdEntityLabel ?? '')} -- ${String(vo.fdEntitySubject ?? '')}`);
618
+ });
619
+ throw new Error('该业务操作已被页面引用,请先在对应视图中移除后再删除');
620
+ }
621
+ await operation_1.operationApi.remove(client, fdId);
622
+ logger_1.logger.success(`业务操作 ${fdId} 已删除`);
623
+ });
624
+ // ---------------------------------------------------------------- attach
625
+ operation
626
+ .command('attach')
627
+ .description('把业务操作挂到列表视图/查看视图(写入 operationList)')
628
+ .requiredOption('--form-id <fdId>', '表单 id')
629
+ .requiredOption('--operation-id <fdId>', '业务操作 id')
630
+ .option('--target <target>', '挂载目标:list(列表视图)| view(查看视图)', 'list')
631
+ .option('--view-id <fdId>', '视图 id(省略时自动定位该表单唯一的列表/查看视图)')
632
+ .option('--platform <platform>', 'desktop | mobile', 'desktop')
633
+ .option('--position <position>', 'top | rightHand | rightSide', 'top')
634
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
635
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
636
+ .action(async (opts) => {
637
+ const formId = String(opts.formId ?? '').trim();
638
+ const opId = String(opts.operationId ?? '').trim();
639
+ if (!formId)
640
+ throw new Error('--form-id 不能为空');
641
+ if (!opId)
642
+ throw new Error('--operation-id 不能为空');
643
+ const target = String(opts.target ?? 'list').trim().toLowerCase();
644
+ if (target !== 'list' && target !== 'view') {
645
+ throw new Error(`未知 --target:${opts.target}(可选 list | view)`);
646
+ }
647
+ const position = String(opts.position ?? 'top').trim();
648
+ if (!ATTACH_POSITIONS.includes(position)) {
649
+ throw new Error(`未知 --position:${position}(可选 ${ATTACH_POSITIONS.join(' | ')})`);
650
+ }
651
+ const platform = normalizePlatform(opts.platform);
652
+ const client = createClient(opts);
653
+ const op = await operation_1.operationApi.get(client, opId);
654
+ assertSameForm(op.fdXForm?.fdId, formId, '业务操作');
655
+ if (target === 'list' && op.fdPosition === 2) {
656
+ logger_1.logger.warn('该操作的使用位置是「查看视图」,挂到列表视图后可能不会显示');
657
+ }
658
+ if (target === 'view' && op.fdPosition === 1) {
659
+ logger_1.logger.warn('该操作的使用位置是「列表视图」,挂到查看视图后可能不会显示');
660
+ }
661
+ const vo = target === 'list'
662
+ ? await loadListView(client, formId, opts.viewId)
663
+ : await loadFormView(client, formId, opts.viewId);
664
+ const config = readViewConfig(vo, target);
665
+ const platformCfg = platformConfig(config, platform);
666
+ const list = Array.isArray(platformCfg.operationList) ? platformCfg.operationList : [];
667
+ const change = upsertOperationItem(list, opId, position, target === 'view' ? operationItemExtra(op) : {});
668
+ if (change === 'unchanged') {
669
+ logger_1.logger.info(`已挂载,无需修改:${TARGET_LABEL[target]} / ${platform} / ${position}`);
670
+ return;
671
+ }
672
+ platformCfg.operationList = list;
673
+ logger_1.logger.step(`保存${TARGET_LABEL[target]}配置:${vo.fdName ?? vo.fdId}`);
674
+ await saveViewConfig(client, target, vo, config, formId);
675
+ const saved = target === 'list'
676
+ ? await view_1.listViewApi.get(client, vo.fdId)
677
+ : await view_1.formViewApi.get(client, vo.fdId);
678
+ const items = platformConfig(readViewConfig(saved, target), platform).operationList ?? [];
679
+ if (!items.some((item) => item.id === opId)) {
680
+ throw new Error('保存后校验失败:视图中未找到该操作');
681
+ }
682
+ logger_1.logger.success(`${change === 'added' ? '已挂载' : '已更新位置'}:${op.fdName} → ${TARGET_LABEL[target]}(${platform} / ${position})`);
683
+ logger_1.logger.info(logger_1.color.gray(`视图 ${vo.fdId},operationList 共 ${items.length} 个按钮`));
684
+ });
685
+ // ---------------------------------------------------------------- detach
686
+ operation
687
+ .command('detach')
688
+ .description('从视图中移除业务操作按钮(不删除操作定义)')
689
+ .requiredOption('--form-id <fdId>', '表单 id')
690
+ .requiredOption('--operation-id <fdId>', '业务操作 id')
691
+ .option('--target <target>', '目标视图:list | view', 'list')
692
+ .option('--view-id <fdId>', '视图 id(省略时自动定位)')
693
+ .option('--platform <platform>', 'desktop | mobile', 'desktop')
694
+ .option('--base-url <url>', '服务端基址(覆盖配置)')
695
+ .option('--token <token>', '鉴权 Token(覆盖配置)')
696
+ .action(async (opts) => {
697
+ const formId = String(opts.formId ?? '').trim();
698
+ const opId = String(opts.operationId ?? '').trim();
699
+ if (!formId)
700
+ throw new Error('--form-id 不能为空');
701
+ if (!opId)
702
+ throw new Error('--operation-id 不能为空');
703
+ const target = String(opts.target ?? 'list').trim().toLowerCase();
704
+ if (target !== 'list' && target !== 'view') {
705
+ throw new Error(`未知 --target:${opts.target}(可选 list | view)`);
706
+ }
707
+ const platform = normalizePlatform(opts.platform);
708
+ const client = createClient(opts);
709
+ const vo = target === 'list'
710
+ ? await loadListView(client, formId, opts.viewId)
711
+ : await loadFormView(client, formId, opts.viewId);
712
+ const config = readViewConfig(vo, target);
713
+ const platformCfg = platformConfig(config, platform);
714
+ const list = Array.isArray(platformCfg.operationList) ? platformCfg.operationList : [];
715
+ let removed = list.length - list.filter((item) => item.id !== opId).length;
716
+ platformCfg.operationList = list.filter((item) => item.id !== opId);
717
+ if (Array.isArray(platformCfg.shortcutOperation)) {
718
+ const shortcuts = platformCfg.shortcutOperation;
719
+ removed += shortcuts.length - shortcuts.filter((item) => item.id !== opId).length;
720
+ platformCfg.shortcutOperation = shortcuts.filter((item) => item.id !== opId);
721
+ }
722
+ if (!removed) {
723
+ logger_1.logger.warn(`该视图未挂载此操作:${TARGET_LABEL[target]} / ${platform}`);
724
+ return;
725
+ }
726
+ logger_1.logger.step(`保存${TARGET_LABEL[target]}配置:${vo.fdName ?? vo.fdId}`);
727
+ await saveViewConfig(client, target, vo, config, formId);
728
+ logger_1.logger.success(`已移除:${opId}(${TARGET_LABEL[target]} / ${platform})`);
729
+ });
730
+ }