worsoft-frontend-codegen-local-mcp 0.1.5 → 0.1.6

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.
@@ -1,8 +1,20 @@
1
1
  <template>
2
2
  <div class="layout-padding-auto layout-padding-view">
3
- <el-card shadow="never">
3
+ <el-card shadow="never" class="w100">
4
4
  <template #header>
5
- <span>{{ form.{{PK_ATTR}} ? (detail ? '查看' : '编辑') : '新增' }}</span>
5
+ <div style="display: flex; justify-content: space-between; align-items: center;">
6
+ <span style="font-size: 16px; font-weight: bold;">{{ form.{{PK_ATTR}} ? (detail ? t('common.viewBtn') : t('common.editBtn')) : t('common.addBtn') }}</span>
7
+ <div v-if="!detail">
8
+ <el-button @click="onSubmit('save')" :disabled="loading" icon="document">{{ actionLabel('save') }}</el-button>
9
+ <el-button @click="onSubmit('flow')" :disabled="loading" plain type="success" icon="position">{{ actionLabel('flow') }}</el-button>
10
+ <el-button @click="onSubmit('submit')" :disabled="loading" type="primary" icon="check">{{ actionLabel('submit') }}</el-button>
11
+ <el-divider direction="vertical" />
12
+ <el-button @click="handleBack" icon="close">{{ t('common.cancelButtonText') }}</el-button>
13
+ </div>
14
+ <div v-else>
15
+ <el-button @click="handleBack" icon="back">{{ actionLabel('back') }}</el-button>
16
+ </div>
17
+ </div>
6
18
  </template>
7
19
  <el-form ref="dataFormRef" :model="form" :rules="dataRules" :disabled="detail" v-loading="loading">
8
20
  <el-row :gutter="24">
@@ -12,10 +24,6 @@
12
24
  {{CHILD_SECTIONS}}
13
25
  </el-row>
14
26
  </el-form>
15
- <div class="dialog-footer" style="text-align: right; margin-top: 18px;">
16
- <el-button @click="handleBack">取消</el-button>
17
- <el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
18
- </div>
19
27
  </el-card>
20
28
  </div>
21
29
  </template>
@@ -25,13 +33,16 @@ import mittBus from '/@/utils/mitt';
25
33
  import { useMessage } from '/@/hooks/message';
26
34
  import { getObj, addObj, putObj, delChildObj } from '/@/api/{{API_MODULE_PATH}}';
27
35
  import { useDict } from '/@/hooks/dict';
36
+ import { useI18n } from 'vue-i18n';
28
37
  import { allDictTypes, childFieldGroups, dataMasterEntity } from './options';
29
38
 
30
39
  const dictRefs = useDict(...allDictTypes);
40
+ const { t } = useI18n();
31
41
 
32
42
  const scFormTable = defineAsyncComponent(() => import('/@/components/FormTable/index.vue'));
33
43
  const route = useRoute();
34
44
  const router = useRouter();
45
+ const pageI18nKey = '{{I18N_NAMESPACE}}';
35
46
 
36
47
  const dataFormRef = ref();
37
48
  const loading = ref(false);
@@ -45,9 +56,25 @@ const isMasterFieldVisible = (prop: string) => {
45
56
  return Boolean(config) && !config.alwaysHide && config.show !== false;
46
57
  };
47
58
  const getChildFieldMeta = (groupName: string, prop: string) => childFieldGroups[groupName]?.[prop];
59
+ const resolveLabel = (labelKey?: string, fallback = '') => {
60
+ if (!labelKey) return fallback;
61
+ const translated = t(labelKey);
62
+ return translated === labelKey ? fallback : translated;
63
+ };
64
+ const getMasterFieldLabel = (prop: string) => {
65
+ const config = getMasterFieldMeta(prop);
66
+ return resolveLabel(config?.labelKey, config?.label || prop);
67
+ };
68
+ const getChildFieldLabel = (groupName: string, prop: string) => {
69
+ const config = getChildFieldMeta(groupName, prop);
70
+ return resolveLabel(config?.labelKey, config?.label || prop);
71
+ };
72
+ const childSectionTitle = (groupName: string) => t(`${pageI18nKey}.children.${groupName}.title`);
48
73
  const getDictOptions = (dictType?: string) => (dictType ? dictRefs[dictType]?.value || [] : []);
49
- const inputPlaceholder = (label: string) => `请输入${label}`;
50
- const selectPlaceholder = (label: string) => `请选择${label}`;
74
+ const inputPlaceholder = (label: string) => t(`${pageI18nKey}.placeholders.input`, { label });
75
+ const selectPlaceholder = (label: string) => t(`${pageI18nKey}.placeholders.select`, { label });
76
+ const fieldRequiredMessage = (prop: string) => t(`${pageI18nKey}.messages.required`, { label: getMasterFieldLabel(prop) });
77
+ const actionLabel = (action: string) => t(`${pageI18nKey}.actions.${action}`);
51
78
 
52
79
  const form = reactive({
53
80
  {{FORM_DEFAULTS}}
@@ -66,7 +93,7 @@ const get{{CLASS_NAME}}Data = async (id: string) => {
66
93
  const { data } = await getObj({ {{PK_ATTR}}: id });
67
94
  Object.assign(form, data[0] || {});
68
95
  } catch (error) {
69
- useMessage().error('获取数据失败');
96
+ useMessage().error(t(`${pageI18nKey}.messages.fetchError`));
70
97
  } finally {
71
98
  loading.value = false;
72
99
  }
@@ -104,7 +131,7 @@ const handleBack = () => {
104
131
  router.back();
105
132
  };
106
133
 
107
- const onSubmit = async () => {
134
+ const onSubmit = async (actionType?: string) => {
108
135
  loading.value = true;
109
136
 
110
137
  const valid = await dataFormRef.value.validate().catch(() => {});
@@ -115,10 +142,15 @@ const onSubmit = async () => {
115
142
 
116
143
  try {
117
144
  form.{{PK_ATTR}} ? await putObj(form) : await addObj(form);
118
- useMessage().success(form.{{PK_ATTR}} ? '修改成功' : '添加成功');
145
+
146
+ let msg = form.{{PK_ATTR}} ? t(`${pageI18nKey}.messages.updateSuccess`) : t(`${pageI18nKey}.messages.createSuccess`);
147
+ if (actionType === 'submit') msg = t(`${pageI18nKey}.messages.quickSubmitSuccess`);
148
+ if (actionType === 'flow') msg = t(`${pageI18nKey}.messages.quickFlowSuccess`);
149
+
150
+ useMessage().success(msg);
119
151
  closeCurrentPage();
120
152
  } catch (err: any) {
121
- useMessage().error(err.msg || '提交失败');
153
+ useMessage().error(err.msg || t(`${pageI18nKey}.messages.submitError`));
122
154
  } finally {
123
155
  loading.value = false;
124
156
  }
@@ -128,9 +160,9 @@ const deleteChild = async (obj: Record<string, any>, childPkAttr: string, childT
128
160
  if (obj[childPkAttr]) {
129
161
  try {
130
162
  await delChildObj([obj[childPkAttr]], childTableName);
131
- useMessage().success('删除成功');
163
+ useMessage().success(t('common.delSuccessText'));
132
164
  } catch (err: any) {
133
- useMessage().error(err.msg || '删除失败');
165
+ useMessage().error(err.msg || t('common.delBtn'));
134
166
  }
135
167
  }
136
168
  };
@@ -1,32 +1,33 @@
1
1
  <template>
2
2
  <div class="layout-padding">
3
- <div class="layout-padding-auto layout-padding-view">
4
- <el-row>
5
- <div class="mb8" style="width: 100%">
6
- <el-button icon="folder-add" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="handleCreate">新增</el-button>
7
- <el-button plain icon="upload-filled" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="excelUploadRef.show()">导入</el-button>
8
- <el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete(selectObjs)">删除</el-button>
9
- <right-toolbar v-model:showSearch="showSearch" :export="'{{PERMISSION_PREFIX}}_export'" @exportExcel="exportExcel" @queryTable="getDataList" class="ml10 mr20" style="float: right;" />
10
- </div>
11
- </el-row>
12
-
13
- <el-table
14
- :data="state.dataList"
15
- v-loading="state.loading"
16
- border
17
- :cell-style="tableStyle.cellStyle"
18
- :header-cell-style="tableStyle.headerCellStyle"
19
- @selection-change="selectionChangeHandle"
20
- @sort-change="sortChangeHandle"
21
- >
3
+ <div class="layout-padding-auto layout-padding-view flex-column" style="display: flex; flex-direction: column; height: 100%;">
4
+ <div class="mb8" style="width: 100%; flex-shrink: 0;">
5
+ <el-button icon="folder-add" type="primary" class="ml10" @click="handleCreate">{{ t('common.addBtn') }}</el-button>
6
+ <el-button plain icon="upload-filled" type="primary" class="ml10" @click="excelUploadRef.show()">{{ t('common.importBtn') }}</el-button>
7
+ <el-button plain :disabled="multiple" icon="Delete" type="primary" @click="handleDelete(selectObjs)">{{ t('common.delBtn') }}</el-button>
8
+ <right-toolbar v-model:showSearch="showSearch" :export="'{{PERMISSION_PREFIX}}_export'" @exportExcel="exportExcel" @queryTable="getDataList" class="ml10 mr20" style="float: right;" />
9
+ </div>
10
+
11
+ <div style="flex: 1; overflow: hidden; display: flex; flex-direction: column;">
12
+ <el-table
13
+ :data="state.dataList"
14
+ v-loading="state.loading"
15
+ border
16
+ height="100%"
17
+ style="width: 100%;"
18
+ :cell-style="tableStyle.cellStyle"
19
+ :header-cell-style="tableStyle.headerCellStyle"
20
+ @selection-change="selectionChangeHandle"
21
+ @sort-change="sortChangeHandle"
22
+ >
22
23
  <el-table-column type="selection" width="40" align="center" />
23
- <el-table-column type="index" label="#" width="40" />
24
+ <el-table-column type="index" :label="t('common.serial')" width="60" />
24
25
  <el-table-column
25
26
  v-for="column in visibleTableColumns"
26
27
  :key="column.prop"
27
28
  :prop="column.prop"
28
- :label="column.label"
29
- :width="column.width"
29
+ :label="resolveLabel(column.labelKey, column.fallbackLabel)"
30
+ :min-width="column.width"
30
31
  show-overflow-tooltip
31
32
  >
32
33
  <template #default="scope">
@@ -34,19 +35,24 @@
34
35
  <span v-else>{{ scope.row[column.prop] }}</span>
35
36
  </template>
36
37
  </el-table-column>
37
- <el-table-column label="操作" width="220">
38
+ <el-table-column :label="t('common.action')" width="380" fixed="right">
38
39
  <template #default="scope">
39
- <el-button text type="primary" icon="view" v-auth="'{{PERMISSION_PREFIX}}_view'" @click="handleDetail(scope.row.{{PK_ATTR}})">查看</el-button>
40
- <el-button icon="edit-pen" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_edit'" @click="handleEdit(scope.row.{{PK_ATTR}})">编辑</el-button>
41
- <el-button icon="delete" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete([scope.row.{{PK_ATTR}}])">删除</el-button>
40
+ <el-button text type="primary" icon="view" v-auth="'{{PERMISSION_PREFIX}}_view'" @click="handleDetail(scope.row.id)">{{ t('common.viewBtn') }}</el-button>
41
+ <el-button icon="edit-pen" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_edit'" @click="handleEdit(scope.row.id)">{{ t('common.editBtn') }}</el-button>
42
+ <el-button text type="primary" icon="check" @click="handleQuickAction(scope.row, 'submit')">{{ actionLabel('submit') }}</el-button>
43
+ <el-button text type="success" icon="position" @click="handleQuickAction(scope.row, 'flow')">{{ actionLabel('flow') }}</el-button>
44
+ <el-button icon="delete" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete([scope.row.id])">{{ t('common.delBtn') }}</el-button>
42
45
  </template>
43
46
  </el-table-column>
44
- </el-table>
47
+ </el-table>
48
+ </div>
45
49
 
46
- <pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
50
+ <div style="flex-shrink: 0; margin-top: 10px; display: flex; justify-content: flex-end;">
51
+ <pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
52
+ </div>
47
53
  </div>
48
54
 
49
- <upload-excel ref="excelUploadRef" title="导入" url="/{{API_PATH}}/import" temp-url="/admin/sys-file/local/file/{{FUNCTION_NAME}}.xlsx" @refreshDataList="getDataList" />
55
+ <upload-excel ref="excelUploadRef" :title="t('common.importBtn')" url="/{{API_PATH}}/import" temp-url="/admin/sys-file/local/file/{{FUNCTION_NAME}}.xlsx" @refreshDataList="getDataList" />
50
56
  </div>
51
57
  </template>
52
58
 
@@ -55,10 +61,13 @@ import { BasicTableProps, useTable } from '/@/hooks/table';
55
61
  import { fetchList, delObjs } from '/@/api/{{API_MODULE_PATH}}';
56
62
  import { useMessage, useMessageBox } from '/@/hooks/message';
57
63
  import { useDict } from '/@/hooks/dict';
64
+ import { useI18n } from 'vue-i18n';
58
65
  import { allDictTypes, dataMasterEntity } from './options';
59
66
 
60
67
  const dictRefs = useDict(...allDictTypes);
68
+ const { t } = useI18n();
61
69
  const router = useRouter();
70
+ const pageI18nKey = '{{I18N_NAMESPACE}}';
62
71
 
63
72
  const excelUploadRef = ref();
64
73
  const showSearch = ref(true);
@@ -70,12 +79,21 @@ const state: BasicTableProps = reactive<BasicTableProps>({
70
79
  pageList: fetchList,
71
80
  });
72
81
 
82
+ const resolveLabel = (labelKey?: string, fallback = '') => {
83
+ if (!labelKey) return fallback;
84
+ const translated = t(labelKey);
85
+ return translated === labelKey ? fallback : translated;
86
+ };
87
+
88
+ const actionLabel = (action: string) => t(`${pageI18nKey}.actions.${action}`);
89
+
73
90
  const visibleTableColumns = computed(() =>
74
91
  Object.entries(dataMasterEntity)
75
92
  .filter(([, config]) => !config.alwaysHide && config.show !== false)
76
93
  .map(([prop, config]) => ({
77
94
  prop,
78
- label: config.label,
95
+ labelKey: config.labelKey,
96
+ fallbackLabel: config.label || prop,
79
97
  width: config.width,
80
98
  dictType: config.dictType || '',
81
99
  }))
@@ -88,29 +106,42 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, do
88
106
  const getFormPath = () => '/{{VIEW_MODULE_PATH}}/form';
89
107
 
90
108
  const handleCreate = () => {
91
- router.push({ path: getFormPath(), query: { tagsViewName: '新增' } });
109
+ router.push({ path: getFormPath(), query: { tagsViewName: t('common.addBtn') } });
92
110
  };
93
111
 
94
112
  const handleDetail = (id: string) => {
95
- router.push({ path: getFormPath(), query: { id, detail: '1', tagsViewName: '查看' } });
113
+ router.push({ path: getFormPath(), query: { id, detail: '1', tagsViewName: t('common.viewBtn') } });
96
114
  };
97
115
 
98
116
  const handleEdit = (id: string) => {
99
- router.push({ path: getFormPath(), query: { id, tagsViewName: '编辑' } });
117
+ router.push({ path: getFormPath(), query: { id, tagsViewName: t('common.editBtn') } });
118
+ };
119
+
120
+ const handleQuickAction = async (row: any, actionType: string) => {
121
+ const actionName = actionLabel(actionType);
122
+ try {
123
+ await useMessageBox().confirm(t(`${pageI18nKey}.messages.quickActionConfirm`, { action: actionName }));
124
+ useMessage().success(
125
+ actionType === 'submit'
126
+ ? t(`${pageI18nKey}.messages.quickSubmitSuccess`)
127
+ : t(`${pageI18nKey}.messages.quickFlowSuccess`)
128
+ );
129
+ getDataList();
130
+ } catch {}
100
131
  };
101
132
 
102
133
  const exportExcel = () => {
103
134
  downBlobFile('/{{API_PATH}}/export', { ...state.queryForm, ids: selectObjs.value }, '{{FUNCTION_NAME}}.xlsx');
104
135
  };
105
136
 
106
- const selectionChangeHandle = (objs: { {{PK_ATTR}}: string }[]) => {
107
- selectObjs.value = objs.map((item) => item.{{PK_ATTR}});
137
+ const selectionChangeHandle = (objs: { id: string }[]) => {
138
+ selectObjs.value = objs.map((item) => item.id);
108
139
  multiple.value = !objs.length;
109
140
  };
110
141
 
111
142
  const handleDelete = async (ids: string[]) => {
112
143
  try {
113
- await useMessageBox().confirm('此操作将永久删除');
144
+ await useMessageBox().confirm(t('common.delConfirmText'));
114
145
  } catch {
115
146
  return;
116
147
  }
@@ -118,9 +149,9 @@ const handleDelete = async (ids: string[]) => {
118
149
  try {
119
150
  await delObjs(ids);
120
151
  getDataList();
121
- useMessage().success('删除成功');
152
+ useMessage().success(t('common.delSuccessText'));
122
153
  } catch (err: any) {
123
- useMessage().error(err.msg || '删除失败');
154
+ useMessage().error(err.msg || t('common.delBtn'));
124
155
  }
125
156
  };
126
157
  </script>
@@ -4,29 +4,21 @@ import type { CrudSchemaDefinition } from '/@/utils/crudSchema';
4
4
 
5
5
  /**
6
6
  * {{TABLE_NAME}} 页面字段声明。
7
- * 这里只维护字段清单,不手写派生配置。
7
+ * 这里只维护字段 key、双语 key、字典类型和显示元数据。
8
8
  */
9
9
  const definition: CrudSchemaDefinition = {
10
- // 主表字段
11
10
  master: [
12
11
  {{MASTER_OPTION_FIELDS}}
13
12
  ],
14
- // 子表字段,key 对应 form.vue 中的列表字段名
15
13
  children: {
16
14
  {{CHILD_OPTION_GROUPS}}
17
15
  },
18
16
  };
19
17
 
20
- // 统一派生页面所需配置,避免在 options 文件里重复手写
21
18
  const schema = createCrudSchema(definition);
22
19
 
23
- // 主表字段配置:列表页、主表表单共用
24
20
  export const dataMasterEntity = schema.master;
25
- // 子表字段配置:主子表 form 页使用
26
21
  export const childFieldGroups = schema.children;
27
- // 主表字典筛选配置
28
22
  export const filterTypes = schema.filterTypes;
29
- // 子表字典筛选配置
30
23
  export const childFilterTypes = schema.childFilterTypes;
31
- // 当前功能涉及的全部字典编码
32
24
  export const allDictTypes = schema.allDictTypes;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <el-dialog v-model="visible" :title="form.{{PK_ATTR}} ? '编辑' : '新增'" :close-on-click-modal="false" draggable>
2
+ <el-dialog v-model="visible" :title="form.{{PK_ATTR}} ? t('common.editBtn') : t('common.addBtn')" :close-on-click-modal="false" draggable>
3
3
  <el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="100px" v-loading="loading">
4
4
  <el-row :gutter="24">
5
5
  {{FORM_FIELDS}}
@@ -7,8 +7,8 @@
7
7
  </el-form>
8
8
  <template #footer>
9
9
  <span class="dialog-footer">
10
- <el-button @click="visible = false">取消</el-button>
11
- <el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
10
+ <el-button @click="visible = false">{{ t('common.cancelButtonText') }}</el-button>
11
+ <el-button type="primary" @click="onSubmit" :disabled="loading">{{ t('common.confirmButtonText') }}</el-button>
12
12
  </span>
13
13
  </template>
14
14
  </el-dialog>
@@ -18,9 +18,12 @@
18
18
  import { useMessage } from '/@/hooks/message';
19
19
  import { getObj, addObj, putObj } from '/@/api/{{API_MODULE_PATH}}';
20
20
  import { useDict } from '/@/hooks/dict';
21
+ import { useI18n } from 'vue-i18n';
21
22
  import { allDictTypes, dataMasterEntity } from './options';
22
23
 
23
24
  const dictRefs = useDict(...allDictTypes);
25
+ const { t } = useI18n();
26
+ const pageI18nKey = '{{I18N_NAMESPACE}}';
24
27
  const emit = defineEmits(['refresh']);
25
28
 
26
29
  const dataFormRef = ref();
@@ -34,9 +37,19 @@ const isMasterFieldVisible = (prop: string) => {
34
37
  const config = getMasterFieldMeta(prop);
35
38
  return Boolean(config) && !config.alwaysHide && config.show !== false;
36
39
  };
40
+ const resolveLabel = (labelKey?: string, fallback = '') => {
41
+ if (!labelKey) return fallback;
42
+ const translated = t(labelKey);
43
+ return translated === labelKey ? fallback : translated;
44
+ };
45
+ const getMasterFieldLabel = (prop: string) => {
46
+ const config = getMasterFieldMeta(prop);
47
+ return resolveLabel(config?.labelKey, config?.label || prop);
48
+ };
37
49
  const getDictOptions = (dictType?: string) => (dictType ? dictRefs[dictType]?.value || [] : []);
38
- const inputPlaceholder = (label: string) => `请输入${label}`;
39
- const selectPlaceholder = (label: string) => `请选择${label}`;
50
+ const inputPlaceholder = (label: string) => t(`${pageI18nKey}.placeholders.input`, { label });
51
+ const selectPlaceholder = (label: string) => t(`${pageI18nKey}.placeholders.select`, { label });
52
+ const fieldRequiredMessage = (prop: string) => t(`${pageI18nKey}.messages.required`, { label: getMasterFieldLabel(prop) });
40
53
 
41
54
  const form = reactive({
42
55
  {{FORM_DEFAULTS}}
@@ -52,7 +65,7 @@ const get{{CLASS_NAME}}Data = async (id: string) => {
52
65
  const { data } = await getObj({ {{PK_ATTR}}: id });
53
66
  Object.assign(form, data[0] || {});
54
67
  } catch (error) {
55
- useMessage().error('获取数据失败');
68
+ useMessage().error(t(`${pageI18nKey}.messages.fetchError`));
56
69
  } finally {
57
70
  loading.value = false;
58
71
  }
@@ -88,11 +101,11 @@ const onSubmit = async () => {
88
101
 
89
102
  try {
90
103
  form.{{PK_ATTR}} ? await putObj(form) : await addObj(form);
91
- useMessage().success(form.{{PK_ATTR}} ? '修改成功' : '添加成功');
104
+ useMessage().success(form.{{PK_ATTR}} ? t(`${pageI18nKey}.messages.updateSuccess`) : t(`${pageI18nKey}.messages.createSuccess`));
92
105
  visible.value = false;
93
106
  emit('refresh');
94
107
  } catch (err: any) {
95
- useMessage().error(err.msg || '提交失败');
108
+ useMessage().error(err.msg || t(`${pageI18nKey}.messages.submitError`));
96
109
  } finally {
97
110
  loading.value = false;
98
111
  }
@@ -3,9 +3,9 @@
3
3
  <div class="layout-padding-auto layout-padding-view">
4
4
  <el-row>
5
5
  <div class="mb8" style="width: 100%">
6
- <el-button icon="folder-add" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="formDialogRef.openDialog()">新增</el-button>
7
- <el-button plain icon="upload-filled" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="excelUploadRef.show()">导入</el-button>
8
- <el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete(selectObjs)">删除</el-button>
6
+ <el-button icon="folder-add" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="formDialogRef.openDialog()">{{ t('common.addBtn') }}</el-button>
7
+ <el-button plain icon="upload-filled" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="excelUploadRef.show()">{{ t('common.importBtn') }}</el-button>
8
+ <el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete(selectObjs)">{{ t('common.delBtn') }}</el-button>
9
9
  <right-toolbar v-model:showSearch="showSearch" :export="'{{PERMISSION_PREFIX}}_export'" @exportExcel="exportExcel" @queryTable="getDataList" class="ml10 mr20" style="float: right;" />
10
10
  </div>
11
11
  </el-row>
@@ -20,13 +20,13 @@
20
20
  @sort-change="sortChangeHandle"
21
21
  >
22
22
  <el-table-column type="selection" width="40" align="center" />
23
- <el-table-column type="index" label="#" width="40" />
23
+ <el-table-column type="index" :label="t('common.serial')" width="60" />
24
24
  <el-table-column
25
25
  v-for="column in visibleTableColumns"
26
26
  :key="column.prop"
27
27
  :prop="column.prop"
28
- :label="column.label"
29
- :width="column.width"
28
+ :label="resolveLabel(column.labelKey, column.fallbackLabel)"
29
+ :min-width="column.width"
30
30
  show-overflow-tooltip
31
31
  >
32
32
  <template #default="scope">
@@ -34,10 +34,10 @@
34
34
  <span v-else>{{ scope.row[column.prop] }}</span>
35
35
  </template>
36
36
  </el-table-column>
37
- <el-table-column label="操作" width="180">
37
+ <el-table-column :label="t('common.action')" width="180">
38
38
  <template #default="scope">
39
- <el-button icon="edit-pen" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_edit'" @click="formDialogRef.openDialog(scope.row.{{PK_ATTR}})">编辑</el-button>
40
- <el-button icon="delete" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete([scope.row.{{PK_ATTR}}])">删除</el-button>
39
+ <el-button icon="edit-pen" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_edit'" @click="formDialogRef.openDialog(scope.row.{{PK_ATTR}})">{{ t('common.editBtn') }}</el-button>
40
+ <el-button icon="delete" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete([scope.row.{{PK_ATTR}}])">{{ t('common.delBtn') }}</el-button>
41
41
  </template>
42
42
  </el-table-column>
43
43
  </el-table>
@@ -46,7 +46,7 @@
46
46
  </div>
47
47
 
48
48
  <form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
49
- <upload-excel ref="excelUploadRef" title="导入" url="/{{API_PATH}}/import" temp-url="/admin/sys-file/local/file/{{FUNCTION_NAME}}.xlsx" @refreshDataList="getDataList" />
49
+ <upload-excel ref="excelUploadRef" :title="t('common.importBtn')" url="/{{API_PATH}}/import" temp-url="/admin/sys-file/local/file/{{FUNCTION_NAME}}.xlsx" @refreshDataList="getDataList" />
50
50
  </div>
51
51
  </template>
52
52
 
@@ -55,9 +55,11 @@ import { BasicTableProps, useTable } from '/@/hooks/table';
55
55
  import { fetchList, delObjs } from '/@/api/{{API_MODULE_PATH}}';
56
56
  import { useMessage, useMessageBox } from '/@/hooks/message';
57
57
  import { useDict } from '/@/hooks/dict';
58
+ import { useI18n } from 'vue-i18n';
58
59
  import { allDictTypes, dataMasterEntity } from './options';
59
60
 
60
61
  const dictRefs = useDict(...allDictTypes);
62
+ const { t } = useI18n();
61
63
 
62
64
  const FormDialog = defineAsyncComponent(() => import('./form.vue'));
63
65
 
@@ -72,12 +74,19 @@ const state: BasicTableProps = reactive<BasicTableProps>({
72
74
  pageList: fetchList,
73
75
  });
74
76
 
77
+ const resolveLabel = (labelKey?: string, fallback = '') => {
78
+ if (!labelKey) return fallback;
79
+ const translated = t(labelKey);
80
+ return translated === labelKey ? fallback : translated;
81
+ };
82
+
75
83
  const visibleTableColumns = computed(() =>
76
84
  Object.entries(dataMasterEntity)
77
85
  .filter(([, config]) => !config.alwaysHide && config.show !== false)
78
86
  .map(([prop, config]) => ({
79
87
  prop,
80
- label: config.label,
88
+ labelKey: config.labelKey,
89
+ fallbackLabel: config.label || prop,
81
90
  width: config.width,
82
91
  dictType: config.dictType || '',
83
92
  }))
@@ -98,7 +107,7 @@ const selectionChangeHandle = (objs: { {{PK_ATTR}}: string }[]) => {
98
107
 
99
108
  const handleDelete = async (ids: string[]) => {
100
109
  try {
101
- await useMessageBox().confirm('此操作将永久删除');
110
+ await useMessageBox().confirm(t('common.delConfirmText'));
102
111
  } catch {
103
112
  return;
104
113
  }
@@ -106,9 +115,9 @@ const handleDelete = async (ids: string[]) => {
106
115
  try {
107
116
  await delObjs(ids);
108
117
  getDataList();
109
- useMessage().success('删除成功');
118
+ useMessage().success(t('common.delSuccessText'));
110
119
  } catch (err: any) {
111
- useMessage().error(err.msg || '删除失败');
120
+ useMessage().error(err.msg || t('common.delBtn'));
112
121
  }
113
122
  };
114
123
  </script>
@@ -4,29 +4,21 @@ import type { CrudSchemaDefinition } from '/@/utils/crudSchema';
4
4
 
5
5
  /**
6
6
  * {{TABLE_NAME}} 页面字段声明。
7
- * 这里只维护字段清单,不手写派生配置。
7
+ * 这里只维护字段 key、双语 key、字典类型和显示元数据。
8
8
  */
9
9
  const definition: CrudSchemaDefinition = {
10
- // 主表字段
11
10
  master: [
12
11
  {{MASTER_OPTION_FIELDS}}
13
12
  ],
14
- // 子表字段,key 对应 form.vue 中的列表字段名
15
13
  children: {
16
14
  {{CHILD_OPTION_GROUPS}}
17
15
  },
18
16
  };
19
17
 
20
- // 统一派生页面所需配置,避免在 options 文件里重复手写
21
18
  const schema = createCrudSchema(definition);
22
19
 
23
- // 主表字段配置:列表页、主表表单共用
24
20
  export const dataMasterEntity = schema.master;
25
- // 子表字段配置:主子表 form 页使用
26
21
  export const childFieldGroups = schema.children;
27
- // 主表字典筛选配置
28
22
  export const filterTypes = schema.filterTypes;
29
- // 子表字典筛选配置
30
23
  export const childFilterTypes = schema.childFilterTypes;
31
- // 当前功能涉及的全部字典编码
32
24
  export const allDictTypes = schema.allDictTypes;
@@ -2,7 +2,7 @@
2
2
  <div class="layout-padding-auto layout-padding-view">
3
3
  <el-card shadow="never">
4
4
  <template #header>
5
- <span>{{ form.{{PK_ATTR}} ? (detail ? '查看' : '编辑') : '新增' }}</span>
5
+ <span>{{ form.{{PK_ATTR}} ? (detail ? t('common.viewBtn') : t('common.editBtn')) : t('common.addBtn') }}</span>
6
6
  </template>
7
7
  <el-form ref="dataFormRef" :model="form" :rules="dataRules" :disabled="detail" v-loading="loading">
8
8
  <el-row :gutter="24">
@@ -10,8 +10,8 @@
10
10
  </el-row>
11
11
  </el-form>
12
12
  <div class="dialog-footer" style="text-align: right; margin-top: 18px;">
13
- <el-button @click="handleBack">取消</el-button>
14
- <el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
13
+ <el-button @click="handleBack">{{ t('common.cancelButtonText') }}</el-button>
14
+ <el-button type="primary" @click="onSubmit" :disabled="loading">{{ t('common.confirmButtonText') }}</el-button>
15
15
  </div>
16
16
  </el-card>
17
17
  </div>
@@ -22,11 +22,14 @@ import mittBus from '/@/utils/mitt';
22
22
  import { useMessage } from '/@/hooks/message';
23
23
  import { getObj, addObj, putObj } from '/@/api/{{API_MODULE_PATH}}';
24
24
  import { useDict } from '/@/hooks/dict';
25
+ import { useI18n } from 'vue-i18n';
25
26
  import { allDictTypes, dataMasterEntity } from './options';
26
27
 
27
28
  const dictRefs = useDict(...allDictTypes);
29
+ const { t } = useI18n();
28
30
  const route = useRoute();
29
31
  const router = useRouter();
32
+ const pageI18nKey = '{{I18N_NAMESPACE}}';
30
33
 
31
34
  const dataFormRef = ref();
32
35
  const loading = ref(false);
@@ -39,9 +42,19 @@ const isMasterFieldVisible = (prop: string) => {
39
42
  const config = getMasterFieldMeta(prop);
40
43
  return Boolean(config) && !config.alwaysHide && config.show !== false;
41
44
  };
45
+ const resolveLabel = (labelKey?: string, fallback = '') => {
46
+ if (!labelKey) return fallback;
47
+ const translated = t(labelKey);
48
+ return translated === labelKey ? fallback : translated;
49
+ };
50
+ const getMasterFieldLabel = (prop: string) => {
51
+ const config = getMasterFieldMeta(prop);
52
+ return resolveLabel(config?.labelKey, config?.label || prop);
53
+ };
42
54
  const getDictOptions = (dictType?: string) => (dictType ? dictRefs[dictType]?.value || [] : []);
43
- const inputPlaceholder = (label: string) => `请输入${label}`;
44
- const selectPlaceholder = (label: string) => `请选择${label}`;
55
+ const inputPlaceholder = (label: string) => t(`${pageI18nKey}.placeholders.input`, { label });
56
+ const selectPlaceholder = (label: string) => t(`${pageI18nKey}.placeholders.select`, { label });
57
+ const fieldRequiredMessage = (prop: string) => t(`${pageI18nKey}.messages.required`, { label: getMasterFieldLabel(prop) });
45
58
 
46
59
  const form = reactive({
47
60
  {{FORM_DEFAULTS}}
@@ -57,7 +70,7 @@ const get{{CLASS_NAME}}Data = async (id: string) => {
57
70
  const { data } = await getObj({ {{PK_ATTR}}: id });
58
71
  Object.assign(form, data[0] || {});
59
72
  } catch (error) {
60
- useMessage().error('获取数据失败');
73
+ useMessage().error(t(`${pageI18nKey}.messages.fetchError`));
61
74
  } finally {
62
75
  loading.value = false;
63
76
  }
@@ -104,10 +117,10 @@ const onSubmit = async () => {
104
117
 
105
118
  try {
106
119
  form.{{PK_ATTR}} ? await putObj(form) : await addObj(form);
107
- useMessage().success(form.{{PK_ATTR}} ? '修改成功' : '添加成功');
120
+ useMessage().success(form.{{PK_ATTR}} ? t(`${pageI18nKey}.messages.updateSuccess`) : t(`${pageI18nKey}.messages.createSuccess`));
108
121
  closeCurrentPage();
109
122
  } catch (err: any) {
110
- useMessage().error(err.msg || '提交失败');
123
+ useMessage().error(err.msg || t(`${pageI18nKey}.messages.submitError`));
111
124
  } finally {
112
125
  loading.value = false;
113
126
  }
@@ -3,9 +3,9 @@
3
3
  <div class="layout-padding-auto layout-padding-view">
4
4
  <el-row>
5
5
  <div class="mb8" style="width: 100%">
6
- <el-button icon="folder-add" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="handleCreate">新增</el-button>
7
- <el-button plain icon="upload-filled" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="excelUploadRef.show()">导入</el-button>
8
- <el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete(selectObjs)">删除</el-button>
6
+ <el-button icon="folder-add" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="handleCreate">{{ t('common.addBtn') }}</el-button>
7
+ <el-button plain icon="upload-filled" type="primary" class="ml10" v-auth="'{{PERMISSION_PREFIX}}_add'" @click="excelUploadRef.show()">{{ t('common.importBtn') }}</el-button>
8
+ <el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete(selectObjs)">{{ t('common.delBtn') }}</el-button>
9
9
  <right-toolbar v-model:showSearch="showSearch" :export="'{{PERMISSION_PREFIX}}_export'" @exportExcel="exportExcel" @queryTable="getDataList" class="ml10 mr20" style="float: right;" />
10
10
  </div>
11
11
  </el-row>
@@ -20,13 +20,13 @@
20
20
  @sort-change="sortChangeHandle"
21
21
  >
22
22
  <el-table-column type="selection" width="40" align="center" />
23
- <el-table-column type="index" label="#" width="40" />
23
+ <el-table-column type="index" :label="t('common.serial')" width="60" />
24
24
  <el-table-column
25
25
  v-for="column in visibleTableColumns"
26
26
  :key="column.prop"
27
27
  :prop="column.prop"
28
- :label="column.label"
29
- :width="column.width"
28
+ :label="resolveLabel(column.labelKey, column.fallbackLabel)"
29
+ :min-width="column.width"
30
30
  show-overflow-tooltip
31
31
  >
32
32
  <template #default="scope">
@@ -34,11 +34,11 @@
34
34
  <span v-else>{{ scope.row[column.prop] }}</span>
35
35
  </template>
36
36
  </el-table-column>
37
- <el-table-column label="操作" width="220">
37
+ <el-table-column :label="t('common.action')" width="220">
38
38
  <template #default="scope">
39
- <el-button text type="primary" icon="view" v-auth="'{{PERMISSION_PREFIX}}_view'" @click="handleDetail(scope.row.{{PK_ATTR}})">查看</el-button>
40
- <el-button icon="edit-pen" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_edit'" @click="handleEdit(scope.row.{{PK_ATTR}})">编辑</el-button>
41
- <el-button icon="delete" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete([scope.row.{{PK_ATTR}}])">删除</el-button>
39
+ <el-button text type="primary" icon="view" v-auth="'{{PERMISSION_PREFIX}}_view'" @click="handleDetail(scope.row.{{PK_ATTR}})">{{ t('common.viewBtn') }}</el-button>
40
+ <el-button icon="edit-pen" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_edit'" @click="handleEdit(scope.row.{{PK_ATTR}})">{{ t('common.editBtn') }}</el-button>
41
+ <el-button icon="delete" text type="primary" v-auth="'{{PERMISSION_PREFIX}}_del'" @click="handleDelete([scope.row.{{PK_ATTR}}])">{{ t('common.delBtn') }}</el-button>
42
42
  </template>
43
43
  </el-table-column>
44
44
  </el-table>
@@ -46,7 +46,7 @@
46
46
  <pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
47
47
  </div>
48
48
 
49
- <upload-excel ref="excelUploadRef" title="导入" url="/{{API_PATH}}/import" temp-url="/admin/sys-file/local/file/{{FUNCTION_NAME}}.xlsx" @refreshDataList="getDataList" />
49
+ <upload-excel ref="excelUploadRef" :title="t('common.importBtn')" url="/{{API_PATH}}/import" temp-url="/admin/sys-file/local/file/{{FUNCTION_NAME}}.xlsx" @refreshDataList="getDataList" />
50
50
  </div>
51
51
  </template>
52
52
 
@@ -55,9 +55,11 @@ import { BasicTableProps, useTable } from '/@/hooks/table';
55
55
  import { fetchList, delObjs } from '/@/api/{{API_MODULE_PATH}}';
56
56
  import { useMessage, useMessageBox } from '/@/hooks/message';
57
57
  import { useDict } from '/@/hooks/dict';
58
+ import { useI18n } from 'vue-i18n';
58
59
  import { allDictTypes, dataMasterEntity } from './options';
59
60
 
60
61
  const dictRefs = useDict(...allDictTypes);
62
+ const { t } = useI18n();
61
63
  const router = useRouter();
62
64
 
63
65
  const excelUploadRef = ref();
@@ -70,12 +72,19 @@ const state: BasicTableProps = reactive<BasicTableProps>({
70
72
  pageList: fetchList,
71
73
  });
72
74
 
75
+ const resolveLabel = (labelKey?: string, fallback = '') => {
76
+ if (!labelKey) return fallback;
77
+ const translated = t(labelKey);
78
+ return translated === labelKey ? fallback : translated;
79
+ };
80
+
73
81
  const visibleTableColumns = computed(() =>
74
82
  Object.entries(dataMasterEntity)
75
83
  .filter(([, config]) => !config.alwaysHide && config.show !== false)
76
84
  .map(([prop, config]) => ({
77
85
  prop,
78
- label: config.label,
86
+ labelKey: config.labelKey,
87
+ fallbackLabel: config.label || prop,
79
88
  width: config.width,
80
89
  dictType: config.dictType || '',
81
90
  }))
@@ -88,15 +97,15 @@ const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, do
88
97
  const getFormPath = () => '/{{VIEW_MODULE_PATH}}/form';
89
98
 
90
99
  const handleCreate = () => {
91
- router.push({ path: getFormPath(), query: { tagsViewName: '新增' } });
100
+ router.push({ path: getFormPath(), query: { tagsViewName: t('common.addBtn') } });
92
101
  };
93
102
 
94
103
  const handleDetail = (id: string) => {
95
- router.push({ path: getFormPath(), query: { id, detail: '1', tagsViewName: '查看' } });
104
+ router.push({ path: getFormPath(), query: { id, detail: '1', tagsViewName: t('common.viewBtn') } });
96
105
  };
97
106
 
98
107
  const handleEdit = (id: string) => {
99
- router.push({ path: getFormPath(), query: { id, tagsViewName: '编辑' } });
108
+ router.push({ path: getFormPath(), query: { id, tagsViewName: t('common.editBtn') } });
100
109
  };
101
110
 
102
111
  const exportExcel = () => {
@@ -110,7 +119,7 @@ const selectionChangeHandle = (objs: { {{PK_ATTR}}: string }[]) => {
110
119
 
111
120
  const handleDelete = async (ids: string[]) => {
112
121
  try {
113
- await useMessageBox().confirm('此操作将永久删除');
122
+ await useMessageBox().confirm(t('common.delConfirmText'));
114
123
  } catch {
115
124
  return;
116
125
  }
@@ -118,9 +127,9 @@ const handleDelete = async (ids: string[]) => {
118
127
  try {
119
128
  await delObjs(ids);
120
129
  getDataList();
121
- useMessage().success('删除成功');
130
+ useMessage().success(t('common.delSuccessText'));
122
131
  } catch (err: any) {
123
- useMessage().error(err.msg || '删除失败');
132
+ useMessage().error(err.msg || t('common.delBtn'));
124
133
  }
125
134
  };
126
135
  </script>
@@ -4,29 +4,21 @@ import type { CrudSchemaDefinition } from '/@/utils/crudSchema';
4
4
 
5
5
  /**
6
6
  * {{TABLE_NAME}} 页面字段声明。
7
- * 这里只维护字段清单,不手写派生配置。
7
+ * 这里只维护字段 key、双语 key、字典类型和显示元数据。
8
8
  */
9
9
  const definition: CrudSchemaDefinition = {
10
- // 主表字段
11
10
  master: [
12
11
  {{MASTER_OPTION_FIELDS}}
13
12
  ],
14
- // 子表字段,key 对应 form.vue 中的列表字段名
15
13
  children: {
16
14
  {{CHILD_OPTION_GROUPS}}
17
15
  },
18
16
  };
19
17
 
20
- // 统一派生页面所需配置,避免在 options 文件里重复手写
21
18
  const schema = createCrudSchema(definition);
22
19
 
23
- // 主表字段配置:列表页、主表表单共用
24
20
  export const dataMasterEntity = schema.master;
25
- // 子表字段配置:主子表 form 页使用
26
21
  export const childFieldGroups = schema.children;
27
- // 主表字典筛选配置
28
22
  export const filterTypes = schema.filterTypes;
29
- // 子表字典筛选配置
30
23
  export const childFilterTypes = schema.childFilterTypes;
31
- // 当前功能涉及的全部字典编码
32
24
  export const allDictTypes = schema.allDictTypes;
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.5';
8
+ const SERVER_VERSION = '0.1.6';
9
9
  const PROTOCOL_VERSION = '2024-11-05';
10
10
  const TOOL_NAME = 'worsoft_codegen_local_generate_frontend';
11
11
  const TEMPLATE_LIBRARY_ROOT = path.resolve(__dirname, '..', 'template');
@@ -21,14 +21,16 @@ const DEFAULT_CRUD_SCHEMA_TEMPLATE = `export interface FieldMeta {
21
21
  show: boolean;
22
22
  alwaysHide: boolean;
23
23
  smart: boolean;
24
- label: string;
24
+ labelKey: string;
25
+ label?: string;
25
26
  width: string;
26
27
  dictType?: string;
27
28
  }
28
29
 
29
30
  export interface FieldConfig {
30
31
  key: string;
31
- label: string;
32
+ labelKey?: string;
33
+ label?: string;
32
34
  width?: string;
33
35
  show?: boolean;
34
36
  alwaysHide?: boolean;
@@ -51,16 +53,16 @@ export interface CrudSchema {
51
53
 
52
54
  const DEFAULT_WIDTH = '120';
53
55
 
54
- export const field = (label: string, width = DEFAULT_WIDTH): FieldMeta => ({
56
+ export const field = (labelKey: string, width = DEFAULT_WIDTH): FieldMeta => ({
55
57
  show: true,
56
58
  alwaysHide: false,
57
59
  smart: false,
58
- label,
60
+ labelKey,
59
61
  width,
60
62
  });
61
63
 
62
- export const dictField = (label: string, dictType: string, width = DEFAULT_WIDTH): FieldMeta => ({
63
- ...field(label, width),
64
+ export const dictField = (labelKey: string, dictType: string, width = DEFAULT_WIDTH): FieldMeta => ({
65
+ ...field(labelKey, width),
64
66
  dictType,
65
67
  });
66
68
 
@@ -82,7 +84,8 @@ const normalizeField = (item: FieldConfig): FieldMeta => ({
82
84
  show: item.show ?? true,
83
85
  alwaysHide: item.alwaysHide ?? false,
84
86
  smart: item.smart ?? false,
85
- label: item.label,
87
+ labelKey: item.labelKey ?? item.label ?? item.key,
88
+ ...(item.label ? { label: item.label } : {}),
86
89
  width: item.width ?? DEFAULT_WIDTH,
87
90
  ...(item.dictType ? { dictType: item.dictType } : {}),
88
91
  });
@@ -226,6 +229,76 @@ function renderDictRegistryContent(entries) {
226
229
  ].join('\n');
227
230
  }
228
231
 
232
+ function isPlainObject(value) {
233
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
234
+ }
235
+
236
+ function parseExportDefaultObject(fileContent) {
237
+ const source = String(fileContent || '').replace(/^\uFEFF/, '').trim();
238
+ if (!source) return null;
239
+ try {
240
+ const executable = source.replace(/^export\s+default/, 'return');
241
+ return new Function(executable)();
242
+ } catch (error) {
243
+ return null;
244
+ }
245
+ }
246
+
247
+ function deepMergeMissing(target, source) {
248
+ if (!isPlainObject(source)) {
249
+ return target === undefined ? source : target;
250
+ }
251
+
252
+ const result = isPlainObject(target) ? { ...target } : {};
253
+ for (const [key, value] of Object.entries(source)) {
254
+ if (result[key] === undefined) {
255
+ result[key] = value;
256
+ continue;
257
+ }
258
+ if (isPlainObject(result[key]) && isPlainObject(value)) {
259
+ result[key] = deepMergeMissing(result[key], value);
260
+ }
261
+ }
262
+ return result;
263
+ }
264
+
265
+ function escapeTsString(value) {
266
+ return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
267
+ }
268
+
269
+ function renderTsLiteral(value, indentLevel = 0) {
270
+ const indent = ' '.repeat(indentLevel);
271
+ const childIndent = ' '.repeat(indentLevel + 1);
272
+
273
+ if (typeof value === 'string') {
274
+ return `'${escapeTsString(value)}'`;
275
+ }
276
+ if (typeof value === 'number' || typeof value === 'boolean') {
277
+ return String(value);
278
+ }
279
+ if (Array.isArray(value)) {
280
+ if (!value.length) return '[]';
281
+ return ['[', ...value.map((item) => `${childIndent}${renderTsLiteral(item, indentLevel + 1)},`), `${indent}]`].join('\n');
282
+ }
283
+ if (isPlainObject(value)) {
284
+ const entries = Object.entries(value);
285
+ if (!entries.length) return '{}';
286
+ return [
287
+ '{',
288
+ ...entries.map(([key, item]) => {
289
+ const renderedKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${escapeTsString(key)}'`;
290
+ return `${childIndent}${renderedKey}: ${renderTsLiteral(item, indentLevel + 1)},`;
291
+ }),
292
+ `${indent}}`,
293
+ ].join('\n');
294
+ }
295
+ return 'null';
296
+ }
297
+
298
+ function renderExportDefaultContent(objectValue) {
299
+ return `export default ${renderTsLiteral(objectValue, 0)};\n`;
300
+ }
301
+
229
302
  function getPreferredDictRegistryKey(dictType) {
230
303
  return DEFAULT_DICT_REGISTRY_KEYS[dictType] || toConstantCase(dictType) || 'DICT_TYPE';
231
304
  }
@@ -243,6 +316,102 @@ function buildUniqueDictRegistryKey(dictType, usedKeys, existingByKey) {
243
316
  return candidate;
244
317
  }
245
318
 
319
+ function buildI18nNamespaceSegments(model) {
320
+ return [...model.moduleName.split('/').filter(Boolean), model.functionName];
321
+ }
322
+
323
+ function buildI18nNamespace(model) {
324
+ return buildI18nNamespaceSegments(model).join('.');
325
+ }
326
+
327
+ function buildFieldLabelKey(model, field) {
328
+ return `${buildI18nNamespace(model)}.fields.${field.attrName}`;
329
+ }
330
+
331
+ function buildChildFieldLabelKey(model, childModel, field) {
332
+ return `${buildI18nNamespace(model)}.children.${childModel.listName}.fields.${field.attrName}`;
333
+ }
334
+
335
+ function buildChildSectionTitleKey(model, childModel) {
336
+ return `${buildI18nNamespace(model)}.children.${childModel.listName}.title`;
337
+ }
338
+
339
+ function buildLocaleLeaf(model) {
340
+ const leaf = {
341
+ title: model.tableComment,
342
+ fields: Object.fromEntries(model.visibleFields.map((field) => [field.attrName, field.comment])),
343
+ placeholders: {
344
+ input: '请输入{label}',
345
+ select: '请选择{label}',
346
+ },
347
+ actions: {
348
+ save: '保存',
349
+ submit: '提交',
350
+ flow: '流转',
351
+ back: '返回',
352
+ },
353
+ messages: {
354
+ required: '{label}不能为空',
355
+ fetchError: '获取数据失败',
356
+ submitError: '提交失败',
357
+ createSuccess: '添加成功',
358
+ updateSuccess: '修改成功',
359
+ quickSubmitSuccess: '提交操作成功',
360
+ quickFlowSuccess: '流转操作成功',
361
+ quickActionConfirm: '确定要{action}该记录吗?',
362
+ },
363
+ };
364
+
365
+ if (model.children.length) {
366
+ leaf.children = Object.fromEntries(
367
+ model.children.map((childModel) => [
368
+ childModel.listName,
369
+ {
370
+ title: childModel.tableComment,
371
+ fields: Object.fromEntries(childModel.visibleFields.map((field) => [field.attrName, field.comment])),
372
+ },
373
+ ])
374
+ );
375
+ }
376
+
377
+ return leaf;
378
+ }
379
+
380
+ function buildZhCnLocaleObject(model) {
381
+ const root = {};
382
+ const segments = buildI18nNamespaceSegments(model);
383
+ let cursor = root;
384
+
385
+ for (let index = 0; index < segments.length - 1; index += 1) {
386
+ const segment = segments[index];
387
+ cursor[segment] = cursor[segment] || {};
388
+ cursor = cursor[segment];
389
+ }
390
+
391
+ cursor[segments[segments.length - 1]] = buildLocaleLeaf(model);
392
+ return root;
393
+ }
394
+
395
+ function prepareZhCnLocaleFile(model) {
396
+ const localePath = path.join(model.frontendPath, 'src', 'i18n', 'biz', ...model.moduleName.split('/'), `${model.functionName}.zh-cn.ts`);
397
+ const exists = fs.existsSync(localePath);
398
+ const currentContent = exists ? readUtf8File(localePath) : '';
399
+ const currentObject = exists ? parseExportDefaultObject(currentContent) : null;
400
+ const generatedObject = buildZhCnLocaleObject(model);
401
+ const isCompatible = !exists || isPlainObject(currentObject);
402
+ const mergedObject = isCompatible ? deepMergeMissing(currentObject || {}, generatedObject) : null;
403
+
404
+ return {
405
+ path: localePath,
406
+ frontendPath: model.frontendPath,
407
+ exists,
408
+ isCompatible,
409
+ namespace: buildI18nNamespace(model),
410
+ content: mergedObject ? renderExportDefaultContent(mergedObject) : '',
411
+ needsWrite: !exists || (isCompatible && renderExportDefaultContent(currentObject || {}) !== renderExportDefaultContent(mergedObject)),
412
+ };
413
+ }
414
+
246
415
  function prepareDictRegistry(frontendPath, dictTypes) {
247
416
  const registryPath = path.join(frontendPath, 'src', 'enums', 'dict-registry.ts');
248
417
  const exists = fs.existsSync(registryPath);
@@ -275,17 +444,19 @@ function ensureCrudSchemaSupportFile(frontendPath) {
275
444
  const schemaPath = path.join(frontendPath, 'src', 'utils', 'crudSchema.ts');
276
445
  const exists = fs.existsSync(schemaPath);
277
446
  const currentContent = exists ? readUtf8File(schemaPath) : '';
278
- const isCompatible =
447
+ const hasCoreShape =
279
448
  currentContent.includes('export interface CrudSchemaDefinition') &&
280
449
  currentContent.includes('export function createCrudSchema') &&
281
450
  currentContent.includes('export interface FieldConfig');
451
+ const supportsLabelKey = currentContent.includes('labelKey');
452
+ const shouldUpgradeLegacy = exists && hasCoreShape && !supportsLabelKey;
282
453
 
283
454
  return {
284
455
  path: schemaPath,
285
456
  content: DEFAULT_CRUD_SCHEMA_TEMPLATE,
286
457
  exists,
287
- isCompatible,
288
- needsWrite: !exists,
458
+ isCompatible: hasCoreShape && supportsLabelKey,
459
+ needsWrite: !exists || shouldUpgradeLegacy,
289
460
  };
290
461
  }
291
462
 
@@ -320,14 +491,24 @@ function maybeWriteSharedSupport(sharedSupport, writeToDisk) {
320
491
  }
321
492
  }
322
493
 
323
- function buildSharedSupportNote(sharedSupport) {
494
+ function buildSupportNote(sharedSupport, localeZhSupport) {
495
+ const notes = [];
496
+
324
497
  if (sharedSupport.crudSchema.exists && !sharedSupport.crudSchema.isCompatible) {
325
- return (
498
+ notes.push(
326
499
  'Detected an existing src/utils/crudSchema.ts that does not match the expected helper signature. ' +
327
- 'MCP preserved the existing file and did not overwrite it. Generated pages now depend on that file being manually aligned.'
500
+ 'MCP preserved the existing file and did not overwrite it. Generated pages now depend on that file being manually aligned.'
328
501
  );
329
502
  }
330
- return 'Runtime template rendering completed.';
503
+
504
+ if (localeZhSupport.exists && !localeZhSupport.isCompatible) {
505
+ notes.push(
506
+ `Detected an existing ${path.relative(localeZhSupport.frontendPath, localeZhSupport.path).replace(/\\/g, '/')} that MCP could not parse. ` +
507
+ 'The file was preserved and not updated, so new Chinese i18n keys may need to be merged manually.'
508
+ );
509
+ }
510
+
511
+ return notes.length ? notes.join(' ') : 'Runtime template rendering completed.';
331
512
  }
332
513
 
333
514
  function getDictRegistryReference(dictType, keyByValue) {
@@ -1168,9 +1349,8 @@ function getDefaultOptionFieldWidthV2(field) {
1168
1349
  return '120';
1169
1350
  }
1170
1351
 
1171
- function renderOptionFieldV2(field, dictRegistryRefs, indent = ' ') {
1172
- const label = field.comment.replace(/'/g, "\\'");
1173
- const parts = [`key: '${field.attrName}'`, `label: '${label}'`];
1352
+ function renderOptionFieldV2(field, labelKey, dictRegistryRefs, indent = ' ') {
1353
+ const parts = [`key: '${field.attrName}'`, `labelKey: '${labelKey}'`];
1174
1354
  const width = getDefaultOptionFieldWidthV2(field);
1175
1355
 
1176
1356
  if (width !== '120') {
@@ -1184,10 +1364,12 @@ function renderOptionFieldV2(field, dictRegistryRefs, indent = ' ') {
1184
1364
  return `${indent}{ ${parts.join(', ')} },`;
1185
1365
  }
1186
1366
 
1187
- function renderChildOptionGroupV2(childModel, dictRegistryRefs) {
1367
+ function renderChildOptionGroupV2(model, childModel, dictRegistryRefs) {
1188
1368
  return [
1189
1369
  ` ${childModel.listName}: [`,
1190
- childModel.visibleFields.map((field) => renderOptionFieldV2(field, dictRegistryRefs, ' ')).join('\n'),
1370
+ childModel.visibleFields
1371
+ .map((field) => renderOptionFieldV2(field, buildChildFieldLabelKey(model, childModel, field), dictRegistryRefs, ' '))
1372
+ .join('\n'),
1191
1373
  ' ],',
1192
1374
  ].join('\n');
1193
1375
  }
@@ -1203,9 +1385,8 @@ function renderTextareaMaxlengthAttrsV2(field) {
1203
1385
  }
1204
1386
 
1205
1387
  function renderFormFieldV2(field) {
1206
- const label = field.comment.replace(/'/g, "\\'");
1207
1388
  const prop = field.attrName;
1208
- const labelExpr = `getMasterFieldMeta('${prop}')?.label || '${label}'`;
1389
+ const labelExpr = `getMasterFieldLabel('${prop}')`;
1209
1390
  const dictExpr = `getMasterFieldMeta('${prop}')?.dictType`;
1210
1391
  const visibilityExpr = `isMasterFieldVisible('${prop}')`;
1211
1392
 
@@ -1281,9 +1462,8 @@ function renderTableColumnV2(field, dictRegistryRefs) {
1281
1462
  }
1282
1463
 
1283
1464
  function renderChildTableColumnV2(field, childListName) {
1284
- const label = field.comment.replace(/'/g, "\\'");
1285
1465
  const rules = field.notNull ? ` :rules="[{ required: true, trigger: 'blur' }]"` : '';
1286
- const labelExpr = `getChildFieldMeta('${childListName}', '${field.attrName}')?.label || '${label}'`;
1466
+ const labelExpr = `getChildFieldLabel('${childListName}', '${field.attrName}')`;
1287
1467
  const dictExpr = `getChildFieldMeta('${childListName}', '${field.attrName}')?.dictType`;
1288
1468
 
1289
1469
  let control = ` <el-input v-model="row.${field.attrName}"${renderTextMaxlengthAttrV2(field)} />`;
@@ -1317,7 +1497,6 @@ function renderChildTableColumnV2(field, childListName) {
1317
1497
  }
1318
1498
 
1319
1499
  function renderChildSectionV2(childModel, childCount) {
1320
- const title = childModel.tableComment.replace(/'/g, "\\'");
1321
1500
  const deleteExpression =
1322
1501
  childCount > 1
1323
1502
  ? `deleteChild(obj, '${childModel.pk.attrName}', '${childModel.tableName}')`
@@ -1325,7 +1504,7 @@ function renderChildSectionV2(childModel, childCount) {
1325
1504
 
1326
1505
  return [
1327
1506
  ' <el-col :span="24" class="mb20">',
1328
- ` <div class="mb10" style="font-weight: 600;">${title}</div>`,
1507
+ ` <div class="mb10" style="font-weight: 600;">{{ childSectionTitle('${childModel.listName}') }}</div>`,
1329
1508
  ` <sc-form-table v-model="form.${childModel.listName}" :addTemplate="childTemp${childModel.className}" @delete="(obj) => ${deleteExpression}" placeholder="鏆傛棤鏁版嵁">`,
1330
1509
  childModel.visibleFields.map((field) => renderChildTableColumnV2(field, childModel.listName)).join('\n'),
1331
1510
  ' </sc-form-table>',
@@ -1344,12 +1523,20 @@ function renderFormRules(visibleFields) {
1344
1523
  return lines.join('\n');
1345
1524
  }
1346
1525
 
1526
+ function renderFormRulesV2(visibleFields) {
1527
+ const lines = visibleFields
1528
+ .filter((field) => field.notNull)
1529
+ .map((field) => ` ${field.attrName}: [{ required: true, message: fieldRequiredMessage('${field.attrName}'), trigger: 'blur' }],`);
1530
+ return lines.join('\n');
1531
+ }
1532
+
1347
1533
  function buildReplacements(model, sharedSupport) {
1348
1534
  const menuBaseId = Date.now();
1349
1535
  const apiModulePath = `${model.moduleName}/${model.functionName}`;
1350
1536
  const routePath = `${model.moduleName}/${model.functionName}`;
1351
1537
  const permissionPrefix = `${model.moduleName}/${model.functionName}`.replace(/\//g, '_');
1352
1538
  const dictRegistryRefs = sharedSupport.dictRegistry.keyByValue;
1539
+ const i18nNamespace = buildI18nNamespace(model);
1353
1540
 
1354
1541
  return {
1355
1542
  TABLE_NAME: model.tableName,
@@ -1361,6 +1548,7 @@ function buildReplacements(model, sharedSupport) {
1361
1548
  API_PATH: apiModulePath,
1362
1549
  VIEW_MODULE_PATH: routePath,
1363
1550
  MENU_ROUTE_PATH: routePath,
1551
+ I18N_NAMESPACE: i18nNamespace,
1364
1552
  PERMISSION_PREFIX: permissionPrefix,
1365
1553
  MENU_BASE_ID: menuBaseId,
1366
1554
  MENU_BASE_ID_PLUS_1: menuBaseId + 1,
@@ -1373,9 +1561,9 @@ function buildReplacements(model, sharedSupport) {
1373
1561
  TABLE_COLUMNS: model.gridFields.map((field) => renderTableColumnV2(field, dictRegistryRefs)).join('\n'),
1374
1562
  FORM_DEFAULTS: renderFormDefaults(model),
1375
1563
  DICT_REGISTRY_IMPORT_BLOCK: model.dictTypes.length ? "import { DictRegistry } from '/@/enums/dict-registry';" : '',
1376
- MASTER_OPTION_FIELDS: model.visibleFields.map((field) => renderOptionFieldV2(field, dictRegistryRefs)).join('\n'),
1377
- CHILD_OPTION_GROUPS: model.children.map((childModel) => renderChildOptionGroupV2(childModel, dictRegistryRefs)).join('\n'),
1378
- FORM_RULES: renderFormRules(model.visibleFields),
1564
+ MASTER_OPTION_FIELDS: model.visibleFields.map((field) => renderOptionFieldV2(field, buildFieldLabelKey(model, field), dictRegistryRefs)).join('\n'),
1565
+ CHILD_OPTION_GROUPS: model.children.map((childModel) => renderChildOptionGroupV2(model, childModel, dictRegistryRefs)).join('\n'),
1566
+ FORM_RULES: renderFormRulesV2(model.visibleFields),
1379
1567
  CHILD_FORM_LIST_DEFAULTS: renderChildFormListDefaults(model.children),
1380
1568
  CHILD_TEMP_DECLARATIONS: renderChildTempDeclarations(model.children),
1381
1569
  CHILD_RESET_LISTS: renderChildResetListLines(model.children),
@@ -1383,7 +1571,7 @@ function buildReplacements(model, sharedSupport) {
1383
1571
  };
1384
1572
  }
1385
1573
 
1386
- function renderFiles(model, stylePreset, sharedSupport) {
1574
+ function renderFiles(model, stylePreset, sharedSupport, localeZhSupport) {
1387
1575
  if (!hasRuntimeSupport(stylePreset)) throw new Error('Runtime templates are not implemented for style: ' + model.style);
1388
1576
 
1389
1577
  const runtime = stylePreset.runtime;
@@ -1404,6 +1592,13 @@ function buildReplacements(model, sharedSupport) {
1404
1592
  { type: 'list', path: path.join(viewRoot, 'index.vue'), content: renderTemplate(listTemplate, replacements) },
1405
1593
  { type: 'options', path: path.join(viewRoot, 'options.ts'), content: renderTemplate(optionsTemplate, replacements) },
1406
1594
  { type: 'api', path: path.join(apiRoot, `${model.functionName}.ts`), content: renderTemplate(apiTemplate, replacements) },
1595
+ {
1596
+ type: 'i18nZh',
1597
+ path: localeZhSupport.path,
1598
+ content: localeZhSupport.content,
1599
+ canWrite: localeZhSupport.isCompatible,
1600
+ needsWrite: localeZhSupport.needsWrite,
1601
+ },
1407
1602
  ];
1408
1603
 
1409
1604
  if (menuSqlTemplate) {
@@ -1440,6 +1635,14 @@ function ensureArguments(input) {
1440
1635
  function maybeWriteFiles(files, writeToDisk, overwrite) {
1441
1636
  if (!writeToDisk) return;
1442
1637
  for (const file of files) {
1638
+ if (file.canWrite === false) {
1639
+ file.status = 'skipped';
1640
+ continue;
1641
+ }
1642
+ if (file.needsWrite === false) {
1643
+ file.status = 'unchanged';
1644
+ continue;
1645
+ }
1443
1646
  if (!overwrite && fs.existsSync(file.path)) {
1444
1647
  file.status = 'skipped';
1445
1648
  continue;
@@ -1513,16 +1716,17 @@ async function handleToolCall(argumentsObject) {
1513
1716
  const model = buildModel(safeArgs);
1514
1717
  const sharedTemplates = resolveSharedTemplates(stylePreset);
1515
1718
  const sharedSupport = prepareSharedSupport(model.frontendPath, model.dictTypes);
1719
+ const localeZhSupport = prepareZhCnLocaleFile(model);
1516
1720
 
1517
1721
  if (!hasRuntimeSupport(stylePreset)) {
1518
1722
  const manifest = buildManifest(model, safeArgs, stylePreset, sharedTemplates, [], 'Style mapping is declared, but runtime template rendering is not implemented yet for this style.');
1519
1723
  return toolTextResult(JSON.stringify(manifest, null, 2));
1520
1724
  }
1521
1725
 
1522
- const files = renderFiles(model, stylePreset, sharedSupport);
1726
+ const files = renderFiles(model, stylePreset, sharedSupport, localeZhSupport);
1523
1727
  maybeWriteFiles(files, safeArgs.writeToDisk, safeArgs.overwrite);
1524
1728
  maybeWriteSharedSupport(sharedSupport, safeArgs.writeToDisk);
1525
- const manifest = buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, buildSharedSupportNote(sharedSupport));
1729
+ const manifest = buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, buildSupportNote(sharedSupport, localeZhSupport));
1526
1730
  return toolTextResult(JSON.stringify(manifest, null, 2));
1527
1731
  }
1528
1732
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worsoft-frontend-codegen-local-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Worsoft frontend local-template code generation MCP server.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "worsoft <sw@worsoft.vip>",