lan-low-code-cli 0.0.0-stage → 2.0.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.
- package/README.md +813 -3
- package/bin/llc.js +13 -0
- package/dist/api/formDefine.js +161 -0
- package/dist/client.js +140 -0
- package/dist/commands/app.js +252 -0
- package/dist/commands/config.js +75 -0
- package/dist/commands/control.js +158 -0
- package/dist/commands/field.js +125 -0
- package/dist/commands/form.js +1294 -0
- package/dist/commands/locale.js +60 -0
- package/dist/config.js +106 -0
- package/dist/constants.js +275 -0
- package/dist/generator/index.js +1304 -0
- package/dist/index.js +136 -0
- package/dist/types.js +50 -0
- package/dist/utils/icon.js +26 -0
- package/dist/utils/locale.js +53 -0
- package/dist/utils/logger.js +78 -0
- package/dist/utils/random.js +79 -0
- package/dist/widgets.js +304 -0
- package/package.json +40 -3
|
@@ -0,0 +1,1294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerFormCommand = registerFormCommand;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const formDefine_1 = require("../api/formDefine");
|
|
7
|
+
const client_1 = require("../client");
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
const constants_1 = require("../constants");
|
|
10
|
+
const generator_1 = require("../generator");
|
|
11
|
+
const types_1 = require("../types");
|
|
12
|
+
const logger_1 = require("../utils/logger");
|
|
13
|
+
const icon_1 = require("../utils/icon");
|
|
14
|
+
const locale_1 = require("../utils/locale");
|
|
15
|
+
const random_1 = require("../utils/random");
|
|
16
|
+
const widgets_1 = require("../widgets");
|
|
17
|
+
function createClient(opts) {
|
|
18
|
+
return new client_1.LcodeClient((0, config_1.resolveRuntime)({ baseUrl: opts.baseUrl, token: opts.token }));
|
|
19
|
+
}
|
|
20
|
+
/** 控件元数据注册表(站点根不变时复用磁盘缓存) */
|
|
21
|
+
function createRegistry(opts) {
|
|
22
|
+
const runtime = (0, config_1.resolveRuntime)({ baseUrl: opts.baseUrl, token: opts.token });
|
|
23
|
+
return new widgets_1.WidgetRegistry({ baseUrl: runtime.baseUrl, token: runtime.token });
|
|
24
|
+
}
|
|
25
|
+
/** 解析表单类型别名 */
|
|
26
|
+
function parseFormType(input) {
|
|
27
|
+
if (input === undefined || input === '')
|
|
28
|
+
return types_1.EFormDefineType.General;
|
|
29
|
+
const numeric = Number(input);
|
|
30
|
+
if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= 6) {
|
|
31
|
+
return numeric;
|
|
32
|
+
}
|
|
33
|
+
const alias = types_1.FORM_TYPE_ALIAS[String(input).trim().toLowerCase()];
|
|
34
|
+
if (alias === undefined) {
|
|
35
|
+
const valid = Object.keys(types_1.FORM_TYPE_ALIAS).filter((k) => /^[a-z]+$/.test(k)).join(', ');
|
|
36
|
+
throw new Error(`未知的表单类型:${input},可选值:${valid}(或数字 1-6)`);
|
|
37
|
+
}
|
|
38
|
+
return alias;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 决定使用哪个接口族。
|
|
42
|
+
* - 显式指定 --api general/design/modeling 时以指定为准
|
|
43
|
+
* - auto:给出了 --app-id 走建模应用(modeling);否则审批意见表单(2)走 general,其余走 design
|
|
44
|
+
*/
|
|
45
|
+
function resolveApiFamily(type, requested, appId) {
|
|
46
|
+
if (requested === 'general' || requested === 'design' || requested === 'modeling') {
|
|
47
|
+
return requested;
|
|
48
|
+
}
|
|
49
|
+
if (requested && requested !== 'auto') {
|
|
50
|
+
throw new Error(`未知的 --api 取值:${requested},可选 auto | general | design | modeling`);
|
|
51
|
+
}
|
|
52
|
+
if (appId)
|
|
53
|
+
return 'modeling';
|
|
54
|
+
if (type === undefined || type === types_1.EFormDefineType.General)
|
|
55
|
+
return 'general';
|
|
56
|
+
return 'design';
|
|
57
|
+
}
|
|
58
|
+
const FAMILY_LABEL = {
|
|
59
|
+
general: 'generalFormDefine',
|
|
60
|
+
design: 'sysXFormDesign',
|
|
61
|
+
modeling: 'sysModelingXForm',
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* 解析 `--locale` / `--lang-file`。
|
|
65
|
+
*
|
|
66
|
+
* - 默认 `['Cn']`;`all` 展开为 `LOCALES`;第一个为主语言
|
|
67
|
+
* - 文案文件结构:`{ "Us": { "姓名": "Name" } }`,内层 key 可为标题或字段名
|
|
68
|
+
*/
|
|
69
|
+
function resolveI18n(opts) {
|
|
70
|
+
const raw = opts.locale ?? [];
|
|
71
|
+
const items = raw
|
|
72
|
+
.flatMap((item) => String(item).split(','))
|
|
73
|
+
.map((item) => item.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
const locale = [];
|
|
76
|
+
for (const item of items) {
|
|
77
|
+
const expanded = item.toLowerCase() === constants_1.LOCALE_ALL ? constants_1.LOCALES : [(0, locale_1.normalizeLocale)(item)];
|
|
78
|
+
for (const code of expanded) {
|
|
79
|
+
if (!locale.includes(code))
|
|
80
|
+
locale.push(code);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (locale.length === 0)
|
|
84
|
+
locale.push(constants_1.DEFAULT_LOCALE);
|
|
85
|
+
let messages;
|
|
86
|
+
const file = opts.langFile;
|
|
87
|
+
if (file) {
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)(file), 'utf8'));
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw new Error(`读取多语言文案文件失败:${file}(${error.message})`);
|
|
94
|
+
}
|
|
95
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
96
|
+
throw new Error(`多语言文案文件格式错误:${file},应为 { "Us": { "标题": "翻译" } }`);
|
|
97
|
+
}
|
|
98
|
+
// 语言码大小写归一到 getLocale 产物(Us/us → Us)
|
|
99
|
+
messages = {};
|
|
100
|
+
for (const [code, dict] of Object.entries(parsed)) {
|
|
101
|
+
if (!dict || typeof dict !== 'object' || Array.isArray(dict)) {
|
|
102
|
+
throw new Error(`多语言文案文件格式错误:${file} 中 "${code}" 应为对象`);
|
|
103
|
+
}
|
|
104
|
+
messages[(0, locale_1.normalizeLocale)(code)] = dict;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { locale, messages };
|
|
108
|
+
}
|
|
109
|
+
/** 控件动作的合法性:标识符形式(onChange / onFocus / …) */
|
|
110
|
+
const ACTION_EVENT_PATTERN = /^[A-Za-z_$][\w$]*$/;
|
|
111
|
+
/** 读取动作函数源码:`@js文件` 读文件,否则视为内联源码 */
|
|
112
|
+
function readActionSource(raw, context) {
|
|
113
|
+
const trimmed = raw.trim();
|
|
114
|
+
if (!trimmed.startsWith('@'))
|
|
115
|
+
return trimmed;
|
|
116
|
+
const file = (0, path_1.resolve)(trimmed.slice(1));
|
|
117
|
+
let source;
|
|
118
|
+
try {
|
|
119
|
+
source = (0, fs_1.readFileSync)(file, 'utf8');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new Error(`读取动作函数文件失败:${file}(${error.message}),位置:${context}`);
|
|
123
|
+
}
|
|
124
|
+
if (!source.trim()) {
|
|
125
|
+
throw new Error(`动作函数文件为空:${file},位置:${context}`);
|
|
126
|
+
}
|
|
127
|
+
return source;
|
|
128
|
+
}
|
|
129
|
+
/** 解析 `--action` 规格:`字段名:事件:函数源码` 或 `字段名:事件:@js文件`;全局动作用 `$global` */
|
|
130
|
+
function parseActionSpecs(specs) {
|
|
131
|
+
return specs.map((raw) => {
|
|
132
|
+
const matched = /^([^:]+):([^:]+):([\s\S]+)$/.exec(raw.trim());
|
|
133
|
+
if (!matched) {
|
|
134
|
+
throw new Error(`--action 格式错误:${raw}(应为 字段名:事件:函数源码 或 字段名:事件:@js文件;全局动作用 $global)`);
|
|
135
|
+
}
|
|
136
|
+
const [, target, event, sourceRaw] = matched;
|
|
137
|
+
if (!ACTION_EVENT_PATTERN.test(event)) {
|
|
138
|
+
throw new Error(`--action 非法事件名:${event}(应为 onChange / onFocus 这类标识符)`);
|
|
139
|
+
}
|
|
140
|
+
if (target === generator_1.ACTION_GLOBAL_TARGET && !generator_1.GLOBAL_ACTION_EVENTS.includes(event)) {
|
|
141
|
+
throw new Error(`全局动作仅支持:${generator_1.GLOBAL_ACTION_EVENTS.join(' / ')},收到 "${event}"`);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
target: target.trim(),
|
|
145
|
+
event,
|
|
146
|
+
source: readActionSource(sourceRaw, raw),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 解析 `--action-file`(JSON)。两种写法:
|
|
152
|
+
*
|
|
153
|
+
* ```json
|
|
154
|
+
* {
|
|
155
|
+
* "fd_col_x": { "onChange": "function onChange(value){ … }" },
|
|
156
|
+
* "$global": { "onLoad": "function onLoad(){ … }" }
|
|
157
|
+
* }
|
|
158
|
+
* ```
|
|
159
|
+
* 或数组形式 `[{ "target": "fd_col_x", "event": "onChange", "source": "…", "name": "可选" }]`
|
|
160
|
+
*/
|
|
161
|
+
function parseActionFile(file) {
|
|
162
|
+
if (!file)
|
|
163
|
+
return [];
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)(file), 'utf8'));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
throw new Error(`读取动作定义文件失败:${file}(${error.message})`);
|
|
170
|
+
}
|
|
171
|
+
const pushSource = (target, event, source, name) => {
|
|
172
|
+
if (!ACTION_EVENT_PATTERN.test(event)) {
|
|
173
|
+
throw new Error(`动作定义文件 ${file} 中非法事件名:${event}`);
|
|
174
|
+
}
|
|
175
|
+
if (typeof source !== 'string' || !source.trim()) {
|
|
176
|
+
throw new Error(`动作定义文件 ${file} 中 ${target}.${event} 的函数源码必须是非空字符串`);
|
|
177
|
+
}
|
|
178
|
+
if (target === generator_1.ACTION_GLOBAL_TARGET && !generator_1.GLOBAL_ACTION_EVENTS.includes(event)) {
|
|
179
|
+
throw new Error(`全局动作仅支持:${generator_1.GLOBAL_ACTION_EVENTS.join(' / ')},收到 "${event}"`);
|
|
180
|
+
}
|
|
181
|
+
return { target, event, source: source.trim(), ...(name ? { name } : {}) };
|
|
182
|
+
};
|
|
183
|
+
if (Array.isArray(parsed)) {
|
|
184
|
+
return parsed.map((item) => {
|
|
185
|
+
const spec = item;
|
|
186
|
+
if (typeof spec.target !== 'string' || typeof spec.event !== 'string') {
|
|
187
|
+
throw new Error(`动作定义文件 ${file} 数组项应为 { target, event, source, name? }`);
|
|
188
|
+
}
|
|
189
|
+
return pushSource(spec.target, spec.event, spec.source, spec.name);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
193
|
+
throw new Error(`动作定义文件 ${file} 格式错误,应为 { "字段名": { "onChange": "函数源码" }, "$global": { "onLoad": "…" } }`);
|
|
194
|
+
}
|
|
195
|
+
const result = [];
|
|
196
|
+
for (const [target, events] of Object.entries(parsed)) {
|
|
197
|
+
if (!events || typeof events !== 'object' || Array.isArray(events)) {
|
|
198
|
+
throw new Error(`动作定义文件 ${file} 中 "${target}" 应为 { 事件: 函数源码 } 对象`);
|
|
199
|
+
}
|
|
200
|
+
for (const [event, source] of Object.entries(events)) {
|
|
201
|
+
result.push(pushSource(target, event, source));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
/** 汇总 `--action` + `--action-file` */
|
|
207
|
+
function resolveActions(opts) {
|
|
208
|
+
const specs = parseActionSpecs(opts.action ?? []);
|
|
209
|
+
const fromFile = parseActionFile(opts.actionFile);
|
|
210
|
+
return [...specs, ...fromFile];
|
|
211
|
+
}
|
|
212
|
+
/** 加载已有表单的 formAttr / viewModel / lang(autofill 时用于合并保留) */
|
|
213
|
+
async function loadPrevDesign(client, family, formId) {
|
|
214
|
+
try {
|
|
215
|
+
const vo = family === 'modeling'
|
|
216
|
+
? await formDefine_1.modelingXFormApi.loadDesign(client, formId)
|
|
217
|
+
: await formDefine_1.formDefineApi.get(client, formId).catch(() => formDefine_1.designApi.load(client, formId));
|
|
218
|
+
const cfg = typeof vo.fdConfig === 'string' ? JSON.parse(vo.fdConfig) : vo.fdConfig;
|
|
219
|
+
const formAttr = cfg?.attribute?.formAttr;
|
|
220
|
+
const lang = cfg?.lang;
|
|
221
|
+
return {
|
|
222
|
+
prevFormAttr: typeof formAttr === 'string' ? formAttr : undefined,
|
|
223
|
+
prevViewModel: Array.isArray(cfg?.viewModel) &&
|
|
224
|
+
cfg.viewModel.length
|
|
225
|
+
? cfg.viewModel
|
|
226
|
+
: undefined,
|
|
227
|
+
prevLang: typeof lang === 'string'
|
|
228
|
+
? (() => {
|
|
229
|
+
try {
|
|
230
|
+
return JSON.parse(lang);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
})()
|
|
236
|
+
: undefined,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 发布前控件必填属性校验(对齐设计器)。
|
|
245
|
+
*
|
|
246
|
+
* 设计器发布时会执行 `dispatch.validates.validating()`,存在控件属性错误即阻断发布;
|
|
247
|
+
* CLI 在保存/发布前按同样规则校验,失败则中止(`--force` 可跳过)。
|
|
248
|
+
* 预览模式(--json / --dry-run / 未加 --apply)只提示不中止。
|
|
249
|
+
*/
|
|
250
|
+
function assertPropValidation(stats, opts, willSave) {
|
|
251
|
+
const errors = stats.validationErrors ?? [];
|
|
252
|
+
if (!errors.length)
|
|
253
|
+
return;
|
|
254
|
+
for (const message of errors)
|
|
255
|
+
logger_1.logger.error(message);
|
|
256
|
+
if (opts.force) {
|
|
257
|
+
logger_1.logger.warn(`已按 --force 跳过控件必填属性校验(${errors.length} 项)`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!willSave) {
|
|
261
|
+
logger_1.logger.warn(`控件必填属性校验失败(共 ${errors.length} 项);实际保存/发布将被中止。修正后重试,或加 --force 跳过校验`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
throw new Error(`控件必填属性校验失败(共 ${errors.length} 项),已中止保存/发布。请修正属性值后重试,或加 --force 跳过校验`);
|
|
265
|
+
}
|
|
266
|
+
/** 解析逗号分隔的字段名列表 */
|
|
267
|
+
function parseFieldList(input) {
|
|
268
|
+
return String(input ?? '')
|
|
269
|
+
.split(',')
|
|
270
|
+
.map((item) => item.trim())
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 解析 `--tabs`:`标题:字段1,字段2|标题2:字段3`。
|
|
275
|
+
* 未列入任何页签的字段由生成器自动归入末尾的「其它」页。
|
|
276
|
+
*/
|
|
277
|
+
function parseTabsSpec(input) {
|
|
278
|
+
const raw = String(input ?? '').trim();
|
|
279
|
+
if (!raw)
|
|
280
|
+
return undefined;
|
|
281
|
+
return raw
|
|
282
|
+
.split('|')
|
|
283
|
+
.map((part) => {
|
|
284
|
+
const index = part.indexOf(':');
|
|
285
|
+
return index < 0
|
|
286
|
+
? { title: part.trim(), fields: [] }
|
|
287
|
+
: { title: part.slice(0, index).trim(), fields: parseFieldList(part.slice(index + 1)) };
|
|
288
|
+
})
|
|
289
|
+
.filter((item) => item.title);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 解析 `--layout` / `--grid-columns` / `--tabs` / `--tab-layout`。
|
|
293
|
+
*
|
|
294
|
+
* `--layout` 未指定时返回 `layout: undefined`:
|
|
295
|
+
* - `create` 由生成器默认按 `rows` 平铺;
|
|
296
|
+
* - `autofill` 则**沿用已有视图**(保持设计器布局不动),显式传入 `--layout` 才重建。
|
|
297
|
+
*/
|
|
298
|
+
function resolveLayout(opts) {
|
|
299
|
+
const raw = String(opts.layout ?? '').trim().toLowerCase();
|
|
300
|
+
if (!raw) {
|
|
301
|
+
return {
|
|
302
|
+
layout: undefined,
|
|
303
|
+
gridColumns: opts.gridColumns === undefined ? undefined : Number(opts.gridColumns),
|
|
304
|
+
tabs: undefined,
|
|
305
|
+
tabLayout: undefined,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const allowed = ['rows', 'grid', 'tabs'];
|
|
309
|
+
if (!allowed.includes(raw)) {
|
|
310
|
+
throw new Error(`未知的布局模式:${opts.layout},可选值:${allowed.join(' | ')}`);
|
|
311
|
+
}
|
|
312
|
+
const layout = raw;
|
|
313
|
+
const tabs = parseTabsSpec(opts.tabs);
|
|
314
|
+
if (layout === 'tabs' && !tabs?.length) {
|
|
315
|
+
logger_1.logger.warn('布局为 tabs 但未提供 --tabs,全部字段将放入单个「其它」页');
|
|
316
|
+
}
|
|
317
|
+
let tabLayout;
|
|
318
|
+
if (opts.tabLayout !== undefined) {
|
|
319
|
+
const value = String(opts.tabLayout).trim().toLowerCase();
|
|
320
|
+
if (value !== 'rows' && value !== 'grid') {
|
|
321
|
+
throw new Error(`未知的 --tab-layout 取值:${opts.tabLayout},可选 rows | grid`);
|
|
322
|
+
}
|
|
323
|
+
tabLayout = value;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
layout,
|
|
327
|
+
gridColumns: opts.gridColumns === undefined ? undefined : Number(opts.gridColumns),
|
|
328
|
+
tabs,
|
|
329
|
+
tabLayout,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 解析 `--option`:`字段名=选项1,选项2:值2,...`(可重复,同一字段的选项会合并)。
|
|
334
|
+
* 未写 `:值` 时按序号 `1..n` 取值(与设计器默认一致)。
|
|
335
|
+
*/
|
|
336
|
+
function parseOptionSpecs(items) {
|
|
337
|
+
const result = {};
|
|
338
|
+
for (const raw of items) {
|
|
339
|
+
const eq = raw.indexOf('=');
|
|
340
|
+
if (eq < 0) {
|
|
341
|
+
throw new Error(`--option 格式错误:${raw},应为 "字段名=选项1,选项2:值2"`);
|
|
342
|
+
}
|
|
343
|
+
const field = raw.slice(0, eq).trim();
|
|
344
|
+
const list = raw
|
|
345
|
+
.slice(eq + 1)
|
|
346
|
+
.split(',')
|
|
347
|
+
.map((item) => item.trim())
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
.map((item, index) => {
|
|
350
|
+
const colon = item.indexOf(':');
|
|
351
|
+
return colon < 0
|
|
352
|
+
? { label: item, value: String(index + 1) }
|
|
353
|
+
: {
|
|
354
|
+
label: item.slice(0, colon).trim(),
|
|
355
|
+
value: item.slice(colon + 1).trim() || String(index + 1),
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
if (!field || list.length === 0) {
|
|
359
|
+
throw new Error(`--option 格式错误:${raw},字段名与选项都不能为空`);
|
|
360
|
+
}
|
|
361
|
+
result[field] = [...(result[field] ?? []), ...list];
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
/** 合并某个操作的字段权限 */
|
|
366
|
+
function mergePermissions(target, fields, patch) {
|
|
367
|
+
const result = { ...(target ?? {}) };
|
|
368
|
+
fields.forEach((name) => {
|
|
369
|
+
result[name] = { ...(result[name] ?? {}), ...patch };
|
|
370
|
+
});
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* 解析字段权限(必填 / 可见 / 可编辑)。
|
|
375
|
+
*
|
|
376
|
+
* 落库位置为 `auth.{add|edit|view}[表名].fields[字段名]`,
|
|
377
|
+
* 对应控件属性面板的「字段权限」。
|
|
378
|
+
*/
|
|
379
|
+
function resolvePermissions(opts) {
|
|
380
|
+
const result = {};
|
|
381
|
+
const required = parseFieldList(opts.required);
|
|
382
|
+
const readonly = parseFieldList(opts.readonly);
|
|
383
|
+
const hidden = parseFieldList(opts.hidden);
|
|
384
|
+
if (required.length) {
|
|
385
|
+
result.add = mergePermissions(result.add, required, { required: true });
|
|
386
|
+
result.edit = mergePermissions(result.edit, required, { required: true });
|
|
387
|
+
}
|
|
388
|
+
if (readonly.length) {
|
|
389
|
+
result.add = mergePermissions(result.add, readonly, { editable: false });
|
|
390
|
+
result.edit = mergePermissions(result.edit, readonly, { editable: false });
|
|
391
|
+
}
|
|
392
|
+
if (hidden.length) {
|
|
393
|
+
;
|
|
394
|
+
['add', 'edit', 'view'].forEach((op) => {
|
|
395
|
+
result[op] = mergePermissions(result[op], hidden, { visible: false });
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const file = opts.authFile;
|
|
399
|
+
if (file) {
|
|
400
|
+
let parsed;
|
|
401
|
+
try {
|
|
402
|
+
parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)(file), 'utf8'));
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
throw new Error(`读取权限文件失败:${file}(${error.message})`);
|
|
406
|
+
}
|
|
407
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
408
|
+
throw new Error(`权限文件格式错误:${file},应为 { "add": { "fd_col_a": { "required": true } } }`);
|
|
409
|
+
}
|
|
410
|
+
;
|
|
411
|
+
['add', 'edit', 'view'].forEach((op) => {
|
|
412
|
+
const dict = parsed[op];
|
|
413
|
+
if (!dict || typeof dict !== 'object' || Array.isArray(dict))
|
|
414
|
+
return;
|
|
415
|
+
const target = { ...(result[op] ?? {}) };
|
|
416
|
+
Object.entries(dict).forEach(([name, patch]) => {
|
|
417
|
+
target[name] = { ...(target[name] ?? {}), ...patch };
|
|
418
|
+
});
|
|
419
|
+
result[op] = target;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
/** 解析逗号分隔的人员列表 */
|
|
425
|
+
function parseIdList(input) {
|
|
426
|
+
if (!input)
|
|
427
|
+
return undefined;
|
|
428
|
+
return input
|
|
429
|
+
.split(',')
|
|
430
|
+
.map((s) => s.trim())
|
|
431
|
+
.filter(Boolean)
|
|
432
|
+
.map((fdId) => ({ fdId }));
|
|
433
|
+
}
|
|
434
|
+
/** 解析字段过滤列表 */
|
|
435
|
+
function parseFieldFilter(input) {
|
|
436
|
+
if (!input)
|
|
437
|
+
return undefined;
|
|
438
|
+
return input
|
|
439
|
+
.split(',')
|
|
440
|
+
.map((s) => s.trim())
|
|
441
|
+
.filter(Boolean);
|
|
442
|
+
}
|
|
443
|
+
function selectFields(models, fields) {
|
|
444
|
+
if (!fields || fields.length === 0)
|
|
445
|
+
return models;
|
|
446
|
+
const set = new Set(fields);
|
|
447
|
+
return models.map((model) => ({
|
|
448
|
+
...model,
|
|
449
|
+
fdFields: (model.fdFields ?? []).filter((f) => set.has(f.fdName) || set.has(f.fdLabel)),
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
function countFields(models, includeSystem = false) {
|
|
453
|
+
return models.reduce((sum, m) => sum + (m.fdFields ?? []).filter((f) => includeSystem || f.fdIsSystem !== true).length, 0);
|
|
454
|
+
}
|
|
455
|
+
/** commander 可重复选项收集器 */
|
|
456
|
+
function collect(value, previous) {
|
|
457
|
+
return previous.concat([value]);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 向主数据模型追加字段定义。
|
|
461
|
+
*
|
|
462
|
+
* 若 `--add-field` 指定的字段名**已存在**,则视为「调整该字段」:
|
|
463
|
+
* 覆盖其标题与控件类型(保留 fdId / fdOrder / 已有属性),否则追加为新字段。
|
|
464
|
+
* 新字段随 fdConfig.dataModel 一起保存,发布时由后端创建真实列。
|
|
465
|
+
*/
|
|
466
|
+
async function injectNewFields(models, specs, controlMap, registry) {
|
|
467
|
+
if (!specs || specs.length === 0)
|
|
468
|
+
return models;
|
|
469
|
+
const mainIndex = Math.max(0, models.findIndex((m) => m.fdType === 'main'));
|
|
470
|
+
const main = models[mainIndex];
|
|
471
|
+
const existing = new Map((main.fdFields ?? []).map((f) => [f.fdName, f]));
|
|
472
|
+
const added = [];
|
|
473
|
+
const patched = new Map();
|
|
474
|
+
for (const spec of specs) {
|
|
475
|
+
const parsed = (0, generator_1.parseFieldSpec)(spec);
|
|
476
|
+
const created = await (0, generator_1.createDataField)(parsed, { controlMap, registry });
|
|
477
|
+
const target = parsed.name ? existing.get(parsed.name) : undefined;
|
|
478
|
+
if (target && !patched.has(parsed.name) && !target.fdIsSystem) {
|
|
479
|
+
// 已有字段:只覆盖标题与控件类型,其余属性保留
|
|
480
|
+
patched.set(parsed.name, {
|
|
481
|
+
...target,
|
|
482
|
+
fdLabel: created.fdLabel || target.fdLabel,
|
|
483
|
+
fdType: created.fdType,
|
|
484
|
+
fdDataType: created.fdDataType,
|
|
485
|
+
fdDictType: created.fdDictType,
|
|
486
|
+
fdLength: created.fdLength || target.fdLength,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
added.push(created);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (patched.size === 0 && added.length === 0)
|
|
494
|
+
return models;
|
|
495
|
+
const fdFields = (main.fdFields ?? []).map((f) => patched.get(f.fdName) ?? f);
|
|
496
|
+
return models.map((m, i) => (i === mainIndex ? { ...m, fdFields: [...fdFields, ...added] } : m));
|
|
497
|
+
}
|
|
498
|
+
/** 解析 `--detail`:`明细表标题=字段1:标题1[:控件类型],字段2:标题2,...` */
|
|
499
|
+
function parseDetailSpecs(items) {
|
|
500
|
+
return items.map((raw) => {
|
|
501
|
+
const eq = raw.indexOf('=');
|
|
502
|
+
if (eq < 0) {
|
|
503
|
+
throw new Error(`--detail 格式错误:${raw},应为 "明细表标题=字段1:标题1,字段2:标题2"`);
|
|
504
|
+
}
|
|
505
|
+
const title = raw.slice(0, eq).trim();
|
|
506
|
+
const fields = raw
|
|
507
|
+
.slice(eq + 1)
|
|
508
|
+
.split(',')
|
|
509
|
+
.map((s) => s.trim())
|
|
510
|
+
.filter(Boolean);
|
|
511
|
+
if (!title || fields.length === 0) {
|
|
512
|
+
throw new Error(`--detail 格式错误:${raw},明细表标题与字段都不能为空`);
|
|
513
|
+
}
|
|
514
|
+
return { title, fields };
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* 构建明细表数据模型。
|
|
519
|
+
*
|
|
520
|
+
* 新建时向服务端取权威模板:`getSystemDefaultModel({fdType:'detail'})`(含 fdId 与
|
|
521
|
+
* `fd_id`/`fd_main_id`/`fd_order` 系统字段),表名由 `generateTableName` 生成;
|
|
522
|
+
* 若已存在同名明细表(按标题或表名匹配)则复用,实现「调整」而非重复创建。
|
|
523
|
+
*/
|
|
524
|
+
async function buildDetailModels(client, formId, formName, specs, controlMap, registry, existing = []) {
|
|
525
|
+
const result = [];
|
|
526
|
+
for (const spec of specs) {
|
|
527
|
+
const found = existing.find((m) => m.fdType === 'detail' && (m.fdName === spec.title || m.fdTableName === spec.title));
|
|
528
|
+
let template;
|
|
529
|
+
let tableName;
|
|
530
|
+
if (found) {
|
|
531
|
+
logger_1.logger.info(` 复用已有明细表:${found.fdName}(表名 ${found.fdTableName})`);
|
|
532
|
+
template = found;
|
|
533
|
+
tableName = found.fdTableName;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
template = await formDefine_1.dataModelApi.getSystemDefaultModel(client, { fdType: 'detail' });
|
|
537
|
+
tableName = await formDefine_1.dataModelApi.generateTableName(client, {
|
|
538
|
+
fdXForm: { fdId: formId },
|
|
539
|
+
fdType: 'detail',
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
const templateFields = template.fdFields ?? [];
|
|
543
|
+
const added = [];
|
|
544
|
+
for (const raw of spec.fields) {
|
|
545
|
+
const parsed = (0, generator_1.parseFieldSpec)(raw);
|
|
546
|
+
const created = await (0, generator_1.createDataField)(parsed, { controlMap, registry });
|
|
547
|
+
const exist = templateFields.find((f) => f.fdName === parsed.name && !f.fdIsSystem);
|
|
548
|
+
if (exist) {
|
|
549
|
+
// 已有字段:覆盖标题与控件类型
|
|
550
|
+
exist.fdLabel = created.fdLabel || exist.fdLabel;
|
|
551
|
+
exist.fdType = created.fdType;
|
|
552
|
+
exist.fdDataType = created.fdDataType;
|
|
553
|
+
exist.fdDictType = created.fdDictType;
|
|
554
|
+
exist.fdLength = created.fdLength || exist.fdLength;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
added.push(created);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const text = spec.title;
|
|
561
|
+
result.push({
|
|
562
|
+
...template,
|
|
563
|
+
fdType: 'detail',
|
|
564
|
+
fdName: text,
|
|
565
|
+
fdTableName: tableName,
|
|
566
|
+
fdTableNameAlias: tableName,
|
|
567
|
+
fdFields: [...templateFields, ...added],
|
|
568
|
+
fdXForm: { fdId: formId, fdName: formName },
|
|
569
|
+
fdOuterMapping: false,
|
|
570
|
+
needDelete: false,
|
|
571
|
+
deletePhysicalTable: false,
|
|
572
|
+
dynamicProps: {},
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
/** 公共的生成器选项 */
|
|
578
|
+
function generatorOptions(opts, formType, appId) {
|
|
579
|
+
const family = resolveApiFamily(formType, opts.api, appId);
|
|
580
|
+
return {
|
|
581
|
+
family,
|
|
582
|
+
mechanismEntityName: opts.mechanismEntityName ??
|
|
583
|
+
(family === 'general' ? constants_1.ENTITY_NAME : constants_1.MODELING_ENTITY_NAME),
|
|
584
|
+
mechanismEntityCode: opts.mechanismEntityCode ??
|
|
585
|
+
(family === 'general' ? constants_1.ENTITY_CODE : constants_1.MODELING_ENTITY_CODE),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/** 读取表单的 fdFormDefineType(general 族带,design 族可能不带) */
|
|
589
|
+
async function resolveFormFamily(client, fdId, requested) {
|
|
590
|
+
// modeling 族的表单先从建模接口读:能读到就是建模应用下的表单
|
|
591
|
+
if (requested !== 'design' && requested !== 'general') {
|
|
592
|
+
try {
|
|
593
|
+
const vo = await formDefine_1.modelingXFormApi.loadBase(client, fdId);
|
|
594
|
+
if (vo && (vo.fdId || vo.fdAppId)) {
|
|
595
|
+
return { family: 'modeling', vo };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
/* 忽略,回落到通用读取 */
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const vo = await formDefine_1.formDefineApi.get(client, fdId).catch(() => formDefine_1.designApi.load(client, fdId));
|
|
603
|
+
const family = resolveApiFamily(vo.fdFormDefineType ?? types_1.EFormDefineType.General, requested);
|
|
604
|
+
return { family, vo };
|
|
605
|
+
}
|
|
606
|
+
/** 加载字段来源数据模型 */
|
|
607
|
+
async function loadModels(client, formId, fromModel) {
|
|
608
|
+
if (fromModel) {
|
|
609
|
+
const model = await formDefine_1.dataModelApi.load(client, fromModel);
|
|
610
|
+
return [model];
|
|
611
|
+
}
|
|
612
|
+
return (await formDefine_1.dataModelApi.loadByFormId(client, formId)) ?? [];
|
|
613
|
+
}
|
|
614
|
+
function registerFormCommand(program) {
|
|
615
|
+
const form = program.command('form').description('表单相关操作(创建 / 查询 / 发布 / 自动完善字段)');
|
|
616
|
+
// ------------------------------------------------------------------ create
|
|
617
|
+
form
|
|
618
|
+
.command('create')
|
|
619
|
+
.description('创建表单,并(可选)根据数据模型自动完善字段配置')
|
|
620
|
+
.requiredOption('--name <name>', '表单名称')
|
|
621
|
+
.option('--code <code>', '表单业务编码,默认自动生成')
|
|
622
|
+
.option('--type <type>', '表单类型:common|general|config|modal|template|refer。缺省时 general 族用 general、其余用 common')
|
|
623
|
+
.option('--app-id <appId>', '表单所属应用 id(应用主键 fdId)。给出后默认走 modeling 族,写入 fdAppId')
|
|
624
|
+
.option('--fd-entity-id <id>', '业务建模对应 appId / 应用模块对应 templateId(默认取 --app-id)')
|
|
625
|
+
.option('--fd-entity-name <name>', '所属实体 model 名称,例如 com.xxx.YourEntity')
|
|
626
|
+
.option('--fd-entity-key <key>', '实体扩展键(预留)')
|
|
627
|
+
.option('--fd-run-entity-name <name>', '表单运行时实体名称')
|
|
628
|
+
.option('--table-name <name>', '主表名(不传则调用对应接口自动生成)')
|
|
629
|
+
.option('--api <family>', '接口族:auto(默认)| modeling | general | design')
|
|
630
|
+
.option('--group-id <fdGroupId>', '表单所属分组 id(modeling 族)')
|
|
631
|
+
.option('--edit-draft', '草稿状态允许编辑(modeling 族,默认 true)')
|
|
632
|
+
.option('--no-edit-draft', '草稿状态不允许编辑(modeling 族)')
|
|
633
|
+
.option('--reader-flag', '阅读者为空时所有人可阅读(modeling 族)')
|
|
634
|
+
.option('--data-source-id <id>', '外部数据源 id(modeling 族)')
|
|
635
|
+
.option('--outer-mapping', '是否映射外部表(modeling 族)')
|
|
636
|
+
.option('--source-form-id <fdId>', '引用源表单 id(type=refer 时必填)')
|
|
637
|
+
.option('--category <fdId>', '表单分类 id')
|
|
638
|
+
.option('--icon <icon>', '页面图标(图标名或图标 JSON),默认使用内置默认图标')
|
|
639
|
+
.option('--readers <ids>', '可阅读者,逗号分隔的组织/人员 id')
|
|
640
|
+
.option('--editors <ids>', '可编辑者,逗号分隔的组织/人员 id')
|
|
641
|
+
.option('--mechanism-entity-name <name>', '机制实体名(决定控件库),默认按接口族推导')
|
|
642
|
+
.option('--mechanism-entity-code <code>', '机制编码,默认按接口族推导')
|
|
643
|
+
.option('--from-model <fdId>', '从指定数据模型 id 拷贝字段(默认使用新表单自身的数据模型)')
|
|
644
|
+
.option('--fields <names>', '仅生成指定字段,逗号分隔(字段名或标题)')
|
|
645
|
+
.option('--add-field <spec>', '新增字段,格式 name:label[:dataType|controlType[:controlType]],可重复;例 "fd_text:单行文本"、"fd_amt:预算金额:moneytext"', collect, [])
|
|
646
|
+
.option('--option <spec>', '为下拉/单选/多选新建选项:"字段名=选项1,选项2:值2",可重复', collect, [])
|
|
647
|
+
.option('--detail <spec>', '新建/调整明细表:"明细表标题=字段1:标题1,字段2:标题2:控件类型",可重复', collect, [])
|
|
648
|
+
.option('--include-system', '同时把系统字段渲染到表单上(默认只渲染业务字段)')
|
|
649
|
+
.option('--per-row <n>', '每行控件数量', (v) => Number(v), 2)
|
|
650
|
+
.option('--span <n>', '控件栅格宽度', (v) => Number(v))
|
|
651
|
+
.option('--locale <locales>', '多语言,逗号分隔,第一个为主语言(默认 Cn);all=Cn,Us,Tw', collect, [])
|
|
652
|
+
.option('--lang-file <path>', '多语言文案文件(JSON),如 {"Us":{"姓名":"Name"}}')
|
|
653
|
+
.option('--layout <mode>', '渲染布局:rows(默认)| grid 栅格 | tabs 多页签')
|
|
654
|
+
.option('--grid-columns <n>', '栅格列数(--layout grid,默认取 --per-row)', (v) => Number(v))
|
|
655
|
+
.option('--tabs <spec>', '多页签定义:"标题:字段1,字段2|标题2:字段3"(--layout tabs)')
|
|
656
|
+
.option('--tab-layout <mode>', '页签内层布局:rows(默认)| grid 栅格')
|
|
657
|
+
.option('--required <fields>', '必填字段(逗号分隔,作用于 add/edit)')
|
|
658
|
+
.option('--readonly <fields>', '只读字段(逗号分隔,作用于 add/edit)')
|
|
659
|
+
.option('--hidden <fields>', '隐藏字段(逗号分隔,visible=false,作用于 add/edit/view)')
|
|
660
|
+
.option('--auth-file <path>', '精细字段权限(JSON),如 {"add":{"fd_col_a":{"required":true}}}')
|
|
661
|
+
.option('--action <spec>', '控件动作:"字段名:事件:函数源码或@js文件",可重复;全局动作用 $global(onLoad/onBeforeSubmit/onAfterSubmit)', collect, [])
|
|
662
|
+
.option('--action-file <path>', '控件动作定义文件(JSON),见 README「控件动作」章节')
|
|
663
|
+
.option('--force', '跳过控件必填属性校验(默认校验失败会中止保存/发布)')
|
|
664
|
+
.option('--no-config', '仅创建空表单,不生成字段配置')
|
|
665
|
+
.option('--publish', '创建后立即发布')
|
|
666
|
+
.option('--dry-run', '仅打印将要提交的数据,不实际请求')
|
|
667
|
+
.option('--base-url <url>', '服务端基址(覆盖配置)')
|
|
668
|
+
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
669
|
+
.option('--json', '以 JSON 输出结果')
|
|
670
|
+
.action(async (opts) => {
|
|
671
|
+
const client = createClient(opts);
|
|
672
|
+
const appId = opts.appId;
|
|
673
|
+
const explicitType = opts.type;
|
|
674
|
+
const parsedType = explicitType ? parseFormType(explicitType) : undefined;
|
|
675
|
+
const { family, mechanismEntityName, mechanismEntityCode } = generatorOptions(opts, parsedType, appId);
|
|
676
|
+
// 未显式指定类型时:审批意见表单(general) 用 2,其余(含建模应用表单)用 1(普通表单)
|
|
677
|
+
const formType = parsedType ?? (family === 'general' ? types_1.EFormDefineType.General : types_1.EFormDefineType.Common);
|
|
678
|
+
// 引用了通用模板的引用者表单必须给出源表单
|
|
679
|
+
if (formType === types_1.EFormDefineType.Refer && !opts.sourceFormId) {
|
|
680
|
+
throw new Error('表单类型为「引用者表单(refer)」时必须提供 --source-form-id');
|
|
681
|
+
}
|
|
682
|
+
// generalFormDefine 接口对表单类型有强校验
|
|
683
|
+
if (family === 'general' && formType !== types_1.EFormDefineType.General) {
|
|
684
|
+
throw new Error(`generalFormDefine 接口仅支持「审批意见表单(general)」。` +
|
|
685
|
+
`当前类型为 ${types_1.FORM_TYPE_LABEL[formType]},请使用 --api design(auto 模式下会自动切换)`);
|
|
686
|
+
}
|
|
687
|
+
// 应用下建表单必须指定应用
|
|
688
|
+
if (family === 'modeling' && !appId) {
|
|
689
|
+
throw new Error('modeling 接口族(应用下建表单)必须提供 --app-id');
|
|
690
|
+
}
|
|
691
|
+
const fdEntityId = opts.fdEntityId ?? appId;
|
|
692
|
+
// 各接口族通用的字段(均存在于基类 SysXFormDesignVO)
|
|
693
|
+
const baseCommon = stripUndefined({
|
|
694
|
+
fdName: opts.name,
|
|
695
|
+
fdCode: opts.code ?? (0, random_1.generateTemplateCode)(),
|
|
696
|
+
fdFormDefineType: formType,
|
|
697
|
+
fdIcon: (0, icon_1.normalizeIcon)(opts.icon),
|
|
698
|
+
fdSourceFormId: opts.sourceFormId,
|
|
699
|
+
appId,
|
|
700
|
+
fdEntityId,
|
|
701
|
+
fdEntityName: opts.fdEntityName,
|
|
702
|
+
fdEntityKey: opts.fdEntityKey,
|
|
703
|
+
fdRunEntityName: opts.fdRunEntityName,
|
|
704
|
+
});
|
|
705
|
+
// 仅 generalFormDefine 的 VO(SysGeneralFormDefineVO)支持的字段
|
|
706
|
+
const generalExtra = stripUndefined({
|
|
707
|
+
fdCategory: opts.category ? { fdId: opts.category } : undefined,
|
|
708
|
+
fdReaders: parseIdList(opts.readers),
|
|
709
|
+
fdEditors: parseIdList(opts.editors),
|
|
710
|
+
});
|
|
711
|
+
// 仅 sysModelingXForm 的 VO(SysModelingXFormDesignVO)支持的字段
|
|
712
|
+
const modelingExtra = stripUndefined({
|
|
713
|
+
fdAppId: appId,
|
|
714
|
+
fdGroupId: opts.groupId,
|
|
715
|
+
fdEditDraft: opts.editDraft !== false,
|
|
716
|
+
fdReaderFlag: Boolean(opts.readerFlag),
|
|
717
|
+
fdDataSourceId: opts.dataSourceId,
|
|
718
|
+
fdOuterMapping: Boolean(opts.outerMapping),
|
|
719
|
+
});
|
|
720
|
+
logger_1.logger.info(logger_1.color.gray(`接口族:${FAMILY_LABEL[family]} ` +
|
|
721
|
+
`类型:${types_1.FORM_TYPE_LABEL[formType]} 应用:${appId ?? '(未指定)'}`));
|
|
722
|
+
let fdId;
|
|
723
|
+
let base;
|
|
724
|
+
if (family === 'modeling') {
|
|
725
|
+
// ---------- 0. 应用下建表单:sysModelingXForm/create ----------
|
|
726
|
+
let tableName = opts.tableName;
|
|
727
|
+
if (!tableName) {
|
|
728
|
+
logger_1.logger.step('生成主表名(sysModelingXForm/generateTableName)');
|
|
729
|
+
try {
|
|
730
|
+
tableName = await formDefine_1.modelingXFormApi.generateTableName(client);
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
logger_1.logger.warn(`生成表名失败,需自行指定 --table-name:${err.message}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
base = {
|
|
737
|
+
...baseCommon,
|
|
738
|
+
...modelingExtra,
|
|
739
|
+
fdTableName: tableName,
|
|
740
|
+
fdStatus: 'draft',
|
|
741
|
+
mechanisms: { load: '*' },
|
|
742
|
+
};
|
|
743
|
+
if (opts.dryRun) {
|
|
744
|
+
logger_1.logger.title('【dry-run】将提交(sysModelingXForm/create)');
|
|
745
|
+
logger_1.logger.json(base);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
logger_1.logger.step(`创建表单:${base.fdName}`);
|
|
749
|
+
const created = await formDefine_1.modelingXFormApi.create(client, base);
|
|
750
|
+
fdId = created?.fdId ?? created;
|
|
751
|
+
}
|
|
752
|
+
else if (family === 'general') {
|
|
753
|
+
// ---------- A. 审批意见表单:init -> add ----------
|
|
754
|
+
logger_1.logger.step('获取表单默认配置(generalFormDefine/init)');
|
|
755
|
+
let defaults = {};
|
|
756
|
+
try {
|
|
757
|
+
defaults = (await formDefine_1.formDefineApi.init(client, {
|
|
758
|
+
mechanisms: { load: '*' },
|
|
759
|
+
appId,
|
|
760
|
+
fdEntityId,
|
|
761
|
+
})) ?? {};
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
logger_1.logger.warn(`init 调用失败,将仅使用用户输入:${err.message}`);
|
|
765
|
+
}
|
|
766
|
+
base = {
|
|
767
|
+
...defaults,
|
|
768
|
+
...baseCommon,
|
|
769
|
+
fdCode: baseCommon.fdCode ?? defaults.fdCode,
|
|
770
|
+
fdCategory: generalExtra.fdCategory ?? defaults.fdCategory,
|
|
771
|
+
fdReaders: generalExtra.fdReaders ?? defaults.fdReaders,
|
|
772
|
+
fdEditors: generalExtra.fdEditors ?? defaults.fdEditors,
|
|
773
|
+
fdIcon: baseCommon.fdIcon ?? defaults.fdIcon,
|
|
774
|
+
// --app-id 显式指定时覆盖 init 给的 fdEntityId
|
|
775
|
+
fdEntityId: fdEntityId ?? defaults.fdEntityId,
|
|
776
|
+
};
|
|
777
|
+
if (opts.dryRun) {
|
|
778
|
+
logger_1.logger.title('【dry-run】将提交(generalFormDefine/add)');
|
|
779
|
+
logger_1.logger.json(base);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
logger_1.logger.step(`创建表单:${base.fdName}`);
|
|
783
|
+
const created = await formDefine_1.formDefineApi.add(client, base);
|
|
784
|
+
fdId = created?.fdId ?? created;
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
// ---------- B. 其余类型:sysXFormDesign/create ----------
|
|
788
|
+
let tableName = opts.tableName;
|
|
789
|
+
if (!tableName && !opts.dryRun) {
|
|
790
|
+
try {
|
|
791
|
+
tableName = await formDefine_1.dataModelApi.generateTableName(client, { fdType: 'main' });
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
logger_1.logger.warn(`生成表名失败:${err.message}`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
base = {
|
|
798
|
+
...baseCommon,
|
|
799
|
+
fdTableName: tableName,
|
|
800
|
+
fdStatus: 'draft',
|
|
801
|
+
mechanisms: { load: '*' },
|
|
802
|
+
};
|
|
803
|
+
if (opts.dryRun) {
|
|
804
|
+
logger_1.logger.title('【dry-run】将提交(sysXFormDesign/create)');
|
|
805
|
+
logger_1.logger.json(base);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
logger_1.logger.step(`创建表单:${base.fdName}`);
|
|
809
|
+
const created = await formDefine_1.designApi.create(client, base);
|
|
810
|
+
fdId = created?.fdId ?? created;
|
|
811
|
+
}
|
|
812
|
+
logger_1.logger.success(`表单创建成功,fdId = ${fdId}`);
|
|
813
|
+
const skipConfig = opts.config === false || formType === types_1.EFormDefineType.Refer;
|
|
814
|
+
if (skipConfig) {
|
|
815
|
+
if (formType === types_1.EFormDefineType.Refer) {
|
|
816
|
+
logger_1.logger.warn('引用者表单自身不保存 fdConfig,将直接使用源表单配置');
|
|
817
|
+
}
|
|
818
|
+
if (opts.json)
|
|
819
|
+
logger_1.logger.json({ fdId, apiFamily: family });
|
|
820
|
+
if (opts.publish)
|
|
821
|
+
await doPublish(client, family, fdId, base);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
// ---------- 生成字段配置 ----------
|
|
825
|
+
logger_1.logger.step('加载数据模型与可用字段');
|
|
826
|
+
const models = await loadModels(client, fdId, opts.fromModel);
|
|
827
|
+
if (models.length === 0) {
|
|
828
|
+
logger_1.logger.warn(`未获取到数据模型,跳过字段生成。可稍后执行 \`llc form autofill --id ${fdId}\``);
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
const controlMap = (0, config_1.resolveControlMap)();
|
|
832
|
+
const registry = createRegistry(opts);
|
|
833
|
+
const i18n = resolveI18n(opts);
|
|
834
|
+
if (i18n.locale.length > 1) {
|
|
835
|
+
logger_1.logger.info(` 多语言:${i18n.locale.join('、')}(主语言 ${i18n.locale[0]})`);
|
|
836
|
+
}
|
|
837
|
+
const layoutOptions = resolveLayout(opts);
|
|
838
|
+
const permissions = resolvePermissions(opts);
|
|
839
|
+
const optionSpecs = parseOptionSpecs(opts.option ?? []);
|
|
840
|
+
const actionSpecs = resolveActions(opts);
|
|
841
|
+
if (actionSpecs.length) {
|
|
842
|
+
logger_1.logger.info(` 控件动作:${actionSpecs.length} 个`);
|
|
843
|
+
}
|
|
844
|
+
if (layoutOptions.layout && layoutOptions.layout !== 'rows') {
|
|
845
|
+
logger_1.logger.info(` 布局:${layoutOptions.layout}`);
|
|
846
|
+
}
|
|
847
|
+
const filtered = selectFields(models, parseFieldFilter(opts.fields));
|
|
848
|
+
const withNew = await injectNewFields(filtered, opts.addField, controlMap, registry);
|
|
849
|
+
// 明细表:新建或调整已有明细表
|
|
850
|
+
const detailSpecs = parseDetailSpecs(opts.detail ?? []);
|
|
851
|
+
let withDetail = withNew;
|
|
852
|
+
if (detailSpecs.length) {
|
|
853
|
+
const details = await buildDetailModels(client, fdId, base.fdName, detailSpecs, controlMap, registry, withNew);
|
|
854
|
+
withDetail = [...withNew, ...details];
|
|
855
|
+
logger_1.logger.info(` 明细表:${details.length} 个`);
|
|
856
|
+
}
|
|
857
|
+
if (opts.addField?.length) {
|
|
858
|
+
logger_1.logger.info(` 新增字段:${opts.addField.length} 个(${opts.addField.join('、')})`);
|
|
859
|
+
}
|
|
860
|
+
logger_1.logger.info(` 表单字段:${countFields(withDetail, opts.includeSystem)} 个(含明细 ${withDetail.filter((m) => m.fdType === 'detail').length} 张)`);
|
|
861
|
+
const stats = { optionsCompleted: 0, actionsCompleted: 0 };
|
|
862
|
+
const payload = await (0, generator_1.buildFormDefinePayload)(withDetail, {
|
|
863
|
+
base: { ...base, fdId },
|
|
864
|
+
formId: fdId,
|
|
865
|
+
formName: base.fdName,
|
|
866
|
+
formDefineType: formType,
|
|
867
|
+
entityName: mechanismEntityName,
|
|
868
|
+
entityCode: mechanismEntityCode,
|
|
869
|
+
includeSystem: Boolean(opts.includeSystem),
|
|
870
|
+
perRow: opts.perRow,
|
|
871
|
+
span: opts.span,
|
|
872
|
+
controlMap,
|
|
873
|
+
registry,
|
|
874
|
+
stats,
|
|
875
|
+
...i18n,
|
|
876
|
+
...layoutOptions,
|
|
877
|
+
permissions,
|
|
878
|
+
options: optionSpecs,
|
|
879
|
+
actions: actionSpecs,
|
|
880
|
+
});
|
|
881
|
+
if (stats.optionsCompleted) {
|
|
882
|
+
logger_1.logger.info(` 完善选项多语言:${stats.optionsCompleted} 个`);
|
|
883
|
+
}
|
|
884
|
+
if (stats.actionsCompleted) {
|
|
885
|
+
logger_1.logger.info(` 配置控件动作:${stats.actionsCompleted} 个`);
|
|
886
|
+
}
|
|
887
|
+
for (const warning of stats.actionWarnings ?? []) {
|
|
888
|
+
logger_1.logger.warn(warning);
|
|
889
|
+
}
|
|
890
|
+
assertPropValidation(stats, opts, true);
|
|
891
|
+
if (family === 'modeling') {
|
|
892
|
+
if (opts.publish) {
|
|
893
|
+
logger_1.logger.step('保存并发布表单(sysModelingXForm/saveDesignAndPublish)');
|
|
894
|
+
await formDefine_1.modelingXFormApi.saveDesignAndPublish(client, payload);
|
|
895
|
+
logger_1.logger.success('表单字段配置已保存并发布');
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
logger_1.logger.step('保存表单字段配置(sysModelingXForm/saveDesign)');
|
|
899
|
+
await formDefine_1.modelingXFormApi.saveDesign(client, payload);
|
|
900
|
+
logger_1.logger.success('表单字段配置已保存');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
logger_1.logger.step(`保存表单字段配置(${FAMILY_LABEL[family]}/save)`);
|
|
905
|
+
if (family === 'general') {
|
|
906
|
+
await formDefine_1.formDefineApi.save(client, payload);
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
await formDefine_1.designApi.save(client, payload);
|
|
910
|
+
}
|
|
911
|
+
logger_1.logger.success('表单字段配置已保存');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// modeling 族若已通过 saveDesignAndPublish 发布,则不再重复发布
|
|
915
|
+
const publishedAlready = family === 'modeling' && opts.publish && models.length > 0;
|
|
916
|
+
if (opts.publish && !publishedAlready)
|
|
917
|
+
await doPublish(client, family, fdId, base);
|
|
918
|
+
if (opts.json) {
|
|
919
|
+
logger_1.logger.json({ fdId, name: base.fdName, formDefineType: formType, appId, apiFamily: family });
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
logger_1.logger.info('');
|
|
923
|
+
logger_1.logger.success(`完成。表单 id:${fdId}`);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
// -------------------------------------------------------------------- list
|
|
927
|
+
form
|
|
928
|
+
.command('list')
|
|
929
|
+
.description('查询表单列表(generalFormDefine/list,即审批意见表单)')
|
|
930
|
+
.option('--keyword <keyword>', '按名称模糊搜索')
|
|
931
|
+
.option('--page-no <n>', '页码', (v) => Number(v), 1)
|
|
932
|
+
.option('--page-size <n>', '每页数量', (v) => Number(v), 20)
|
|
933
|
+
.option('--base-url <url>', '服务端基址(覆盖配置)')
|
|
934
|
+
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
935
|
+
.option('--json', '以 JSON 输出')
|
|
936
|
+
.action(async (opts) => {
|
|
937
|
+
const client = createClient(opts);
|
|
938
|
+
const conditions = opts.keyword
|
|
939
|
+
? { fdName: { $contains: String(opts.keyword).trim() } }
|
|
940
|
+
: undefined;
|
|
941
|
+
const result = await formDefine_1.formDefineApi.list(client, {
|
|
942
|
+
pageNo: opts.pageNo,
|
|
943
|
+
pageSize: opts.pageSize,
|
|
944
|
+
conditions,
|
|
945
|
+
sorts: { fdCreateTime: 'DESC' },
|
|
946
|
+
columns: ['fdId', 'fdName', 'fdCode', 'fdFormDefineType', 'fdStatus', 'fdCreateTime'],
|
|
947
|
+
});
|
|
948
|
+
if (opts.json) {
|
|
949
|
+
logger_1.logger.json(result);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const content = result?.content ?? [];
|
|
953
|
+
logger_1.logger.table(content.map((item) => ({
|
|
954
|
+
fdId: item.fdId ?? '',
|
|
955
|
+
名称: item.fdName ?? '',
|
|
956
|
+
编码: item.fdCode ?? '',
|
|
957
|
+
类型: item.fdFormDefineType
|
|
958
|
+
? types_1.FORM_TYPE_LABEL[item.fdFormDefineType]
|
|
959
|
+
: '',
|
|
960
|
+
状态: item.fdStatus ?? '',
|
|
961
|
+
})));
|
|
962
|
+
logger_1.logger.info(logger_1.color.gray(`共 ${content.length} 条`));
|
|
963
|
+
});
|
|
964
|
+
// --------------------------------------------------------------------- get
|
|
965
|
+
form
|
|
966
|
+
.command('get')
|
|
967
|
+
.description('查看表单详情')
|
|
968
|
+
.requiredOption('--id <fdId>', '表单 id')
|
|
969
|
+
.option('--with-config', '同时解析并展示 fdConfig 概要')
|
|
970
|
+
.option('--base-url <url>', '服务端基址(覆盖配置)')
|
|
971
|
+
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
972
|
+
.option('--json', '以 JSON 输出')
|
|
973
|
+
.action(async (opts) => {
|
|
974
|
+
const client = createClient(opts);
|
|
975
|
+
let vo;
|
|
976
|
+
// 依次尝试 建模应用接口 -> 审批意见表单接口 -> 表单引擎接口
|
|
977
|
+
try {
|
|
978
|
+
vo = await formDefine_1.modelingXFormApi.loadBase(client, opts.id);
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
try {
|
|
982
|
+
vo = await formDefine_1.formDefineApi.get(client, opts.id, { mechanisms: { load: '*' } });
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
vo = await formDefine_1.designApi.load(client, opts.id);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (opts.json || !opts.withConfig) {
|
|
989
|
+
logger_1.logger.json(vo);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
logger_1.logger.json(vo);
|
|
993
|
+
try {
|
|
994
|
+
const fdConfig = typeof vo.fdConfig === 'string' ? JSON.parse(vo.fdConfig) : vo.fdConfig;
|
|
995
|
+
const viewModel = fdConfig?.viewModel ?? [];
|
|
996
|
+
const dataModel = fdConfig?.dataModel ?? [];
|
|
997
|
+
logger_1.logger.title('\nfdConfig 概要');
|
|
998
|
+
logger_1.logger.table(dataModel.map((m) => ({
|
|
999
|
+
表: m.fdName,
|
|
1000
|
+
表名: m.fdTableName,
|
|
1001
|
+
类型: m.fdType,
|
|
1002
|
+
字段数: m.fdFields?.length ?? 0,
|
|
1003
|
+
})));
|
|
1004
|
+
logger_1.logger.info(logger_1.color.gray(`场景数:${viewModel.length}`));
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
logger_1.logger.warn('fdConfig 解析失败,已原样输出');
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
// ----------------------------------------------------------------- publish
|
|
1011
|
+
form
|
|
1012
|
+
.command('publish')
|
|
1013
|
+
.description('发布表单')
|
|
1014
|
+
.requiredOption('--id <fdId>', '表单 id')
|
|
1015
|
+
.option('--api <family>', '接口族:auto(默认)| modeling | general | design')
|
|
1016
|
+
.option('--desc <desc>', '发布说明', '')
|
|
1017
|
+
.option('--base-url <url>', '服务端基址(覆盖配置)')
|
|
1018
|
+
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
1019
|
+
.action(async (opts) => {
|
|
1020
|
+
const client = createClient(opts);
|
|
1021
|
+
const { family, vo } = await resolveFormFamily(client, opts.id, opts.api);
|
|
1022
|
+
await doPublish(client, family, opts.id, vo, opts.desc);
|
|
1023
|
+
logger_1.logger.success(`表单 ${opts.id} 已发布`);
|
|
1024
|
+
});
|
|
1025
|
+
// ---------------------------------------------------------------- autofill
|
|
1026
|
+
form
|
|
1027
|
+
.command('autofill')
|
|
1028
|
+
.description('自动完善表单字段:依据数据模型重建字段配置并保存')
|
|
1029
|
+
.requiredOption('--id <fdId>', '表单 id')
|
|
1030
|
+
.option('--from-model <fdId>', '使用指定数据模型 id 的字段(默认使用表单自身数据模型)')
|
|
1031
|
+
.option('--fields <names>', '仅处理指定字段,逗号分隔')
|
|
1032
|
+
.option('--add-field <spec>', '新增字段,格式 name:label[:dataType|controlType[:controlType]],可重复', collect, [])
|
|
1033
|
+
.option('--option <spec>', '为下拉/单选/多选新建选项:"字段名=选项1,选项2:值2",可重复', collect, [])
|
|
1034
|
+
.option('--detail <spec>', '新建/调整明细表:"明细表标题=字段1:标题1,字段2:标题2:控件类型",可重复', collect, [])
|
|
1035
|
+
.option('--include-system', '同时把系统字段渲染到表单上(默认只渲染业务字段)')
|
|
1036
|
+
.option('--icon <icon>', '页面图标(图标名或图标 JSON);缺失时会自动补默认图标')
|
|
1037
|
+
.option('--per-row <n>', '每行控件数量', (v) => Number(v), 2)
|
|
1038
|
+
.option('--span <n>', '控件栅格宽度', (v) => Number(v))
|
|
1039
|
+
.option('--locale <locales>', '多语言,逗号分隔,第一个为主语言(默认 Cn);all=Cn,Us,Tw', collect, [])
|
|
1040
|
+
.option('--lang-file <path>', '多语言文案文件(JSON),如 {"Us":{"姓名":"Name"}}')
|
|
1041
|
+
.option('--layout <mode>', '渲染布局:rows(默认)| grid 栅格 | tabs 多页签')
|
|
1042
|
+
.option('--grid-columns <n>', '栅格列数(--layout grid,默认取 --per-row)', (v) => Number(v))
|
|
1043
|
+
.option('--tabs <spec>', '多页签定义:"标题:字段1,字段2|标题2:字段3"(--layout tabs)')
|
|
1044
|
+
.option('--tab-layout <mode>', '页签内层布局:rows(默认)| grid 栅格')
|
|
1045
|
+
.option('--required <fields>', '必填字段(逗号分隔,作用于 add/edit)')
|
|
1046
|
+
.option('--readonly <fields>', '只读字段(逗号分隔,作用于 add/edit)')
|
|
1047
|
+
.option('--hidden <fields>', '隐藏字段(逗号分隔,visible=false,作用于 add/edit/view)')
|
|
1048
|
+
.option('--auth-file <path>', '精细字段权限(JSON),如 {"add":{"fd_col_a":{"required":true}}}')
|
|
1049
|
+
.option('--action <spec>', '控件动作:"字段名:事件:函数源码或@js文件",可重复;全局动作用 $global(onLoad/onBeforeSubmit/onAfterSubmit)', collect, [])
|
|
1050
|
+
.option('--action-file <path>', '控件动作定义文件(JSON),见 README「控件动作」章节')
|
|
1051
|
+
.option('--force', '跳过控件必填属性校验(默认校验失败会中止保存/发布)')
|
|
1052
|
+
.option('--mechanism-entity-name <name>', '机制实体名(决定控件库)')
|
|
1053
|
+
.option('--mechanism-entity-code <code>', '机制编码')
|
|
1054
|
+
.option('--apply', '确认写入(不加该参数仅预览生成结果)')
|
|
1055
|
+
.option('--publish', '写入后同时发布(保持发布态,避免掉回草稿)')
|
|
1056
|
+
.option('--base-url <url>', '服务端基址(覆盖配置)')
|
|
1057
|
+
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
1058
|
+
.option('--json', '以 JSON 输出生成结果')
|
|
1059
|
+
.action(async (opts) => {
|
|
1060
|
+
const client = createClient(opts);
|
|
1061
|
+
logger_1.logger.step(`加载表单:${opts.id}`);
|
|
1062
|
+
const { family, vo } = await resolveFormFamily(client, opts.id);
|
|
1063
|
+
/** 保存前的状态:saveDesign 会置为草稿态,用于提示/重新发布 */
|
|
1064
|
+
const fdStatusBefore = vo.fdStatus;
|
|
1065
|
+
// 图标缺失会让 sys-modeling 编辑器进入时报 "undefined" is not valid JSON(必须走 saveBase 写镜像表)
|
|
1066
|
+
const currentIcon = vo.fdIcon;
|
|
1067
|
+
if (family === 'modeling' && (!currentIcon || opts.icon)) {
|
|
1068
|
+
const icon = (0, icon_1.normalizeIcon)(opts.icon);
|
|
1069
|
+
if (icon !== currentIcon) {
|
|
1070
|
+
logger_1.logger.step('更新表单图标(sysModelingXForm/saveBase)');
|
|
1071
|
+
await formDefine_1.modelingXFormApi.saveBase(client, {
|
|
1072
|
+
fdId: opts.id,
|
|
1073
|
+
fdName: vo.fdName,
|
|
1074
|
+
fdCode: vo.fdCode,
|
|
1075
|
+
fdIcon: icon,
|
|
1076
|
+
});
|
|
1077
|
+
vo.fdIcon = icon;
|
|
1078
|
+
logger_1.logger.success('表单图标已更新');
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const formType = vo.fdFormDefineType ?? types_1.EFormDefineType.General;
|
|
1082
|
+
const mechanismEntityName = opts.mechanismEntityName ??
|
|
1083
|
+
(family === 'general' ? constants_1.ENTITY_NAME : constants_1.MODELING_ENTITY_NAME);
|
|
1084
|
+
const mechanismEntityCode = opts.mechanismEntityCode ??
|
|
1085
|
+
(family === 'general' ? constants_1.ENTITY_CODE : constants_1.MODELING_ENTITY_CODE);
|
|
1086
|
+
logger_1.logger.step('加载数据模型字段');
|
|
1087
|
+
const models = await loadModels(client, opts.id, opts.fromModel);
|
|
1088
|
+
if (models.length === 0) {
|
|
1089
|
+
throw new Error('未获取到数据模型字段,无法自动完善');
|
|
1090
|
+
}
|
|
1091
|
+
const controlMap = (0, config_1.resolveControlMap)();
|
|
1092
|
+
const registry = createRegistry(opts);
|
|
1093
|
+
const i18n = resolveI18n(opts);
|
|
1094
|
+
if (i18n.locale.length > 1) {
|
|
1095
|
+
logger_1.logger.info(` 多语言:${i18n.locale.join('、')}(主语言 ${i18n.locale[0]})`);
|
|
1096
|
+
}
|
|
1097
|
+
const layoutOptions = resolveLayout(opts);
|
|
1098
|
+
const permissions = resolvePermissions(opts);
|
|
1099
|
+
const optionSpecs = parseOptionSpecs(opts.option ?? []);
|
|
1100
|
+
if (layoutOptions.layout && layoutOptions.layout !== 'rows') {
|
|
1101
|
+
logger_1.logger.info(` 布局:${layoutOptions.layout}`);
|
|
1102
|
+
}
|
|
1103
|
+
const filtered = selectFields(models, parseFieldFilter(opts.fields));
|
|
1104
|
+
const withNew = await injectNewFields(filtered, opts.addField, controlMap, registry);
|
|
1105
|
+
// 明细表:新建或调整已有明细表
|
|
1106
|
+
const detailSpecs = parseDetailSpecs(opts.detail ?? []);
|
|
1107
|
+
let withDetail = withNew;
|
|
1108
|
+
if (detailSpecs.length) {
|
|
1109
|
+
const details = await buildDetailModels(client, opts.id, vo.fdName, detailSpecs, controlMap, registry, withNew);
|
|
1110
|
+
withDetail = [...withNew, ...details];
|
|
1111
|
+
logger_1.logger.info(` 明细表:${details.length} 个(新建 ${details.filter((d) => !withNew.some((m) => m.fdId === d.fdId)).length},复用 ${details.filter((d) => withNew.some((m) => m.fdId === d.fdId)).length})`);
|
|
1112
|
+
}
|
|
1113
|
+
const actionSpecs = resolveActions(opts);
|
|
1114
|
+
// 已有设计:保留控件动作/表单规则;--layout 未指定时沿用已有视图(布局不动)
|
|
1115
|
+
const prev = await loadPrevDesign(client, family, opts.id);
|
|
1116
|
+
if (actionSpecs.length) {
|
|
1117
|
+
logger_1.logger.info(` 控件动作:${actionSpecs.length} 个`);
|
|
1118
|
+
}
|
|
1119
|
+
const stats = { optionsCompleted: 0, actionsCompleted: 0 };
|
|
1120
|
+
const payload = await (0, generator_1.buildFormDefinePayload)(withDetail, {
|
|
1121
|
+
base: {
|
|
1122
|
+
...vo,
|
|
1123
|
+
fdId: opts.id,
|
|
1124
|
+
// 图标缺失会导致 sys-modeling 编辑器进入时报 "undefined" is not valid JSON
|
|
1125
|
+
fdIcon: vo.fdIcon || (0, icon_1.normalizeIcon)(undefined),
|
|
1126
|
+
},
|
|
1127
|
+
formId: opts.id,
|
|
1128
|
+
formName: vo.fdName,
|
|
1129
|
+
formDefineType: formType,
|
|
1130
|
+
entityName: mechanismEntityName,
|
|
1131
|
+
entityCode: mechanismEntityCode,
|
|
1132
|
+
includeSystem: Boolean(opts.includeSystem),
|
|
1133
|
+
perRow: opts.perRow,
|
|
1134
|
+
span: opts.span,
|
|
1135
|
+
registry,
|
|
1136
|
+
controlMap: (0, config_1.resolveControlMap)(),
|
|
1137
|
+
stats,
|
|
1138
|
+
...i18n,
|
|
1139
|
+
...layoutOptions,
|
|
1140
|
+
permissions,
|
|
1141
|
+
options: optionSpecs,
|
|
1142
|
+
actions: actionSpecs,
|
|
1143
|
+
prevFormAttr: prev.prevFormAttr,
|
|
1144
|
+
prevViewModel: prev.prevViewModel,
|
|
1145
|
+
prevLang: prev.prevLang,
|
|
1146
|
+
});
|
|
1147
|
+
logger_1.logger.info(` 使用字段:${countFields(withNew, opts.includeSystem)} 个(接口族:${family})`);
|
|
1148
|
+
if (stats.optionsCompleted) {
|
|
1149
|
+
logger_1.logger.info(` 完善选项多语言:${stats.optionsCompleted} 个`);
|
|
1150
|
+
}
|
|
1151
|
+
if (stats.actionsCompleted) {
|
|
1152
|
+
logger_1.logger.info(` 配置控件动作:${stats.actionsCompleted} 个`);
|
|
1153
|
+
}
|
|
1154
|
+
for (const warning of [...(stats.actionWarnings ?? []), ...(stats.layoutWarnings ?? [])]) {
|
|
1155
|
+
logger_1.logger.warn(warning);
|
|
1156
|
+
}
|
|
1157
|
+
assertPropValidation(stats, opts, Boolean(opts.apply));
|
|
1158
|
+
if (opts.json) {
|
|
1159
|
+
logger_1.logger.json(JSON.parse(payload.fdConfig));
|
|
1160
|
+
if (!opts.apply)
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (!opts.apply) {
|
|
1164
|
+
logger_1.logger.warn('预览模式:未写入。确认无误后追加 --apply 执行保存');
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (family === 'modeling' && opts.publish) {
|
|
1168
|
+
logger_1.logger.step('保存并发布表单(sysModelingXForm/saveDesignAndPublish)');
|
|
1169
|
+
await formDefine_1.modelingXFormApi.saveDesignAndPublish(client, payload);
|
|
1170
|
+
logger_1.logger.success(`表单 ${opts.id} 字段配置已更新并发布`);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
if (family === 'general') {
|
|
1174
|
+
await formDefine_1.formDefineApi.save(client, payload);
|
|
1175
|
+
}
|
|
1176
|
+
else if (family === 'modeling') {
|
|
1177
|
+
await formDefine_1.modelingXFormApi.saveDesign(client, payload);
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
await formDefine_1.designApi.save(client, payload);
|
|
1181
|
+
}
|
|
1182
|
+
logger_1.logger.success(`表单 ${opts.id} 字段配置已更新`);
|
|
1183
|
+
// saveDesign 会把表单置为草稿态,已发布表单需显式再发布
|
|
1184
|
+
if (opts.publish) {
|
|
1185
|
+
await doPublish(client, family, opts.id, payload);
|
|
1186
|
+
logger_1.logger.success(`表单 ${opts.id} 已重新发布`);
|
|
1187
|
+
}
|
|
1188
|
+
else if (fdStatusBefore === 'official') {
|
|
1189
|
+
logger_1.logger.warn('保存后表单状态变为草稿态,如需保持发布态请追加 --publish');
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
// ---------------------------------------------------------------- category
|
|
1193
|
+
form
|
|
1194
|
+
.command('category-list')
|
|
1195
|
+
.description('查询表单分类列表')
|
|
1196
|
+
.option('--base-url <url>', '服务端基址(覆盖配置)')
|
|
1197
|
+
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
1198
|
+
.option('--json', '以 JSON 输出')
|
|
1199
|
+
.action(async (opts) => {
|
|
1200
|
+
const client = createClient(opts);
|
|
1201
|
+
const result = await formDefine_1.formCategoryApi.list(client, {
|
|
1202
|
+
pageNo: 1,
|
|
1203
|
+
pageSize: 1000,
|
|
1204
|
+
columns: ['fdId', 'fdName', 'fdCode', 'fdCreator', 'fdCreateTime'],
|
|
1205
|
+
sorts: { fdCreateTime: 'DESC' },
|
|
1206
|
+
});
|
|
1207
|
+
if (opts.json) {
|
|
1208
|
+
logger_1.logger.json(result);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const content = result?.content ?? [];
|
|
1212
|
+
logger_1.logger.table(content.map((c) => ({ fdId: c.fdId, 名称: c.fdName, 编码: c.fdCode ?? '' })));
|
|
1213
|
+
});
|
|
1214
|
+
// ---------------------------------------------------------------- delete
|
|
1215
|
+
form
|
|
1216
|
+
.command('delete')
|
|
1217
|
+
.description('删除表单(连同其数据模型,不可逆;需 --yes 确认)')
|
|
1218
|
+
.requiredOption('--id <fdId>', '表单 id')
|
|
1219
|
+
.option('--yes', '确认执行删除')
|
|
1220
|
+
.option('--base-url <url>', '服务端基址(覆盖配置)')
|
|
1221
|
+
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
1222
|
+
.action(async (opts) => {
|
|
1223
|
+
const fdId = String(opts.id ?? '').trim();
|
|
1224
|
+
if (!fdId)
|
|
1225
|
+
throw new Error('表单 id 不能为空');
|
|
1226
|
+
if (!opts.yes) {
|
|
1227
|
+
logger_1.logger.warn('已取消:删除表单属于不可逆操作,请追加 --yes 确认执行');
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const client = createClient(opts);
|
|
1231
|
+
logger_1.logger.step(`删除表单:${fdId}`);
|
|
1232
|
+
await formDefine_1.modelingXFormApi.remove(client, fdId);
|
|
1233
|
+
logger_1.logger.success(`表单 ${fdId} 已删除`);
|
|
1234
|
+
});
|
|
1235
|
+
form
|
|
1236
|
+
.command('delete-data')
|
|
1237
|
+
.description('删除表单的运行时业务数据(保留表单定义;需 --yes 确认)')
|
|
1238
|
+
.requiredOption('--id <fdId>', '表单 id')
|
|
1239
|
+
.option('--yes', '确认执行删除')
|
|
1240
|
+
.option('--base-url <url>', '服务端基址(覆盖配置)')
|
|
1241
|
+
.option('--token <token>', '鉴权 Token(覆盖配置)')
|
|
1242
|
+
.action(async (opts) => {
|
|
1243
|
+
const fdId = String(opts.id ?? '').trim();
|
|
1244
|
+
if (!fdId)
|
|
1245
|
+
throw new Error('表单 id 不能为空');
|
|
1246
|
+
if (!opts.yes) {
|
|
1247
|
+
logger_1.logger.warn('已取消:删除业务数据属于不可逆操作,请追加 --yes 确认执行');
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const client = createClient(opts);
|
|
1251
|
+
logger_1.logger.step(`删除表单业务数据:${fdId}`);
|
|
1252
|
+
await formDefine_1.modelingXFormApi.removeData(client, fdId);
|
|
1253
|
+
logger_1.logger.success(`表单 ${fdId} 的业务数据已删除`);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
/** 按接口族发布表单 */
|
|
1257
|
+
async function doPublish(client, family, fdId, base, desc = '') {
|
|
1258
|
+
logger_1.logger.step(`发布表单(${FAMILY_LABEL[family]}/publish)`);
|
|
1259
|
+
if (family === 'general') {
|
|
1260
|
+
const fresh = await formDefine_1.formDefineApi.get(client, fdId);
|
|
1261
|
+
await formDefine_1.formDefineApi.publish(client, {
|
|
1262
|
+
...base,
|
|
1263
|
+
...fresh,
|
|
1264
|
+
fdId,
|
|
1265
|
+
fdBuildNew: true,
|
|
1266
|
+
fdDescribe: desc,
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
else if (family === 'modeling') {
|
|
1270
|
+
await formDefine_1.modelingXFormApi.publish(client, {
|
|
1271
|
+
...base,
|
|
1272
|
+
fdId,
|
|
1273
|
+
fdDescribe: desc,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
await formDefine_1.designApi.publish(client, {
|
|
1278
|
+
fdId,
|
|
1279
|
+
fdBuildNew: true,
|
|
1280
|
+
fdDescribe: desc,
|
|
1281
|
+
mechanisms: base.mechanisms ?? { load: '*' },
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
logger_1.logger.success('表单已发布');
|
|
1285
|
+
}
|
|
1286
|
+
/** 去掉值为 undefined 的键,避免覆盖 init 返回的默认值 */
|
|
1287
|
+
function stripUndefined(obj) {
|
|
1288
|
+
const result = {};
|
|
1289
|
+
Object.keys(obj).forEach((key) => {
|
|
1290
|
+
if (obj[key] !== undefined)
|
|
1291
|
+
result[key] = obj[key];
|
|
1292
|
+
});
|
|
1293
|
+
return result;
|
|
1294
|
+
}
|