worsoft-frontend-codegen-local-mcp 0.1.90 → 0.1.91

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.
@@ -24,8 +24,9 @@
24
24
  <!-- 主子表表单:按 PRD 表单显隐和顺序渲染字段 -->
25
25
  <el-form ref="dataFormRef" :model="form" :rules="dataRules" :disabled="detail" v-loading="loading" label-width="120px">
26
26
  <el-collapse v-model="activeCollapseNames">
27
- {{FORM_COLLAPSE_SECTIONS}}
27
+ {{FORM_COLLAPSE_SECTIONS_BEFORE_CHILDREN}}
28
28
  {{CHILD_SECTIONS}}
29
+ {{FORM_COLLAPSE_SECTIONS_AFTER_CHILDREN}}
29
30
  </el-collapse>
30
31
  </el-form>
31
32
  </el-card>
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.90';
8
+ const SERVER_VERSION = '0.1.91';
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');
@@ -2567,19 +2567,68 @@ function renderDisabledBoolV2(field) {
2567
2567
  return field.readonly ? 'true' : 'false';
2568
2568
  }
2569
2569
 
2570
- function renderMicromeFormatAttr(field) {
2571
- const length = Number.parseInt(String(field.length || ''), 10);
2572
- const scale = Number.parseInt(String(field.scale || ''), 10);
2573
- if (!length || Number.isNaN(length) || Number.isNaN(scale) || scale < 0) return '';
2574
- const integerLength = Math.max(length - scale, 1);
2575
- return ` format="${integerLength}-${scale}"`;
2576
- }
2577
-
2578
- function isAttachmentLikeField(field) {
2579
- const fieldName = String(field?.fieldName || field?.attrName || '').toLowerCase();
2580
- const comment = String(field?.comment || field?.description || '').toLowerCase();
2581
- return fieldName.includes('attachment') || comment.includes('\u9644\u4ef6') || comment.includes('\u4e0a\u4f20');
2582
- }
2570
+ function renderMicromeFormatAttr(field) {
2571
+ const length = Number.parseInt(String(field.length || ''), 10);
2572
+ const scale = Number.parseInt(String(field.scale || ''), 10);
2573
+ if (!length || Number.isNaN(length) || Number.isNaN(scale) || scale < 0) return '';
2574
+ const integerLength = Math.max(length - scale, 1);
2575
+ return ` format="${integerLength}-${scale}"`;
2576
+ }
2577
+
2578
+ const FORM_LAYOUT_DOMAIN_GROUPS = [
2579
+ { key: 'contract', title: '合同信息', words: ['contract', 'sign', 'supplier', 'owner', 'customer', 'tax', 'term'] },
2580
+ { key: 'pay', title: '支付信息', words: ['pay', 'payable', 'paid', 'payment', 'advance', 'offset'] },
2581
+ { key: 'receive', title: '收款信息', words: ['receivable', 'receive', 'income', 'collect'] },
2582
+ { key: 'settle', title: '结算信息', words: ['settle', 'finsettle', 'accountingperiod'] },
2583
+ { key: 'warranty', title: '保证金/质保金信息', words: ['deposit', 'warranty', 'guarantee', 'return'] },
2584
+ { key: 'clause', title: '条款信息', words: ['breach', 'argue', 'clause', 'substance', 'content'] },
2585
+ { key: 'change', title: '变更信息', words: ['change', 'adjust', 'before', 'after'] },
2586
+ { key: 'claim', title: '索赔/扣款/折扣信息', words: ['claim', 'reply', 'deduct', 'discount'] },
2587
+ { key: 'work', title: '人员工作信息', words: ['pos', 'dept', 'ogn', 'induction', 'job', 'labor', 'retired', 'restart'] },
2588
+ { key: 'borrow', title: '借用/领用/使用信息', words: ['borrow', 'lend', 'use', 'requisition', 'admin'] },
2589
+ { key: 'document', title: '公文信息', words: ['document', 'reference', 'urgency', 'level', 'drafter', 'tomain', 'tocopy', 'tosubmit', 'year', 'no', 'proofread'] },
2590
+ ];
2591
+
2592
+ function getFormLayoutFieldText(field) {
2593
+ return [
2594
+ field?.attrName,
2595
+ field?.fieldName,
2596
+ field?.comment,
2597
+ field?.label,
2598
+ field?.description,
2599
+ field?.formType,
2600
+ field?.sqlType,
2601
+ ].filter(Boolean).join(' ').toLowerCase();
2602
+ }
2603
+
2604
+ function includesAnyLayoutWord(text, words) {
2605
+ const normalized = String(text || '').toLowerCase();
2606
+ return words.some((word) => normalized.includes(String(word).toLowerCase()));
2607
+ }
2608
+
2609
+ function isAttachmentLikeField(field) {
2610
+ const text = getFormLayoutFieldText(field);
2611
+ return field?.formType === 'upload'
2612
+ || includesAnyLayoutWord(text, ['attachment', 'file', 'upload', 'photo', 'sign'])
2613
+ || /附件|上传|图片|签名/.test(text);
2614
+ }
2615
+
2616
+ function computeFormFieldSpan(field) {
2617
+ const prop = String(field?.attrName || field?.fieldName || '');
2618
+ const propLower = prop.toLowerCase();
2619
+ const text = getFormLayoutFieldText(field);
2620
+
2621
+ if (['upload', 'textarea', 'richtext'].includes(field?.formType)) return 24;
2622
+ if (isAttachmentLikeField(field)) return 24;
2623
+ if (includesAnyLayoutWord(text, ['remark', 'remarks', 'reason', 'opinion', 'content', 'address', 'explain', 'description', 'substance', 'clause', 'breach', 'argue'])) return 24;
2624
+ if (/备注|原因|意见|内容|地址|说明|条款|违约|争议/.test(text)) return 24;
2625
+ if (propLower.includes('keywords')) return 18;
2626
+ if (prop === 'projectName') return 12;
2627
+ if (includesAnyLayoutWord(propLower, ['fullname', 'orgname', 'deptname', 'ognname', 'companyname', 'suppliername', 'customername', 'unitname'])) return 12;
2628
+ if (/项目名称|完整名称|组织名称|部门名称|单位名称|供应商名称|客户名称/.test(text)) return 12;
2629
+
2630
+ return 6;
2631
+ }
2583
2632
 
2584
2633
  function renderFieldCommentV2(field, indent = ' ') {
2585
2634
  const label = stripDictAnnotation(field.comment || field.attrName).replace(/-->/g, '').trim() || field.attrName;
@@ -2587,27 +2636,28 @@ function renderFieldCommentV2(field, indent = ' ') {
2587
2636
  }
2588
2637
 
2589
2638
  function renderFormFieldV2(field) {
2590
- const prop = field.attrName;
2591
- const labelExpr = `getMasterFieldLabel('${prop}')`;
2592
- const dictExpr = `getMasterFieldMeta('${prop}')?.dictType`;
2593
- const disabledAttr = renderDisabledAttrV2(field);
2594
- const disabledBool = renderDisabledBoolV2(field);
2595
-
2596
- if (field.formType === 'upload') {
2597
- return [
2598
- renderFieldCommentV2(field),
2599
- ` <el-col :span="24" class="mb20">`,
2600
- ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2601
- ` <UploadFile v-model="form.${prop}"${disabledAttr} />`,
2639
+ const prop = field.attrName;
2640
+ const labelExpr = `getMasterFieldLabel('${prop}')`;
2641
+ const dictExpr = `getMasterFieldMeta('${prop}')?.dictType`;
2642
+ const disabledAttr = renderDisabledAttrV2(field);
2643
+ const disabledBool = renderDisabledBoolV2(field);
2644
+ const span = computeFormFieldSpan(field);
2645
+
2646
+ if (field.formType === 'upload') {
2647
+ return [
2648
+ renderFieldCommentV2(field),
2649
+ ` <el-col :span="${span}" class="mb20">`,
2650
+ ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2651
+ ` <UploadFile v-model="form.${prop}"${disabledAttr} />`,
2602
2652
  ' </el-form-item>',
2603
2653
  ' </el-col>',
2604
2654
  ].join('\n');
2605
2655
  }
2606
2656
 
2607
- if (field.formType === 'select') {
2608
- return [
2609
- renderFieldCommentV2(field),
2610
- ` <el-col :span="12" class="mb20">`,
2657
+ if (field.formType === 'select') {
2658
+ return [
2659
+ renderFieldCommentV2(field),
2660
+ ` <el-col :span="${span}" class="mb20">`,
2611
2661
  ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2612
2662
  ` <el-select v-model="form.${prop}" :placeholder="formSelectPlaceholder(${labelExpr}, ${disabledBool})" style="width: 100%"${disabledAttr}>`,
2613
2663
  ` <el-option v-for="item in getDictOptions(${dictExpr})" :key="item.value" :label="item.label" :value="item.value" />`,
@@ -2617,12 +2667,12 @@ function renderFormFieldV2(field) {
2617
2667
  ].join('\n');
2618
2668
  }
2619
2669
 
2620
- if (field.formType === 'number') {
2670
+ if (field.formType === 'number') {
2621
2671
  const max = field.comment.includes('%') || /\u6BD4\u4F8B/.test(field.comment) ? ' :max="100"' : '';
2622
2672
  const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
2623
- return [
2624
- renderFieldCommentV2(field),
2625
- ` <el-col :span="12" class="mb20">`,
2673
+ return [
2674
+ renderFieldCommentV2(field),
2675
+ ` <el-col :span="${span}" class="mb20">`,
2626
2676
  ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2627
2677
  ` <el-input-number v-model="form.${prop}" :min="0"${max}${precision} :placeholder="formInputPlaceholder(${labelExpr}, ${disabledBool})" style="width: 100%"${disabledAttr} />`,
2628
2678
  ' </el-form-item>',
@@ -2630,10 +2680,10 @@ function renderFormFieldV2(field) {
2630
2680
  ].join('\n');
2631
2681
  }
2632
2682
 
2633
- if (field.formType === 'microme-operator') {
2634
- return [
2635
- renderFieldCommentV2(field),
2636
- ` <el-col :span="12" class="mb20">`,
2683
+ if (field.formType === 'microme-operator') {
2684
+ return [
2685
+ renderFieldCommentV2(field),
2686
+ ` <el-col :span="${span}" class="mb20">`,
2637
2687
  ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2638
2688
  ` <MicromeOperator v-model="form.${prop}"${renderMicromeFormatAttr(field)} :placeholder="formInputPlaceholder(${labelExpr}, ${disabledBool})"${disabledAttr} />`,
2639
2689
  ' </el-form-item>',
@@ -2641,23 +2691,12 @@ function renderFormFieldV2(field) {
2641
2691
  ].join('\n');
2642
2692
  }
2643
2693
 
2644
- if (field.formType === 'microme-operator') {
2645
- return [
2646
- renderFieldCommentV2(field),
2647
- ' <el-col :span="12" class="mb20">',
2648
- ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2649
- ` <MicromeOperator v-model="form.${prop}"${renderMicromeFormatAttr(field)} :placeholder="formInputPlaceholder(${labelExpr}, ${disabledBool})"${disabledAttr} />`,
2650
- ' </el-form-item>',
2651
- ' </el-col>',
2652
- ].join('\n');
2653
- }
2654
-
2655
- if (field.formType === 'datetime' || field.formType === 'date') {
2694
+ if (field.formType === 'datetime' || field.formType === 'date') {
2656
2695
  const pickerType = field.formType === 'datetime' ? 'datetime' : 'date';
2657
2696
  const formatName = field.formType === 'datetime' ? 'dateTimeStr' : 'dateStr';
2658
- return [
2659
- renderFieldCommentV2(field),
2660
- ` <el-col :span="12" class="mb20">`,
2697
+ return [
2698
+ renderFieldCommentV2(field),
2699
+ ` <el-col :span="${span}" class="mb20">`,
2661
2700
  ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2662
2701
  ` <el-date-picker type="${pickerType}" :placeholder="formInputPlaceholder(${labelExpr}, ${disabledBool})" v-model="form.${prop}" :value-format="${formatName}" style="width: 100%"${disabledAttr}></el-date-picker>`,
2663
2702
  ' </el-form-item>',
@@ -2665,11 +2704,11 @@ function renderFormFieldV2(field) {
2665
2704
  ].join('\n');
2666
2705
  }
2667
2706
 
2668
- if (field.formType === 'textarea') {
2707
+ if (field.formType === 'textarea') {
2669
2708
  const textareaAttrs = renderTextareaMaxlengthAttrsV2(field);
2670
- return [
2671
- renderFieldCommentV2(field),
2672
- ` <el-col :span="24" class="mb20">`,
2709
+ return [
2710
+ renderFieldCommentV2(field),
2711
+ ` <el-col :span="${span}" class="mb20">`,
2673
2712
  ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2674
2713
  ` <el-input type="textarea" v-model="form.${prop}" :placeholder="formInputPlaceholder(${labelExpr}, ${disabledBool})"${textareaAttrs}${disabledAttr} />`,
2675
2714
  ' </el-form-item>',
@@ -2678,9 +2717,9 @@ function renderFormFieldV2(field) {
2678
2717
  }
2679
2718
 
2680
2719
  const maxlengthAttr = renderInputMaxlengthAttr(field);
2681
- return [
2682
- renderFieldCommentV2(field),
2683
- ` <el-col :span="12" class="mb20">`,
2720
+ return [
2721
+ renderFieldCommentV2(field),
2722
+ ` <el-col :span="${span}" class="mb20">`,
2684
2723
  ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
2685
2724
  ` <el-input v-model="form.${prop}" :placeholder="formInputPlaceholder(${labelExpr}, ${disabledBool})"${maxlengthAttr}${disabledAttr} />`,
2686
2725
  ' </el-form-item>',
@@ -2688,8 +2727,8 @@ function renderFormFieldV2(field) {
2688
2727
  ].join('\n');
2689
2728
  }
2690
2729
 
2691
- const FILL_REPORT_FIELD_NAMES = new Set(['createBy', 'createTime', 'finishedTime']);
2692
- const FILL_REPORT_FIELD_ORDER = ['createBy', 'createTime', 'finishedTime'];
2730
+ const FILL_REPORT_FIELD_NAMES = new Set(['createBy', 'createUser', 'createTime', 'finishedTime']);
2731
+ const FILL_REPORT_FIELD_ORDER = ['createBy', 'createUser', 'createTime', 'finishedTime'];
2693
2732
 
2694
2733
  function isFillReportField(field) {
2695
2734
  return FILL_REPORT_FIELD_NAMES.has(String(field?.attrName || field?.fieldName || ''));
@@ -2705,34 +2744,102 @@ function renderBusinessFormCollapseItem(title, name, fields) {
2705
2744
  ].join('\n');
2706
2745
  }
2707
2746
 
2708
- function splitBusinessFormFields(fields) {
2709
- const fillReportFields = fields
2710
- .filter(isFillReportField)
2711
- .sort((left, right) => FILL_REPORT_FIELD_ORDER.indexOf(left.attrName) - FILL_REPORT_FIELD_ORDER.indexOf(right.attrName));
2712
- if (!fillReportFields.length) {
2713
- return { basicFields: fields, fillReportFields: [] };
2714
- }
2715
- return {
2716
- basicFields: fields.filter((field) => !isFillReportField(field)),
2717
- fillReportFields,
2718
- };
2747
+ function getFormFieldOriginalIndex(fields, field) {
2748
+ return fields.indexOf(field);
2719
2749
  }
2720
2750
 
2721
- function renderBusinessFormCollapseSections(model) {
2751
+ function classifyBusinessFormDomain(field) {
2752
+ const text = getFormLayoutFieldText(field).replace(/[_\-\s]/g, '');
2753
+ const group = FORM_LAYOUT_DOMAIN_GROUPS.find((item) => item.words.some((word) => text.includes(String(word).toLowerCase())));
2754
+ return group?.key || 'basic';
2755
+ }
2756
+
2757
+ function createBusinessFormLayoutSections(model) {
2722
2758
  const fields = model.pageType === 'ledger' ? model.visibleFields.map(asReadonlyField) : model.visibleFields;
2723
- const { basicFields, fillReportFields } = splitBusinessFormFields(fields);
2724
- const sections = [renderBusinessFormCollapseItem('基本信息', '0', basicFields)];
2759
+ const basicFields = [];
2760
+ const attachmentFields = [];
2761
+ const fillReportFields = [];
2762
+ const domainBuckets = new Map();
2763
+
2764
+ fields.forEach((field) => {
2765
+ if (isFillReportField(field)) {
2766
+ fillReportFields.push(field);
2767
+ return;
2768
+ }
2769
+ if (isAttachmentLikeField(field)) {
2770
+ attachmentFields.push(field);
2771
+ return;
2772
+ }
2773
+ const domain = classifyBusinessFormDomain(field);
2774
+ if (domain === 'basic') {
2775
+ basicFields.push(field);
2776
+ return;
2777
+ }
2778
+ if (!domainBuckets.has(domain)) domainBuckets.set(domain, []);
2779
+ domainBuckets.get(domain).push(field);
2780
+ });
2781
+
2782
+ const beforeChildren = [{ title: '基本信息', fields: basicFields }];
2783
+
2784
+ FORM_LAYOUT_DOMAIN_GROUPS.forEach((group) => {
2785
+ const bucket = domainBuckets.get(group.key) || [];
2786
+ if (bucket.length >= 3) {
2787
+ beforeChildren.push({ title: group.title, fields: bucket });
2788
+ } else {
2789
+ beforeChildren[0].fields.push(...bucket);
2790
+ }
2791
+ });
2792
+
2793
+ const afterChildren = [];
2794
+ if (attachmentFields.length >= 2) {
2795
+ afterChildren.push({ title: '附件信息', fields: attachmentFields });
2796
+ } else {
2797
+ beforeChildren[0].fields.push(...attachmentFields);
2798
+ }
2799
+
2800
+ beforeChildren.concat(afterChildren).forEach((section) => {
2801
+ section.fields.sort((left, right) => getFormFieldOriginalIndex(fields, left) - getFormFieldOriginalIndex(fields, right));
2802
+ });
2803
+
2725
2804
  if (fillReportFields.length) {
2726
- sections.push(renderBusinessFormCollapseItem('填报信息', '1', fillReportFields));
2805
+ afterChildren.push({
2806
+ title: '填报信息',
2807
+ fields: fillReportFields.sort((left, right) => FILL_REPORT_FIELD_ORDER.indexOf(left.attrName) - FILL_REPORT_FIELD_ORDER.indexOf(right.attrName)),
2808
+ });
2727
2809
  }
2728
- return sections.join('\n');
2810
+
2811
+ let nameIndex = 0;
2812
+ beforeChildren.filter((section) => section.fields.length).forEach((section) => {
2813
+ section.name = String(nameIndex++);
2814
+ });
2815
+ afterChildren.filter((section) => section.fields.length).forEach((section) => {
2816
+ section.name = String(nameIndex++);
2817
+ });
2818
+
2819
+ return {
2820
+ beforeChildren: beforeChildren.filter((section) => section.fields.length),
2821
+ afterChildren: afterChildren.filter((section) => section.fields.length),
2822
+ };
2823
+ }
2824
+
2825
+ function renderBusinessFormCollapseSections(model, placement = 'all') {
2826
+ const layout = createBusinessFormLayoutSections(model);
2827
+ const sections =
2828
+ placement === 'beforeChildren'
2829
+ ? layout.beforeChildren
2830
+ : placement === 'afterChildren'
2831
+ ? layout.afterChildren
2832
+ : [...layout.beforeChildren, ...layout.afterChildren];
2833
+ return sections.map((section) => renderBusinessFormCollapseItem(section.title, section.name, section.fields)).join('\n');
2729
2834
  }
2730
2835
 
2731
2836
  function renderActiveCollapseNames(model) {
2732
- const fields = model.pageType === 'ledger' ? model.visibleFields.map(asReadonlyField) : model.visibleFields;
2733
- const names = ['0'];
2734
- if (fields.some(isFillReportField)) names.push('1');
2735
- model.children.forEach((childModel) => names.push(`child-${childModel.listName}`));
2837
+ const layout = createBusinessFormLayoutSections(model);
2838
+ const names = [
2839
+ ...layout.beforeChildren.map((section) => section.name),
2840
+ ...model.children.map((childModel) => `child-${childModel.listName}`),
2841
+ ...layout.afterChildren.map((section) => section.name),
2842
+ ];
2736
2843
  return `[${names.map((name) => `'${name}'`).join(', ')}]`;
2737
2844
  }
2738
2845
 
@@ -4402,6 +4509,8 @@ function buildReplacements(model, sharedSupport) {
4402
4509
  GENERATED_AT: new Date().toISOString(),
4403
4510
  FORM_FIELDS: (model.pageType === 'ledger' ? model.visibleFields.map(asReadonlyField) : model.visibleFields).map(renderFormFieldV2).join('\n'),
4404
4511
  FORM_COLLAPSE_SECTIONS: renderBusinessFormCollapseSections(model),
4512
+ FORM_COLLAPSE_SECTIONS_BEFORE_CHILDREN: renderBusinessFormCollapseSections(model, 'beforeChildren'),
4513
+ FORM_COLLAPSE_SECTIONS_AFTER_CHILDREN: renderBusinessFormCollapseSections(model, 'afterChildren'),
4405
4514
  ACTIVE_COLLAPSE_NAMES: renderActiveCollapseNames(model),
4406
4515
  TABLE_COLUMNS: model.gridFields.map((field) => renderTableColumn(field, dictRegistryRefs)).join('\n'),
4407
4516
  FORM_DEFAULTS: renderFormDefaults(model),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worsoft-frontend-codegen-local-mcp",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
4
4
  "description": "Worsoft frontend local-template code generation MCP server.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "worsoft <sw@worsoft.vip>",