lan-low-code-cli 2.0.0 → 2.2.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 +242 -7
- package/bin/llc.js +0 -0
- package/dist/api/formDefine.js +12 -0
- package/dist/commands/control.js +34 -3
- package/dist/commands/form.js +44 -4
- package/dist/constants.js +326 -2
- package/dist/generator/index.js +338 -33
- package/dist/index.js +10 -1
- package/dist/relation.js +130 -0
- package/dist/widgets.js +39 -4
- package/package.json +45 -43
package/dist/generator/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GLOBAL_ACTION_EVENTS = exports.ACTION_GLOBAL_TARGET = void 0;
|
|
4
|
+
exports.buildRelationProps = buildRelationProps;
|
|
4
5
|
exports.resolveLocales = resolveLocales;
|
|
5
6
|
exports.completeOptionsI18n = completeOptionsI18n;
|
|
6
7
|
exports.buildViewModel = buildViewModel;
|
|
@@ -15,6 +16,57 @@ const random_1 = require("../utils/random");
|
|
|
15
16
|
const widgets_1 = require("../widgets");
|
|
16
17
|
/** `$global`:动作目标为表单全局(onLoad / onBeforeSubmit / onAfterSubmit) */
|
|
17
18
|
exports.ACTION_GLOBAL_TARGET = '$global';
|
|
19
|
+
/** 建模表单的数据模型类名(业务关联的 `modelName`,取自真实表单) */
|
|
20
|
+
const MODELING_MODEL_NAME = 'com.landray.sys.xform.core.entity.design.SysXFormDesign';
|
|
21
|
+
/**
|
|
22
|
+
* 构建业务关联的 `controlProps` 片段(`relationCfg` + `datasource` + 显示方式)。
|
|
23
|
+
*
|
|
24
|
+
* `datasource` 的骨架键取自真实表单(queryCondition/tree/filters/isListThrough…),
|
|
25
|
+
* 这些是设计器「列表设置」弹窗的固有字段,缺失会导致设计器打开该控件时结构不完整。
|
|
26
|
+
*/
|
|
27
|
+
function buildRelationProps(spec) {
|
|
28
|
+
const { target } = spec;
|
|
29
|
+
const columns = spec.columns;
|
|
30
|
+
return {
|
|
31
|
+
renderMode: 'singlelist',
|
|
32
|
+
modelName: MODELING_MODEL_NAME,
|
|
33
|
+
relationCfg: {
|
|
34
|
+
appCode: target.appCode,
|
|
35
|
+
xformName: target.xformName,
|
|
36
|
+
modelId: target.modelId,
|
|
37
|
+
showFields: spec.showFields,
|
|
38
|
+
refFieldName: spec.refFieldName,
|
|
39
|
+
tableType: 'main',
|
|
40
|
+
tableName: target.tableName,
|
|
41
|
+
fdFormType: target.fdFormType ?? 0,
|
|
42
|
+
},
|
|
43
|
+
datasource: {
|
|
44
|
+
queryCondition: { linkType: '$and', query: {} },
|
|
45
|
+
columns,
|
|
46
|
+
filters: spec.filters ?? columns,
|
|
47
|
+
tree: {
|
|
48
|
+
treeLoop: true,
|
|
49
|
+
querySorters: [],
|
|
50
|
+
conditionSetting: false,
|
|
51
|
+
relationCfg: {},
|
|
52
|
+
relationConditions: [{ operator: { value: '$eq' }, value: { format: 'field' } }],
|
|
53
|
+
queryCondition: {
|
|
54
|
+
linkType: '$and',
|
|
55
|
+
query: { type: 'conditionGroup', orAndOperator: 'type', condition: [] },
|
|
56
|
+
},
|
|
57
|
+
navigationMode: 'custom',
|
|
58
|
+
},
|
|
59
|
+
isListThrough: true,
|
|
60
|
+
isDisplayMode: true,
|
|
61
|
+
viewId: '',
|
|
62
|
+
conditionSetting: false,
|
|
63
|
+
isForwardView: true,
|
|
64
|
+
queryCollection: {},
|
|
65
|
+
sorters: [],
|
|
66
|
+
},
|
|
67
|
+
...(spec.outParams ? { outParams: spec.outParams } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
18
70
|
/** 全局动作支持的(前端注册的)事件类型 */
|
|
19
71
|
exports.GLOBAL_ACTION_EVENTS = ['onLoad', 'onBeforeSubmit', 'onAfterSubmit'];
|
|
20
72
|
/** 字段级多语言的 prop 集合(沿用旧视图时这些条目由生成器重建,不从旧 lang 携带) */
|
|
@@ -68,6 +120,28 @@ function collectUnreferencedFields(referencedKeys, built, details) {
|
|
|
68
120
|
}
|
|
69
121
|
return missing;
|
|
70
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* 由生成器统一管理的控件属性键:沿用已有 controlProps 时跳过(否则会覆盖新生成的身份信息、
|
|
125
|
+
* 选项、长度等)。其余键(如 address 的 range/multi/org、money 的 precision/symbol)
|
|
126
|
+
* 视为设计器配置,同控件包时原样沿用。
|
|
127
|
+
*/
|
|
128
|
+
const CONTROL_PROP_MANAGED_KEYS = new Set([
|
|
129
|
+
'id',
|
|
130
|
+
'desktop',
|
|
131
|
+
'mobile',
|
|
132
|
+
'name',
|
|
133
|
+
'uuid',
|
|
134
|
+
'title',
|
|
135
|
+
'span',
|
|
136
|
+
'$$tableType',
|
|
137
|
+
'$$tableName',
|
|
138
|
+
'$$lockMode',
|
|
139
|
+
'options',
|
|
140
|
+
'placeholder',
|
|
141
|
+
'maxLength',
|
|
142
|
+
]);
|
|
143
|
+
/** 不写入 fdFontExtendData 的键(options 由选项逻辑单独维护) */
|
|
144
|
+
const FONT_EXTEND_SKIP_KEYS = new Set(['options', 'rules']);
|
|
71
145
|
/** 控件配置面板默认值中,不应写入 controlProps 的键(属于样式/布局,存放在 controlStyle) */
|
|
72
146
|
const DEFAULT_EXCLUDE_KEYS = new Set([
|
|
73
147
|
'layout',
|
|
@@ -235,12 +309,25 @@ function completeOptionsI18n(field, lang, locales, messages) {
|
|
|
235
309
|
field.fdFontExtendData = JSON.stringify(fontExtend);
|
|
236
310
|
return count;
|
|
237
311
|
}
|
|
238
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* 依据数据项类型/逻辑类型解析控件包与元数据。
|
|
314
|
+
*
|
|
315
|
+
* 匹配顺序(前者优先):
|
|
316
|
+
* 1. `CONTROL_TYPE_MAP[fdType]`:按控件逻辑类型精确匹配(避免「金额/评分/下拉」
|
|
317
|
+
* 被降级成同数据项类型的默认控件,如 number -> 数字);
|
|
318
|
+
* 2. `controlMap[fdType]`:用户自定义映射;
|
|
319
|
+
* 3. **字段已有属性中的控件包**:CLI 未登记的环境控件按原样保留(不被降级破坏);
|
|
320
|
+
* 4. `DEFAULT_CONTROL_MAP[fdDataType]`:按数据项类型兜底;
|
|
321
|
+
* 5. 自定义映射中 logicalType 相同的条目;
|
|
322
|
+
* 6. `FALLBACK_CONTROL`(输入框)。
|
|
323
|
+
*/
|
|
239
324
|
async function resolveControl(field, options) {
|
|
240
325
|
const map = controlMapOf(options);
|
|
241
|
-
// 优先按控件逻辑类型(xform.type)匹配,避免把「金额/评分/下拉」等
|
|
242
|
-
// 错误降级成同数据项类型的默认控件(如 number -> 数字)
|
|
243
326
|
let entry = constants_1.CONTROL_TYPE_MAP[field.fdType] ?? map[field.fdType];
|
|
327
|
+
// 未登记控件:从字段已有属性还原控件包(并保留原字段类型,不被控件包声明覆盖)
|
|
328
|
+
const preserved = entry ? undefined : preservedControlOf(field);
|
|
329
|
+
if (!entry)
|
|
330
|
+
entry = preserved;
|
|
244
331
|
if (!entry)
|
|
245
332
|
entry = map[field.fdDataType];
|
|
246
333
|
if (!entry)
|
|
@@ -253,11 +340,34 @@ async function resolveControl(field, options) {
|
|
|
253
340
|
return {
|
|
254
341
|
desktopPkg: entry.desktop,
|
|
255
342
|
mobilePkg: entry.mobile,
|
|
256
|
-
logicalType:
|
|
343
|
+
logicalType: preserved
|
|
344
|
+
? field.fdType || preserved.logicalType
|
|
345
|
+
: meta?.xform?.type || entry.logicalType,
|
|
257
346
|
meta,
|
|
258
347
|
metaMobile,
|
|
259
348
|
};
|
|
260
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* 从字段已有 `fdAttribute` 中还原控件包。
|
|
352
|
+
*
|
|
353
|
+
* 用于 **CLI 未登记的环境控件**(平台新增或定制控件包):此时若直接回落到
|
|
354
|
+
* `FALLBACK_CONTROL`(输入框),`autofill` 会把表单里的高级控件**降级破坏**。
|
|
355
|
+
* 只要字段属性里带着控件包名,就原样沿用,控件类型得以保全。
|
|
356
|
+
*/
|
|
357
|
+
function preservedControlOf(field) {
|
|
358
|
+
if (typeof field.fdAttribute !== 'string')
|
|
359
|
+
return undefined;
|
|
360
|
+
const attr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
|
|
361
|
+
const config = attr?.config;
|
|
362
|
+
const desktop = config?.controlProps?.desktop?.type;
|
|
363
|
+
if (!desktop)
|
|
364
|
+
return undefined;
|
|
365
|
+
return {
|
|
366
|
+
desktop,
|
|
367
|
+
mobile: config?.controlProps?.mobile?.type || desktop,
|
|
368
|
+
logicalType: config?.type || field.fdType || '',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
261
371
|
/** 从 fdFontExtendData 提取需要注入 controlProps 的前端扩展属性 */
|
|
262
372
|
function pickFontExtend(data) {
|
|
263
373
|
const picked = {};
|
|
@@ -271,6 +381,61 @@ function pickFontExtend(data) {
|
|
|
271
381
|
function fontExtendOf(field) {
|
|
272
382
|
return pickFontExtend((0, random_1.safeJsonParse)(field.fdFontExtendData, {}));
|
|
273
383
|
}
|
|
384
|
+
/** 按点号路径取值(用于语义校验规则) */
|
|
385
|
+
function getByPath(source, path) {
|
|
386
|
+
return path.split('.').reduce((acc, key) => {
|
|
387
|
+
if (!acc || typeof acc !== 'object')
|
|
388
|
+
return undefined;
|
|
389
|
+
return acc[key];
|
|
390
|
+
}, source);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 控件语义校验:`config.json` 必填属性之外、由控件属性面板/数据源弹窗强制的规则
|
|
394
|
+
* (`CONTROL_SEMANTIC_RULES`,如业务关联的「关联表单」与「显示项」)。
|
|
395
|
+
*
|
|
396
|
+
* @returns 未通过规则的报错文案
|
|
397
|
+
*/
|
|
398
|
+
function validateSemanticProps(logicalType, controlProps) {
|
|
399
|
+
const rules = constants_1.CONTROL_SEMANTIC_RULES[logicalType];
|
|
400
|
+
if (!rules?.length)
|
|
401
|
+
return [];
|
|
402
|
+
const get = (path) => getByPath(controlProps, path);
|
|
403
|
+
const renderMode = String(controlProps.renderMode ?? '');
|
|
404
|
+
const errors = [];
|
|
405
|
+
for (const rule of rules) {
|
|
406
|
+
if (rule.renderModes && !rule.renderModes.includes(renderMode))
|
|
407
|
+
continue;
|
|
408
|
+
const pass = rule.ok ? rule.ok(get) : Boolean(get(rule.path ?? ''));
|
|
409
|
+
if (!pass)
|
|
410
|
+
errors.push(rule.message);
|
|
411
|
+
}
|
|
412
|
+
return errors;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* 把 `controlProps` 中「前端扩展数据」同步进 `fdFontExtendData`。
|
|
416
|
+
*
|
|
417
|
+
* 平台约定两处并存(前端 `syncApi.frontExtendData2Config` 反向同步、公式/接口 Schema 的
|
|
418
|
+
* `$ref` 依赖 `fdFontExtendData`)——例如 address(地址本)的 `orgTypeArr` 决定公式里
|
|
419
|
+
* 该字段是 `ORG_PERSON` 还是 `ORG_ALL`。缺了它,控件渲染仍走 controlProps,但公式取值
|
|
420
|
+
* 会退化。已有的其它键(如 options)原样保留。
|
|
421
|
+
*/
|
|
422
|
+
function syncFontExtendData(field, meta, controlProps) {
|
|
423
|
+
const keys = (0, widgets_1.collectFrontExtendKeys)(meta).filter(({ key }) => !FONT_EXTEND_SKIP_KEYS.has(key));
|
|
424
|
+
if (!keys.length)
|
|
425
|
+
return;
|
|
426
|
+
const fontExtend = field.fdFontExtendData
|
|
427
|
+
? (0, random_1.safeJsonParse)(field.fdFontExtendData, {})
|
|
428
|
+
: {};
|
|
429
|
+
for (const { path, key } of keys) {
|
|
430
|
+
const [head, child] = path.split('.');
|
|
431
|
+
const value = child
|
|
432
|
+
? controlProps[head]?.[child]
|
|
433
|
+
: controlProps[head];
|
|
434
|
+
if (value !== undefined)
|
|
435
|
+
fontExtend[key] = value;
|
|
436
|
+
}
|
|
437
|
+
field.fdFontExtendData = JSON.stringify(fontExtend);
|
|
438
|
+
}
|
|
274
439
|
/**
|
|
275
440
|
* 生成数据项的 fdAttribute。
|
|
276
441
|
*
|
|
@@ -293,11 +458,31 @@ function buildFieldAttribute(params) {
|
|
|
293
458
|
controlDefaults[k] = defaults[k];
|
|
294
459
|
});
|
|
295
460
|
const showText = defaults.showText;
|
|
461
|
+
// 必填属性兜底默认值:支持 $title / $name 占位符(如「描述文字」默认取字段标题)
|
|
462
|
+
const requiredDefaults = {};
|
|
463
|
+
for (const [propKey, value] of Object.entries(constants_1.REQUIRED_PROP_DEFAULTS[resolved.desktopPkg] ?? {})) {
|
|
464
|
+
requiredDefaults[propKey] =
|
|
465
|
+
value === '$title' ? langKey : value === '$name' ? fieldName : value;
|
|
466
|
+
}
|
|
467
|
+
// 沿用同控件包的已有属性(设计器配置过的值优先于 config.json 默认值)
|
|
468
|
+
const carried = {};
|
|
469
|
+
const prev = params.prevControlProps;
|
|
470
|
+
const prevPkg = prev?.desktop?.type;
|
|
471
|
+
if (prev && prevPkg === resolved.desktopPkg) {
|
|
472
|
+
for (const [propKey, value] of Object.entries(prev)) {
|
|
473
|
+
if (!CONTROL_PROP_MANAGED_KEYS.has(propKey))
|
|
474
|
+
carried[propKey] = value;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
296
477
|
const controlProps = {
|
|
297
478
|
...controlDefaults,
|
|
479
|
+
// 设计器默认属性(控件包 config.json 未声明的,如业务关联的 renderMode=单一列表)
|
|
480
|
+
...(constants_1.CONTROL_DEFAULT_PROPS[resolved.desktopPkg] ?? {}),
|
|
298
481
|
// 必填属性兜底默认值(config.json 声明 required 但无 default 的项)
|
|
299
|
-
...
|
|
482
|
+
...requiredDefaults,
|
|
483
|
+
...carried,
|
|
300
484
|
...params.fontExtend,
|
|
485
|
+
...(params.extraControlProps ?? {}),
|
|
301
486
|
// 保留已有选项:本函数会重建 controlProps,不显式带回会丢失用户配置的 options
|
|
302
487
|
...(params.options && params.options.length ? { options: params.options } : {}),
|
|
303
488
|
...(params.placeholderKey ? { placeholder: params.placeholderKey } : {}),
|
|
@@ -386,6 +571,16 @@ function enrichField(field, model, resolved) {
|
|
|
386
571
|
}
|
|
387
572
|
/** 依据数据项生成(或补全)控件配置,并回写 fdAttribute / fdLabelLangKey */
|
|
388
573
|
async function buildField(field, model, options, lang, locales) {
|
|
574
|
+
// 业务关联配置(--relation)命中该字段:先把数据类型改写为 relation,
|
|
575
|
+
// 使控件解析走业务关联控件(无论原字段是文本还是下拉)
|
|
576
|
+
const relationSpec = options.relations?.find((item) => item.fieldName === field.fdName);
|
|
577
|
+
if (relationSpec) {
|
|
578
|
+
field.fdType = 'relation';
|
|
579
|
+
field.fdDataType = 'relation';
|
|
580
|
+
field.fdDictType = 'querySourceDict';
|
|
581
|
+
if (!field.fdLength)
|
|
582
|
+
field.fdLength = 200;
|
|
583
|
+
}
|
|
389
584
|
const resolved = await resolveControl(field, options);
|
|
390
585
|
const span = options.span ?? constants_1.DEFAULT_SPAN;
|
|
391
586
|
enrichField(field, model, resolved);
|
|
@@ -400,24 +595,47 @@ async function buildField(field, model, options, lang, locales) {
|
|
|
400
595
|
type: 'input',
|
|
401
596
|
content: buildLangContent(locales, field.fdLabel || field.fdName, field.fdName, options.messages),
|
|
402
597
|
};
|
|
403
|
-
//
|
|
598
|
+
// 已有控件属性(沿用选项、提示文字等)
|
|
599
|
+
const prevAttr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
|
|
600
|
+
// 控件提示文字多语言。
|
|
601
|
+
// ① 已有提示文字优先沿用(设计器里的自定义文案不能丢,如「请选择交办日期」);
|
|
602
|
+
// ② 否则取控件自带译文——资源键**因控件而异**(datetime=pleaseEnter、m-datetime=enter、
|
|
603
|
+
// area=pleaseSelect,其余=placeholder),键名不对就取不到文案(日期控件提示文字缺失即此原因)。
|
|
404
604
|
let placeholderKey;
|
|
405
605
|
const registry = options.registry;
|
|
406
606
|
if (registry && (0, widgets_1.hasConfigItem)(resolved.meta, 'placeholder')) {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
607
|
+
const prevPlaceholder = prevAttr?.config?.controlProps?.placeholder;
|
|
608
|
+
const prevLangEntry = typeof prevPlaceholder === 'string' ? options.prevLang?.[prevPlaceholder] : undefined;
|
|
609
|
+
if (typeof prevPlaceholder === 'string' &&
|
|
610
|
+
prevPlaceholder &&
|
|
611
|
+
(prevLangEntry || !(0, random_1.isMessageKey)(prevPlaceholder))) {
|
|
612
|
+
placeholderKey = prevPlaceholder;
|
|
613
|
+
if (prevLangEntry)
|
|
614
|
+
lang[prevPlaceholder] = prevLangEntry;
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
const mapped = constants_1.CONTROL_PLACEHOLDER_LANG_KEYS[resolved.desktopPkg];
|
|
618
|
+
const candidates = [
|
|
619
|
+
...new Set([mapped, ...constants_1.PLACEHOLDER_LANG_KEY_CANDIDATES].filter(Boolean)),
|
|
620
|
+
];
|
|
621
|
+
for (const keyName of candidates) {
|
|
622
|
+
const langs = await registry.getElementLangs((0, widgets_1.elementLangKey)(resolved.desktopPkg, keyName));
|
|
623
|
+
// 控件自带的多语言(zh-cn/en-us/...),用于填充非主语言的真实译文
|
|
624
|
+
const elementLangs = langs ? (0, locale_1.normalizeLangMap)(langs) : undefined;
|
|
625
|
+
const value = elementLangs?.[locales[0]] ??
|
|
626
|
+
elementLangs?.[constants_1.DEFAULT_LOCALE] ??
|
|
627
|
+
(elementLangs ? Object.values(elementLangs)[0] : undefined);
|
|
628
|
+
if (!value)
|
|
629
|
+
continue;
|
|
630
|
+
placeholderKey = (0, random_1.generateMessageKey)();
|
|
631
|
+
lang[placeholderKey] = {
|
|
632
|
+
prop: 'placeholder',
|
|
633
|
+
name: field.fdName,
|
|
634
|
+
type: 'input',
|
|
635
|
+
content: buildLangContent(locales, value, undefined, options.messages, elementLangs),
|
|
636
|
+
};
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
421
639
|
}
|
|
422
640
|
}
|
|
423
641
|
// 新建选项(--option):写入 fdFontExtendData.options,作为选项的唯一来源
|
|
@@ -437,7 +655,6 @@ async function buildField(field, model, options, lang, locales) {
|
|
|
437
655
|
field.fdFontExtendData = JSON.stringify(fontExtend);
|
|
438
656
|
}
|
|
439
657
|
// 保留已有的控件选项(数据项/设计器中配置),否则重建 attribute 会丢
|
|
440
|
-
const prevAttr = (0, random_1.safeJsonParse)(field.fdAttribute, {});
|
|
441
658
|
const attrOptions = prevAttr?.config?.controlProps?.options;
|
|
442
659
|
const fontOptions = field.fdFontExtendData
|
|
443
660
|
? ((0, random_1.safeJsonParse)(field.fdFontExtendData, {}).options ?? [])
|
|
@@ -462,13 +679,33 @@ async function buildField(field, model, options, lang, locales) {
|
|
|
462
679
|
placeholderKey,
|
|
463
680
|
fontExtend: fontExtendOf(field),
|
|
464
681
|
options: prevOptions.length ? prevOptions : undefined,
|
|
682
|
+
prevControlProps: prevAttr?.config?.controlProps,
|
|
683
|
+
extraControlProps: relationSpec ? buildRelationProps(relationSpec) : undefined,
|
|
465
684
|
});
|
|
466
685
|
field.fdAttribute = attribute;
|
|
686
|
+
// 前端扩展数据同步(如 address 的 orgTypeArr/multi/range)
|
|
687
|
+
syncFontExtendData(field, resolved.meta, (config.controlProps ?? {}));
|
|
688
|
+
// 业务关联的前端扩展数据(控件源码 JSDoc 声明的 @frontExtendData 路径):
|
|
689
|
+
// modelId/refFieldName/tableType/tableName、datasource 各键、renderMode、outParams
|
|
690
|
+
if (relationSpec) {
|
|
691
|
+
const fontExtend = field.fdFontExtendData
|
|
692
|
+
? (0, random_1.safeJsonParse)(field.fdFontExtendData, {})
|
|
693
|
+
: {};
|
|
694
|
+
const props = (config.controlProps ?? {});
|
|
695
|
+
for (const path of constants_1.RELATION_FRONT_EXTEND_PATHS) {
|
|
696
|
+
const value = getByPath(props, path);
|
|
697
|
+
if (value !== undefined)
|
|
698
|
+
fontExtend[path.split('.').pop()] = value;
|
|
699
|
+
}
|
|
700
|
+
field.fdFontExtendData = JSON.stringify(fontExtend);
|
|
701
|
+
}
|
|
467
702
|
// 选项多语言(下拉/单选/多选)
|
|
468
703
|
const optionCount = completeOptionsI18n(field, lang, locales, options.messages);
|
|
469
704
|
// 控件必填属性校验(对齐设计器发布前校验)
|
|
470
705
|
const propErrors = (0, widgets_1.validateRequiredProps)(resolved.meta, config);
|
|
471
|
-
|
|
706
|
+
// 控件语义校验(属性面板/数据源弹窗强制的规则,如业务关联的关联表单与显示项)
|
|
707
|
+
const semanticErrors = validateSemanticProps(resolved.logicalType, (config.controlProps ?? {}));
|
|
708
|
+
return { field, model, resolved, langKey, optionCount, propErrors, semanticErrors };
|
|
472
709
|
}
|
|
473
710
|
/** 构建虚拟行:控件节点在持久化格式中**只保留 key 引用** */
|
|
474
711
|
function buildRows(built, options) {
|
|
@@ -489,7 +726,7 @@ function buildRows(built, options) {
|
|
|
489
726
|
return rows;
|
|
490
727
|
}
|
|
491
728
|
/**
|
|
492
|
-
* 栅格布局:`layout` → `@elem/layout-grid` →
|
|
729
|
+
* 栅格布局:`layout` → `@elem/layout-grid` → `columns × rows` 个 `@elem/layout-grid.GridItem`。
|
|
493
730
|
*
|
|
494
731
|
* 结构依据真实环境表单(`layout~xxxx` / `controlProps:{id}`):
|
|
495
732
|
* ```
|
|
@@ -497,6 +734,9 @@ function buildRows(built, options) {
|
|
|
497
734
|
* └ @elem/layout-grid { controlProps:{ id, columns, rows } }
|
|
498
735
|
* └ GridItem { controlProps:{ id, column, row, rowSpan, columnSpan, style:{backgroundColor:''} }, children:[{key:<fieldUuid>}] }
|
|
499
736
|
* ```
|
|
737
|
+
* **最后一行的空位也要保留**:`GridItem` 总数 = `columns × rows`,未放控件的格子为
|
|
738
|
+
* `children: []`、`controlProps: { id, column, row }`(与设计器一致,已核对真实表单——
|
|
739
|
+
* 如《业务审批管理》cols=3 而字段只有 2 个,第 3 个格子为空仍在)。
|
|
500
740
|
* 注意:栅格布局控件**不进** `controlMappings`(已核对真实表单)。
|
|
501
741
|
*/
|
|
502
742
|
function buildGrid(built, columns) {
|
|
@@ -504,6 +744,7 @@ function buildGrid(built, columns) {
|
|
|
504
744
|
const rows = Math.max(1, Math.ceil(built.length / cols));
|
|
505
745
|
const layoutKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT);
|
|
506
746
|
const gridKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID);
|
|
747
|
+
// 先放控件,再补齐最后一行的空位(总数 = cols × rows;无字段时不生成格子)
|
|
507
748
|
const items = built.map((item, index) => {
|
|
508
749
|
const itemKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID_ITEM);
|
|
509
750
|
return {
|
|
@@ -521,6 +762,21 @@ function buildGrid(built, columns) {
|
|
|
521
762
|
children: [{ key: item.field.uuid }],
|
|
522
763
|
};
|
|
523
764
|
});
|
|
765
|
+
for (let index = built.length; index < (built.length ? rows * cols : 0); index++) {
|
|
766
|
+
const itemKey = (0, random_1.generateCmptKey)(constants_1.LAYOUT_GRID_ITEM);
|
|
767
|
+
items.push({
|
|
768
|
+
key: itemKey,
|
|
769
|
+
type: constants_1.LAYOUT_GRID_ITEM,
|
|
770
|
+
kind: 'container',
|
|
771
|
+
// 空位与设计器一致:仅 column/row/id,children 为空
|
|
772
|
+
controlProps: {
|
|
773
|
+
column: (index % cols) + 1,
|
|
774
|
+
row: Math.floor(index / cols) + 1,
|
|
775
|
+
id: itemKey,
|
|
776
|
+
},
|
|
777
|
+
children: [],
|
|
778
|
+
});
|
|
779
|
+
}
|
|
524
780
|
return [
|
|
525
781
|
{
|
|
526
782
|
key: layoutKey,
|
|
@@ -966,16 +1222,29 @@ function applyControlActions(formAttr, targets, actions) {
|
|
|
966
1222
|
const warnings = [];
|
|
967
1223
|
let completed = 0;
|
|
968
1224
|
for (const action of actions) {
|
|
969
|
-
// 函数名:显式指定 > 源码解析 >
|
|
970
|
-
|
|
1225
|
+
// 函数名:显式指定 > 源码解析 > 自动生成
|
|
1226
|
+
let rawName = action.name ??
|
|
971
1227
|
extractFunctionName(action.source) ??
|
|
972
1228
|
`${action.event}_${action.target.replace(/[^\w]/g, '_')}`;
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1229
|
+
let source = action.source;
|
|
1230
|
+
// 命名规范:禁止裸事件名(onChange / onClick / onLoad 等)——无法分辨触发控件。
|
|
1231
|
+
// 统一追加控件 ID:onChange -> onChange_<fdName>($global 全局动作保持裸事件名,不做后缀)
|
|
1232
|
+
if (action.target !== exports.ACTION_GLOBAL_TARGET && /^on[A-Z][a-zA-Z0-9]*$/.test(rawName)) {
|
|
1233
|
+
const suffix = action.target === exports.ACTION_GLOBAL_TARGET ? 'global' : action.target;
|
|
1234
|
+
const normalized = `${rawName}_${suffix}`;
|
|
1235
|
+
// 同步重写源码中的函数声明,保持 JS 面板展示一致(仅替换声明处,不影响函数体)
|
|
1236
|
+
const decl = `function ${rawName}(`;
|
|
1237
|
+
if (source.includes(decl))
|
|
1238
|
+
source = source.replace(decl, `function ${normalized}(`);
|
|
1239
|
+
warnings.push(`动作函数名 "${rawName}" 无法分辨触发控件,已规范为 "${normalized}"`);
|
|
1240
|
+
rawName = normalized;
|
|
1241
|
+
}
|
|
1242
|
+
const name = usedNames.has(rawName) ? `${rawName}_${(0, random_1.uuid)()}` : rawName;
|
|
1243
|
+
if (name !== rawName) {
|
|
1244
|
+
warnings.push(`动作函数重名:${rawName} 已重命名为 ${name}`);
|
|
976
1245
|
}
|
|
977
1246
|
usedNames.add(name);
|
|
978
|
-
const item = { name, id: (0, random_1.uuid)(), function:
|
|
1247
|
+
const item = { name, id: (0, random_1.uuid)(), function: source };
|
|
979
1248
|
if (action.target === exports.ACTION_GLOBAL_TARGET) {
|
|
980
1249
|
;
|
|
981
1250
|
(globalMap[_a = action.event] ?? (globalMap[_a] = [])).push(item);
|
|
@@ -1106,6 +1375,9 @@ async function buildFdConfig(models, options = {}, stats) {
|
|
|
1106
1375
|
for (const error of builtField.propErrors) {
|
|
1107
1376
|
validationErrors.push(`字段 ${field.fdName}(${builtField.resolved.desktopPkg}):请输入${error.label}(${error.key})`);
|
|
1108
1377
|
}
|
|
1378
|
+
for (const error of builtField.semanticErrors) {
|
|
1379
|
+
validationErrors.push(`字段 ${field.fdName}(${builtField.resolved.desktopPkg}):${error}`);
|
|
1380
|
+
}
|
|
1109
1381
|
}
|
|
1110
1382
|
// ---------- 明细表 ----------
|
|
1111
1383
|
const detailModels = models.filter((m) => m.fdType === 'detail');
|
|
@@ -1131,6 +1403,9 @@ async function buildFdConfig(models, options = {}, stats) {
|
|
|
1131
1403
|
for (const error of builtField.propErrors) {
|
|
1132
1404
|
validationErrors.push(`明细字段 ${model.fdTableName}.${field.fdName}(${builtField.resolved.desktopPkg}):请输入${error.label}(${error.key})`);
|
|
1133
1405
|
}
|
|
1406
|
+
for (const error of builtField.semanticErrors) {
|
|
1407
|
+
validationErrors.push(`明细字段 ${model.fdTableName}.${field.fdName}(${builtField.resolved.desktopPkg}):${error}`);
|
|
1408
|
+
}
|
|
1134
1409
|
}
|
|
1135
1410
|
// 明细表标题多语言(prop:label,content 额外带 default)
|
|
1136
1411
|
const langKey = (0, random_1.isMessageKey)(model.fdLabelLangKey)
|
|
@@ -1214,6 +1489,22 @@ async function buildFdConfig(models, options = {}, stats) {
|
|
|
1214
1489
|
stats.actionWarnings = actionResult.warnings;
|
|
1215
1490
|
if (validationErrors.length)
|
|
1216
1491
|
stats.validationErrors = validationErrors;
|
|
1492
|
+
// 需到设计器完成配置的高级控件(关键属性是环境对象选择,CLI 无法替选)。
|
|
1493
|
+
// 仅在该字段确实缺配置(有必填/语义校验错误)时提示——已配置好的字段不该被误导;
|
|
1494
|
+
// associatedFields 无必填属性、问题只在发布时暴露,故无条件提示。
|
|
1495
|
+
const notes = new Set();
|
|
1496
|
+
for (const item of [...built, ...details.flatMap((d) => d.fields)]) {
|
|
1497
|
+
const logicalType = String(item.field.fdType);
|
|
1498
|
+
const note = constants_1.CONTROL_DESIGNER_NOTES[logicalType];
|
|
1499
|
+
if (!note)
|
|
1500
|
+
continue;
|
|
1501
|
+
const hasErrors = item.propErrors.length > 0 || item.semanticErrors.length > 0;
|
|
1502
|
+
if (!hasErrors && !constants_1.CONTROL_NOTE_WITHOUT_ERRORS.has(logicalType))
|
|
1503
|
+
continue;
|
|
1504
|
+
notes.add(`${item.field.fdLabel || item.field.fdName}(${logicalType})—— ${note}`);
|
|
1505
|
+
}
|
|
1506
|
+
if (notes.size)
|
|
1507
|
+
stats.controlWarnings = [...notes];
|
|
1217
1508
|
}
|
|
1218
1509
|
return {
|
|
1219
1510
|
id: options.formId ?? '',
|
|
@@ -1246,6 +1537,14 @@ async function createDataField(spec, options = {}) {
|
|
|
1246
1537
|
controlType = dataType;
|
|
1247
1538
|
dataType = undefined;
|
|
1248
1539
|
}
|
|
1540
|
+
// 容器类控件不是数据项(明细表/大数据明细/分页),给出明确指引而不是模糊报错。
|
|
1541
|
+
// 第三段既可写数据项类型也可写控件类型(见下方容错),两个位置都要拦。
|
|
1542
|
+
for (const token of [spec.controlType, spec.dataType]) {
|
|
1543
|
+
const container = token ? constants_1.CONTAINER_CONTROL_TYPES[token] : undefined;
|
|
1544
|
+
if (container) {
|
|
1545
|
+
throw new Error(`"${token}" 是容器类控件,不能用 --add-field 创建:${container.hint}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1249
1548
|
let entry;
|
|
1250
1549
|
if (controlType) {
|
|
1251
1550
|
entry = alias[controlType];
|
|
@@ -1265,16 +1564,22 @@ async function createDataField(spec, options = {}) {
|
|
|
1265
1564
|
const registry = options.registry;
|
|
1266
1565
|
const meta = registry ? await registry.get(entry.desktop) : null;
|
|
1267
1566
|
const xform = meta?.xform ?? {};
|
|
1567
|
+
// 元数据(控件包 config.json)优先,映射表兜底:高级控件中 desc/serial-number
|
|
1568
|
+
// 等控件包未声明 dataType,必须靠映射表补齐,否则数据项类型为空会导致保存失败
|
|
1569
|
+
const resolvedDataType = xform.dataType || entry.dataType || dataType || 'varchar';
|
|
1570
|
+
const resolvedDictType = xform.dictType || entry.dictType;
|
|
1571
|
+
const resolvedLength = xform.defaultLength ?? entry.defaultLength ?? 0;
|
|
1572
|
+
const resolvedStored = xform.isStored ?? entry.isStored ?? true;
|
|
1268
1573
|
return {
|
|
1269
1574
|
fdName: spec.name || `${constants_1.FIELD_NAME_PREFIX}${Math.random().toString(36).slice(-6)}`,
|
|
1270
1575
|
fdLabel: spec.label,
|
|
1271
|
-
fdDataType:
|
|
1576
|
+
fdDataType: resolvedDataType,
|
|
1272
1577
|
fdType: xform.type || entry.logicalType,
|
|
1273
|
-
fdDictType:
|
|
1274
|
-
fdLength:
|
|
1578
|
+
...(resolvedDictType ? { fdDictType: resolvedDictType } : {}),
|
|
1579
|
+
fdLength: resolvedLength,
|
|
1275
1580
|
fdIsSystem: false,
|
|
1276
1581
|
fdDisplay: true,
|
|
1277
|
-
fdIsStored:
|
|
1582
|
+
fdIsStored: resolvedStored,
|
|
1278
1583
|
fdState: 'effective',
|
|
1279
1584
|
};
|
|
1280
1585
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ const field_1 = require("./commands/field");
|
|
|
9
9
|
const form_1 = require("./commands/form");
|
|
10
10
|
const locale_1 = require("./commands/locale");
|
|
11
11
|
const logger_1 = require("./utils/logger");
|
|
12
|
-
const VERSION = '2.
|
|
12
|
+
const VERSION = '2.2.0';
|
|
13
13
|
const program = new commander_1.Command();
|
|
14
14
|
exports.program = program;
|
|
15
15
|
program
|
|
@@ -103,9 +103,18 @@ ${logger_1.color.bold('从零到一')}
|
|
|
103
103
|
${logger_1.color.bold('常用查询')}
|
|
104
104
|
llc app list [--keyword 名称] [--no-auth] 应用列表(fdId 即 --app-id)
|
|
105
105
|
llc app forms --id <appId> 应用下表单列表
|
|
106
|
+
llc control list 环境可用控件(含高级控件;明细/分页为容器类)
|
|
106
107
|
llc locale list 环境提供了哪些语言译文
|
|
107
108
|
llc field library [--keyword 关键词] 字段库
|
|
108
109
|
|
|
110
|
+
${logger_1.color.gray('高级控件(desc / tags / mdm / association / mechnumber …)与普通控件同样写法:')}
|
|
111
|
+
llc form autofill --id <fdId> --apply --add-field "字段名:标题:控件类型"
|
|
112
|
+
${logger_1.color.gray('(需环境对象选择的控件会在生成时提示,并需到设计器补配置)')}
|
|
113
|
+
|
|
114
|
+
${logger_1.color.gray('业务关联(relation)用 --relation 配置业务对象与显示项:')}
|
|
115
|
+
llc form autofill --id <fdId> --apply --publish \\
|
|
116
|
+
--add-field "fd_r:关联选择:relation" --relation "fd_r=目标表单名"
|
|
117
|
+
|
|
109
118
|
环境变量:
|
|
110
119
|
LLC_BASE_URL 服务端基址
|
|
111
120
|
LLC_TOKEN 鉴权 Token
|