worsoft-frontend-codegen-local-mcp 0.1.88 → 0.1.89

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.
@@ -22,15 +22,11 @@
22
22
  </div>
23
23
  </template>
24
24
  <!-- 主子表表单:按 PRD 表单显隐和顺序渲染字段 -->
25
- <el-form ref="dataFormRef" :model="form" :rules="dataRules" :disabled="detail" v-loading="loading">
26
- <!-- 主表字段区:字段级注释由 MCP 根据字段名称生成 -->
27
- <el-row :gutter="24">
28
- {{FORM_FIELDS}}
29
- </el-row>
30
- <!-- 子表明细区:按 children 配置渲染多个子表 -->
31
- <el-row :gutter="24">
25
+ <el-form ref="dataFormRef" :model="form" :rules="dataRules" :disabled="detail" v-loading="loading" label-width="120px">
26
+ <el-collapse v-model="activeCollapseNames">
27
+ {{FORM_COLLAPSE_SECTIONS}}
32
28
  {{CHILD_SECTIONS}}
33
- </el-row>
29
+ </el-collapse>
34
30
  </el-form>
35
31
  </el-card>
36
32
  </div>
@@ -72,6 +68,7 @@ const pageI18nKey = '{{I18N_NAMESPACE}}';
72
68
 
73
69
  // 表单引用
74
70
  const dataFormRef = ref();
71
+ const activeCollapseNames = ref({{ACTIVE_COLLAPSE_NAMES}});
75
72
  // 页面加载状态
76
73
  const loading = ref(false);
77
74
  // 是否详情模式
@@ -22,11 +22,10 @@
22
22
  </div>
23
23
  </template>
24
24
  <!-- 主表表单:按 PRD 表单显隐和顺序渲染字段 -->
25
- <el-form ref="dataFormRef" :model="form" :rules="dataRules" :disabled="detail" v-loading="loading">
26
- <!-- 主表字段区:字段级注释由 MCP 根据字段名称生成 -->
27
- <el-row :gutter="24">
28
- {{FORM_FIELDS}}
29
- </el-row>
25
+ <el-form ref="dataFormRef" :model="form" :rules="dataRules" :disabled="detail" v-loading="loading" label-width="120px">
26
+ <el-collapse v-model="activeCollapseNames">
27
+ {{FORM_COLLAPSE_SECTIONS}}
28
+ </el-collapse>
30
29
  </el-form>
31
30
  </el-card>
32
31
  </div>
@@ -63,6 +62,7 @@ const { closeCurrentPage } = useCloseCurrentPage();
63
62
 
64
63
  // 表单引用
65
64
  const dataFormRef = ref();
65
+ const activeCollapseNames = ref({{ACTIVE_COLLAPSE_NAMES}});
66
66
  // 页面加载状态
67
67
  const loading = ref(false);
68
68
  // 是否详情模式
package/mcp_server.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
7
  const SERVER_NAME = 'worsoft-codegen-local';
8
- const SERVER_VERSION = '0.1.88';
8
+ const SERVER_VERSION = '0.1.89';
9
9
  const PROTOCOL_VERSION = '2024-11-05';
10
10
  const TOOL_NAME = 'worsoft_codegen_local_generate_frontend';
11
11
  const STYLE_CATALOG_PATH = path.join(__dirname, 'assets', 'style-catalog.json');
@@ -1228,13 +1228,19 @@ function readUtf8File(filePath) {
1228
1228
  return stripBom(fs.readFileSync(filePath, 'utf8'));
1229
1229
  }
1230
1230
 
1231
- function normalizeDictType(value) {
1232
- const normalized = String(value || '').trim();
1233
- if (!normalized || normalized === '-' || normalized === '/') {
1234
- return null;
1235
- }
1236
- return normalized.replace(/^['"`]+|['"`]+$/g, '');
1237
- }
1231
+ function normalizeDictType(value) {
1232
+ const normalized = String(value || '').trim();
1233
+ if (!normalized || normalized === '-' || normalized === '/') {
1234
+ return null;
1235
+ }
1236
+ return normalized.replace(/^['"`]+|['"`]+$/g, '');
1237
+ }
1238
+
1239
+ function normalizeDictTypeFromSourceLabel(value) {
1240
+ const normalized = normalizeDictType(value);
1241
+ if (!normalized) return null;
1242
+ return /^[a-z][a-z0-9_]*$/.test(normalized) ? normalized : null;
1243
+ }
1238
1244
 
1239
1245
  function stripDictAnnotation(label) {
1240
1246
  const text = String(label || '').trim();
@@ -1433,29 +1439,33 @@ function normalizeStructuredField(inputField, index, contextLabel) {
1433
1439
  throw new Error(contextLabel + '[' + index + '] is missing required field: type');
1434
1440
  }
1435
1441
 
1436
- const { length, scale } = normalizeStructuredLengthAndScale(inputField.length ?? inputField.maxLength, inputField.scale);
1437
- const explicitFormType = normalizeStructuredFormType(inputField.formType || inputField.componentType);
1438
- const formType = explicitFormType;
1439
- const explicitQueryType =
1440
- inputField.queryType === undefined || inputField.queryType === null || inputField.queryType === ''
1441
- ? undefined
1442
+ const { length, scale } = normalizeStructuredLengthAndScale(inputField.length ?? inputField.maxLength, inputField.scale);
1443
+ const explicitFormType = normalizeStructuredFormType(inputField.formType || inputField.componentType);
1444
+ const formType = explicitFormType;
1445
+ const dictType = normalizeDictType(inputField.dictType) || normalizeDictTypeFromSourceLabel(inputField.sourceDictLabel);
1446
+ const explicitQueryType =
1447
+ inputField.queryType === undefined || inputField.queryType === null || inputField.queryType === ''
1448
+ ? undefined
1442
1449
  : Number.parseInt(String(inputField.queryType), 10);
1443
1450
  const show = parseBooleanLike(inputField.show, true);
1444
- const listShow = parseBooleanLike(inputField.listShow, show);
1445
- const formShow = parseBooleanLike(inputField.formShow, show);
1446
- const formOrder = parseOptionalOrder(inputField.formOrder, contextLabel + '[' + index + '].formOrder');
1447
-
1448
- return {
1451
+ const listShow = parseBooleanLike(inputField.listShow, show);
1452
+ const formShow = parseBooleanLike(inputField.formShow, show);
1453
+ const formOrder = parseOptionalOrder(inputField.formOrder, contextLabel + '[' + index + '].formOrder');
1454
+ if (formShow === false && formOrder !== undefined) {
1455
+ throw new Error(`${contextLabel}[${index}].formOrder must be empty when formShow=false`);
1456
+ }
1457
+
1458
+ return {
1449
1459
  fieldName,
1450
1460
  attrName: toCamelCase(fieldName),
1451
1461
  sqlType: type,
1452
1462
  length,
1453
1463
  scale,
1454
1464
  comment: label,
1455
- label,
1456
- description: String(inputField.description || '').trim(),
1457
- formType,
1458
- dictType: normalizeDictType(inputField.dictType),
1465
+ label,
1466
+ description: String(inputField.description || '').trim(),
1467
+ formType,
1468
+ dictType,
1459
1469
  notNull: parseBooleanLike(inputField.required, false),
1460
1470
  defaultValue: normalizeDefaultValue(inputField.defaultValue),
1461
1471
  readonly: parseBooleanLike(inputField.readonly, false),
@@ -1465,13 +1475,13 @@ function normalizeStructuredField(inputField, index, contextLabel) {
1465
1475
  formOrder,
1466
1476
  width: normalizeOptionWidth(inputField.width),
1467
1477
  smart: parseBooleanLike(inputField.smart, false),
1468
- queryType: Number.isNaN(explicitQueryType)
1469
- ? undefined
1470
- : explicitQueryType !== undefined
1471
- ? explicitQueryType
1472
- : normalizeDictType(inputField.dictType) && listShow
1473
- ? 30
1474
- : undefined,
1478
+ queryType: Number.isNaN(explicitQueryType)
1479
+ ? undefined
1480
+ : explicitQueryType !== undefined
1481
+ ? explicitQueryType
1482
+ : dictType && listShow
1483
+ ? 30
1484
+ : undefined,
1475
1485
  sourceKind: normalizeStructuredSourceKind(inputField.sourceKind),
1476
1486
  primary: parseBooleanLike(inputField.primary, fieldName === 'id'),
1477
1487
  };
@@ -1714,6 +1724,73 @@ function assertParseResultPayloadConsistency(parseResult, mcpPayload, parseResul
1714
1724
  }
1715
1725
  }
1716
1726
 
1727
+ function collectPayloadFieldGroups(mcpPayload) {
1728
+ const groups = [];
1729
+ if (Array.isArray(mcpPayload.fields)) {
1730
+ groups.push({ path: 'mcpPayload.fields', fields: mcpPayload.fields });
1731
+ }
1732
+ if (Array.isArray(mcpPayload.children)) {
1733
+ mcpPayload.children.forEach((child, childIndex) => {
1734
+ if (Array.isArray(child.fields)) {
1735
+ groups.push({ path: `mcpPayload.children[${childIndex}].fields`, fields: child.fields });
1736
+ }
1737
+ });
1738
+ }
1739
+ if (Array.isArray(mcpPayload.levels)) {
1740
+ mcpPayload.levels.forEach((level, levelIndex) => {
1741
+ if (!Array.isArray(level.modules)) return;
1742
+ level.modules.forEach((module, moduleIndex) => {
1743
+ if (Array.isArray(module.fields)) {
1744
+ groups.push({ path: `mcpPayload.levels[${levelIndex}].modules[${moduleIndex}].fields`, fields: module.fields });
1745
+ }
1746
+ });
1747
+ });
1748
+ }
1749
+ return groups;
1750
+ }
1751
+
1752
+ function normalizeFieldSet(values) {
1753
+ if (!Array.isArray(values)) return new Set();
1754
+ return new Set(
1755
+ values
1756
+ .map((item) => (typeof item === 'string' ? item : item && (item.fieldName || item.name)))
1757
+ .filter(Boolean)
1758
+ .map((item) => String(item).trim())
1759
+ );
1760
+ }
1761
+
1762
+ function assertVisibilityMatrixConsistency(parseResult, mcpPayload, parseResultPath) {
1763
+ if (!parseResult.visibilityMatrix || typeof parseResult.visibilityMatrix !== 'object') return;
1764
+ const matrixGroups = Array.isArray(parseResult.visibilityMatrix.groups) ? parseResult.visibilityMatrix.groups : [];
1765
+ const fallbackVisibleFields = normalizeFieldSet(parseResult.visibilityMatrix.visibleFormFields);
1766
+ const fallbackHiddenFields = normalizeFieldSet(parseResult.visibilityMatrix.explicitHiddenFormFields || parseResult.visibilityMatrix.hiddenFormFields);
1767
+ if (!matrixGroups.length && !fallbackVisibleFields.size && !fallbackHiddenFields.size) return;
1768
+
1769
+ const issues = [];
1770
+ for (const group of collectPayloadFieldGroups(mcpPayload)) {
1771
+ const matrixGroup = matrixGroups.find((item) => item && item.path === group.path);
1772
+ const visibleFields = matrixGroup ? normalizeFieldSet(matrixGroup.visibleFormFields) : fallbackVisibleFields;
1773
+ const hiddenFields = matrixGroup ? normalizeFieldSet(matrixGroup.explicitHiddenFormFields || matrixGroup.hiddenFormFields) : fallbackHiddenFields;
1774
+ if (!visibleFields.size && !hiddenFields.size) continue;
1775
+
1776
+ for (const field of group.fields) {
1777
+ if (!field || typeof field !== 'object' || !field.fieldName) continue;
1778
+ const fieldName = String(field.fieldName);
1779
+ const expectedFormShow = visibleFields.has(fieldName) && !hiddenFields.has(fieldName);
1780
+ if (Boolean(field.formShow) !== expectedFormShow) {
1781
+ issues.push(`${group.path}.${fieldName}.formShow expected ${expectedFormShow} from visibilityMatrix, got ${field.formShow}`);
1782
+ }
1783
+ if (!expectedFormShow && field.formOrder !== undefined && field.formOrder !== null && field.formOrder !== '') {
1784
+ issues.push(`${group.path}.${fieldName}.formOrder must be empty when visibilityMatrix marks it hidden`);
1785
+ }
1786
+ }
1787
+ }
1788
+
1789
+ if (issues.length) {
1790
+ throw new Error(`parseResult visibilityMatrix conflicts with mcpPayload in ${parseResultPath}: ${issues.join('; ')}`);
1791
+ }
1792
+ }
1793
+
1717
1794
  function resolveGenerationInput(input) {
1718
1795
  if (!input.parseResultPath) {
1719
1796
  return { generationInput: input, parseResultPath: '' };
@@ -1744,6 +1821,7 @@ function resolveGenerationInput(input) {
1744
1821
  'moduleName',
1745
1822
  ]);
1746
1823
  assertParseResultPayloadConsistency(parseResult, generationInput, parsed.path);
1824
+ assertVisibilityMatrixConsistency(parseResult, generationInput, parsed.path);
1747
1825
 
1748
1826
  for (const key of ['frontendPath', 'moduleName', 'writeToDisk', 'overwrite', 'writeSupportFiles', 'mergeI18nZh']) {
1749
1827
  if (Object.prototype.hasOwnProperty.call(input, key)) {
@@ -2344,23 +2422,24 @@ function renderChildSection(childModel, childCount) {
2344
2422
  const deleteExpression = `deleteChild(obj, '${childModel.pk.attrName}')`;
2345
2423
 
2346
2424
  return [
2347
- ' <el-col :span="24" class="mb20">',
2348
- ` <!-- 子表区域:${sanitizeHtmlComment(childModel.comment || childModel.tableName)} -->`,
2349
- ' <div class="mb10" style="display:flex;justify-content:space-between;align-items:center;">',
2350
- ` <span style="font-weight: 600;">{{ childSectionTitle('${childModel.listName}') }}</span>`,
2351
- ' <div style="display:flex;align-items:center;gap:8px;">',
2425
+ ` <el-collapse-item :title="childSectionTitle('${childModel.listName}')" name="child-${childModel.listName}">`,
2426
+ ' <el-row :gutter="24">',
2427
+ ' <el-col :span="24" class="mb20">',
2428
+ ` <!-- 子表区域:${sanitizeHtmlComment(childModel.comment || childModel.tableName)} -->`,
2429
+ ' <div class="mb10" style="display:flex;justify-content:flex-end;align-items:center;gap:8px;">',
2352
2430
  ` <el-button icon="Plus" type="primary" :disabled="detail" @click="handleAddChild('${childModel.listName}')">{{ t('common.addBtn') }}</el-button>`,
2353
2431
  ` <el-button icon="Delete" type="danger" plain :disabled="detail || !getSelectedChildRows('${childModel.listName}').length" @click="handleDeleteSelectedChild('${childModel.listName}', '${childModel.pk.attrName}')">{{ t('common.delBtn') }}</el-button>`,
2354
2432
  ' </div>',
2355
- ' </div>',
2356
- ' <!-- 子表编辑表格:新增/删除按钮外置,表格内部隐藏新增、删除和序号控制列 -->',
2357
- ` <sc-form-table :ref="(el) => setChildTableRef('${childModel.listName}', el)" v-model="form.${childModel.listName}" :addTemplate="childTemp${childModel.className}" hide-index hide-add hide-delete @delete="(obj) => ${deleteExpression}" :placeholder="t('common.noData')" @selection-change="(rows) => handleChildSelectionChange('${childModel.listName}', rows)">`,
2358
- ' <el-table-column type="selection" width="55" :selectable="() => !detail" />',
2433
+ ' <!-- 子表编辑表格:新增/删除按钮外置,表格内部隐藏新增、删除和序号控制列 -->',
2434
+ ` <sc-form-table :ref="(el) => setChildTableRef('${childModel.listName}', el)" v-model="form.${childModel.listName}" :addTemplate="childTemp${childModel.className}" hide-index hide-add hide-delete @delete="(obj) => ${deleteExpression}" :placeholder="t('common.noData')" @selection-change="(rows) => handleChildSelectionChange('${childModel.listName}', rows)">`,
2435
+ ' <el-table-column type="selection" width="55" :selectable="() => !detail" />',
2359
2436
  childModel.visibleFields.map((field) => renderChildTableColumn(field, childModel.listName)).join('\n'),
2360
- ' </sc-form-table>',
2361
- ' </el-col>',
2362
- ].join('\n');
2363
- }
2437
+ ' </sc-form-table>',
2438
+ ' </el-col>',
2439
+ ' </el-row>',
2440
+ ' </el-collapse-item>',
2441
+ ].join('\n');
2442
+ }
2364
2443
 
2365
2444
  function renderChildFormListDefaults(children) {
2366
2445
  if (!children.length) return '';
@@ -2485,7 +2564,7 @@ function renderFieldCommentV2(field, indent = ' ') {
2485
2564
  return indent + '<!-- ' + label + ' -->';
2486
2565
  }
2487
2566
 
2488
- function renderFormFieldV2(field) {
2567
+ function renderFormFieldV2(field) {
2489
2568
  const prop = field.attrName;
2490
2569
  const labelExpr = `getMasterFieldLabel('${prop}')`;
2491
2570
  const dictExpr = `getMasterFieldMeta('${prop}')?.dictType`;
@@ -2584,10 +2663,58 @@ function renderFormFieldV2(field) {
2584
2663
  ` <el-input v-model="form.${prop}" :placeholder="formInputPlaceholder(${labelExpr}, ${disabledBool})"${maxlengthAttr}${disabledAttr} />`,
2585
2664
  ' </el-form-item>',
2586
2665
  ' </el-col>',
2587
- ].join('\n');
2588
- }
2589
-
2590
- function renderMultiLevelOptionField(field, model, moduleModel, dictRegistryRefs, indent = ' ') {
2666
+ ].join('\n');
2667
+ }
2668
+
2669
+ const FILL_REPORT_FIELD_NAMES = new Set(['createBy', 'createTime', 'finishedTime']);
2670
+ const FILL_REPORT_FIELD_ORDER = ['createBy', 'createTime', 'finishedTime'];
2671
+
2672
+ function isFillReportField(field) {
2673
+ return FILL_REPORT_FIELD_NAMES.has(String(field?.attrName || field?.fieldName || ''));
2674
+ }
2675
+
2676
+ function renderBusinessFormCollapseItem(title, name, fields) {
2677
+ return [
2678
+ ` <el-collapse-item title="${title}" name="${name}">`,
2679
+ ' <el-row :gutter="24">',
2680
+ fields.map(renderFormFieldV2).join('\n'),
2681
+ ' </el-row>',
2682
+ ' </el-collapse-item>',
2683
+ ].join('\n');
2684
+ }
2685
+
2686
+ function splitBusinessFormFields(fields) {
2687
+ const fillReportFields = fields
2688
+ .filter(isFillReportField)
2689
+ .sort((left, right) => FILL_REPORT_FIELD_ORDER.indexOf(left.attrName) - FILL_REPORT_FIELD_ORDER.indexOf(right.attrName));
2690
+ if (!fillReportFields.length) {
2691
+ return { basicFields: fields, fillReportFields: [] };
2692
+ }
2693
+ return {
2694
+ basicFields: fields.filter((field) => !isFillReportField(field)),
2695
+ fillReportFields,
2696
+ };
2697
+ }
2698
+
2699
+ function renderBusinessFormCollapseSections(model) {
2700
+ const fields = model.pageType === 'ledger' ? model.visibleFields.map(asReadonlyField) : model.visibleFields;
2701
+ const { basicFields, fillReportFields } = splitBusinessFormFields(fields);
2702
+ const sections = [renderBusinessFormCollapseItem('基本信息', '0', basicFields)];
2703
+ if (fillReportFields.length) {
2704
+ sections.push(renderBusinessFormCollapseItem('填报信息', '1', fillReportFields));
2705
+ }
2706
+ return sections.join('\n');
2707
+ }
2708
+
2709
+ function renderActiveCollapseNames(model) {
2710
+ const fields = model.pageType === 'ledger' ? model.visibleFields.map(asReadonlyField) : model.visibleFields;
2711
+ const names = ['0'];
2712
+ if (fields.some(isFillReportField)) names.push('1');
2713
+ model.children.forEach((childModel) => names.push(`child-${childModel.listName}`));
2714
+ return `[${names.map((name) => `'${name}'`).join(', ')}]`;
2715
+ }
2716
+
2717
+ function renderMultiLevelOptionField(field, model, moduleModel, dictRegistryRefs, indent = ' ') {
2591
2718
  const parts = [
2592
2719
  `key: '${field.attrName}'`,
2593
2720
  `labelKey: '${buildMultiLevelFieldLabelKey(model, moduleModel, field)}'`,
@@ -4250,10 +4377,12 @@ function buildReplacements(model, sharedSupport) {
4250
4377
  BUSINESS_FORM_KEEP_ALIVE_COLUMN: model.pageType === 'business' ? ', keep_alive' : '',
4251
4378
  BUSINESS_FORM_KEEP_ALIVE_VALUE: model.pageType === 'business' ? ", '1'" : '',
4252
4379
  BILL_CODE: deriveBillCode(model),
4253
- GENERATED_AT: new Date().toISOString(),
4254
- FORM_FIELDS: (model.pageType === 'ledger' ? model.visibleFields.map(asReadonlyField) : model.visibleFields).map(renderFormFieldV2).join('\n'),
4255
- TABLE_COLUMNS: model.gridFields.map((field) => renderTableColumn(field, dictRegistryRefs)).join('\n'),
4256
- FORM_DEFAULTS: renderFormDefaults(model),
4380
+ GENERATED_AT: new Date().toISOString(),
4381
+ FORM_FIELDS: (model.pageType === 'ledger' ? model.visibleFields.map(asReadonlyField) : model.visibleFields).map(renderFormFieldV2).join('\n'),
4382
+ FORM_COLLAPSE_SECTIONS: renderBusinessFormCollapseSections(model),
4383
+ ACTIVE_COLLAPSE_NAMES: renderActiveCollapseNames(model),
4384
+ TABLE_COLUMNS: model.gridFields.map((field) => renderTableColumn(field, dictRegistryRefs)).join('\n'),
4385
+ FORM_DEFAULTS: renderFormDefaults(model),
4257
4386
  DICT_REGISTRY_IMPORT_BLOCK: model.dictTypes.length ? "import { DictRegistry } from '/@/enums/dict-registry';" : '',
4258
4387
  MASTER_OPTION_FIELDS: model.optionFields.map((field) => renderOptionFieldV2(field, buildFieldLabelKey(model, field), dictRegistryRefs)).join('\n'),
4259
4388
  CHILD_OPTION_GROUPS: model.children.map((childModel) => renderChildOptionGroupV2(model, childModel, dictRegistryRefs)).join('\n'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worsoft-frontend-codegen-local-mcp",
3
- "version": "0.1.88",
3
+ "version": "0.1.89",
4
4
  "description": "Worsoft frontend local-template code generation MCP server.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "worsoft <sw@worsoft.vip>",