refactor(page-table, page-dialog-form): 实现国际化自动响应,移除提前翻译逻辑

1. 为page-table和page-dialog-form添加内部$t翻译逻辑,支持语言切换自动更新
2. 移除data()中提前翻译的k()辅助函数,改为直接传入i18n key
3. 更新文档说明最新的i18n使用规范,新增迁移指南文档
4. 修复工厂区域页面的国际化调用方式,统一使用this.key()传参
This commit is contained in:
sheng
2026-05-28 11:30:08 +08:00
parent 48cfebd008
commit 4539bec3a4
6 changed files with 351 additions and 219 deletions

View File

@@ -200,16 +200,13 @@ import { i18nMixin } from '@/composables/useI18n'
export default {
mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
data () {
const t = this.$t.bind(this)
const k = (s) => t(this.key(s)) // 当前页面翻译
const ck = (s) => t(this.ckey(s)) // 公共翻译
return {
// 传入完整 i18n key不在此处翻译由子组件内部 $t() 处理
formCols: [
[{ label: k('code'), placeholder: k('enter_code') }]
[{ label: this.key('code'), placeholder: this.key('enter_code') }]
],
rules: {
code: [{ required: true, message: k('enter_code'), trigger: 'blur' }]
code: [{ required: true, message: this.key('enter_code'), trigger: 'blur' }]
}
}
},
@@ -236,12 +233,13 @@ export default {
</template>
```
### 4.3 data() 中翻译的时机说明
### 4.3 data() 中传参的时机说明
`data()``created` 之前执行,此时 `this` 已经可用。因此:
- **column label / formCols / rules.message**:在 `data()` 中或 `created()``this.key()` 都可以
- **推荐在 `data()` 中用 `k()` 提前翻译**,避免子组件 `$t()` 的 webpack HMR 缓存问题
- **column label / formCols / rules.message**:在 `data()` 中或 `created()` `this.key()` 传入完整 i18n key
- **翻译由子组件负责**`page-table` `page-dialog-form` 内部使用 `$t()` 翻译传入的 key切换语言时自动响应更新
- **不要用 `k()` 提前翻译**`k()` 将 key 翻译成静态字符串,语言切换后不会更新
---
@@ -382,7 +380,7 @@ this.$t('please_enter', { name: '所区编码' })
- [ ] 5. 验证 JSON 合法性:`node -e "JSON.parse(require('fs').readFileSync('src/locales/zh-chs.json','utf8'))"`
- [ ] 6. 页面中使用 `mixins: [i18nMixin('key前缀')]`
- [ ] 7. 模板中用 `$t(key('xxx'))` 替代硬编码文字
- [ ] 8. data() 中用 `k('xxx')` 提前翻译 formCols / rules
- [ ] 8. data() 中用 `this.key('xxx')` 传入完整 i18n key不用 `k()` 提前翻译)
- [ ] 9. 公共文案(帮助、确定、取消等)用 `ckey('xxx')` 引用
- [ ] 10. `pnpm lint` 无报错

88
docs/migration-prompt.md Normal file
View File

@@ -0,0 +1,88 @@
## 任务:迁移旧 CRUD 页面到新版 page-table + page-dialog-form 方案
### 输入
用户会提供一个旧 CRUD 页面的文件路径列表和对照表信息。
---
### 迁移规则(必须严格遵守)
#### 1. 组件方案
- 使用 `<page-table>` + `<page-dialog-form>` 替代旧版 `<sct-base-table>` + `<sct-base-dialog>` + `<SctBaseForm>`
- 使用 `useTableColumns()` 生成列定义(不再手动分配 idx
- 使用 `useTableButtons()` 生成工具栏按钮和行内按钮(不再分开写 buttonList / tableButtonList
- 使用 `i18nMixin(prefix)` 注入 `key()``ckey()` 方法
- 参考文档:[表格组件使用说明.md](file:///d:/code/mes/mes-ui/docs/表格组件使用说明.md)
- 参考示例:`src/views/production-master-data/factory-model/factory-area/index.vue`
#### 2. i18n 国际化(重要)
- 使用 `mixins: [i18nMixin('完整i18n前缀')]`,不要在每个页面手动定义 `T` 常量和 `tkey()` 方法
- `data()` 中用 `this.key('xxx')` 传完整 i18n key**不要用 `k()` 提前翻译**(翻译由 page-table / page-dialog-form 内部处理,切换语言自动响应)
- 模板搜索区用 `$t(key('xxx'))`,公共 key 用 `$t(ckey('xxx'))`
- 语言包必须中英文同步添加(`zh-chs.json` + `en.json`
- 参考文档:[i18n-rules.md](file:///d:/code/mes/mes-ui/docs/i18n-rules.md)
#### 3. 文件夹命名(重要)
- 文件夹名称遵循 [后台Webman界面截图对照表](file:///d:/code/mes/mes-ui/后台Webman界面截图对照表.md) 的 snake_case/kebab-case 命名
- 目录示例:`src/views/production-master-data/factory-model/factory-area/`
- 路由模块:`src/router/modules/production-master-data.js`
- API 文件:`src/api/production-master-data/factory-area.js`
#### 4. 路由 path 和 API BASE URL重要 — 临时方案)
- **路由 path** 和 **API BASE URL** **暂时保留旧项目的命名**,因为后台数据库的路由表还未更新
- 示例:目录按对照表叫 `production-master-data`,但路由 path 保持旧值 `production_configuration`
```js
// src/router/modules/production-master-data.js
path: '/production_configuration', // 旧值暂留
```
```js
// src/api/production-master-data/factory-area.js
const BASE = 'production_configuration/factory_model/factory_area/' // 旧值暂留
```
- 后续统一修改路由后再批量替换
#### 5. 旧 key → 新 key 映射
如果用户提供的是旧页面代码,需要根据对照表做 key 映射。已知映射如下(持续补充):
| 旧 key 前缀 | 新 key 前缀 |
|------------|------------|
| `page.production_configuration.factory_model.factory_area` | `page.production_master_data.factory_model.factory_area` |
| `page.system_settings.user_management.role` | `page.system_administration.user_management.role` |
| `page.system_settings.user_management.user` | `page.system_administration.user_management.user` |
| `page.system_settings.menu_configuration.menu` | `page.system_administration.menu_management.menu_configuration` |
| `page.system_settings.system_assistant.operate_log` | `page.system_administration.system_utilities.operation_logs` |
| `page.system_settings.system_assistant.api_log` | `page.system_administration.system_utilities.api_logs` |
| `page.system_settings.system_monitoring.system.login` | `page.system_administration.system_monitoring.login` |
| `page.production_configuration.matetial_model.*` | `page.production_master_data.material_model.*` |
| `page.planning_production.production_batch_management.batch` | `page.planning_production.batch_management.batch_list` |
| `page.data_middleground.basic_traceability.*` | `page.data_platform.traceability.*` |
#### 6. 处理流程(每迁移一个页面执行以下步骤)
1. **确定对照表位置**:从 [后台Webman界面截图对照表](file:///d:/code/mes/mes-ui/后台Webman界面截图对照表.md) 找到对应行的英文名,转换为 snake_case
2. **创建目录结构**`src/views/{一级snake_case}/{二级snake_case}/{三级snake_case}/`
3. **创建 API 文件**`src/api/{一级snake_case}/{二级snake_case}/{三级snake_case}.js`BASE URL 暂用旧值
4. **添加路由模块**`src/router/modules/{一级snake_case}.js`path 暂用旧值
5. **编写页面代码**:使用 page-table + page-dialog-form 方案
6. **添加 i18n**:在 `zh-chs.json` 和 `en.json` 中添加对应 key
7. **验证 JSON 合法性**`node -e "JSON.parse(require('fs').readFileSync('src/locales/zh-chs.json','utf8'))"`
---
### 页面模板(通用 CRUD 页面骨架)
参考 `src/views/production-master-data/factory-model/factory-area/index.vue` 的结构,核心模式:
- **Script**`mixins: [i18nMixin('page.{一}.{二}.{三}')]` + `data()` 中用 `this.key('xxx')` 传 key
- **Template**`<page-table>` 传 columns/data/loading/toolbarButtons/rowButtons/pagination + `<page-dialog-form>` 传 formCols/formData/rules/title
- **Buttons**`useTableButtons({ toolbar: [...], row: [...] }, this.$permission)`
- **Columns**`useTableColumns([...])`
- **Methods**`fetchData / onSearch / onReset / onPageChange / openAdd / openEdit / onDialogSubmit / handleDelete`
- **i18n key 模板**(每个页面至少要有这些):`search / reset / add / edit / delete / operation / add_title / edit_title / code / name / remark / enter_code / enter_name / remark_length / operation_success / confirm / cancel / tip / confirm_delete`
---
### 输出要求
- 创建完整的页面文件、路由模块、API 文件
- 在 `zh-chs.json` 和 `en.json` 中添加对应 i18n key中文和英文同步
- 完成后提醒用户验证 JSON 合法性
- 如涉及对照表中已有映射的 key 前缀变更,自动纠正为新的 key 前缀

View File

@@ -1,6 +1,6 @@
# 表格组件使用说明
> 基于 `page-table` + `page-dialog-form` 的新一<EFBFBD><EFBFBD>?CRUD 表格方案<EFBFBD><EFBFBD>?
> 基于 `page-table` + `page-dialog-form` 的新一CRUD 表格方案
> 源码位置:`src/components/page-table/`、`src/components/page-dialog-form/`、`src/composables/`
> 完整可运行示例:`src/views/production-master-data/factory-model/factory-area/index.vue`
@@ -10,13 +10,13 @@
1. [快速开始(三步走)](#1-快速开始三步走)
2. [完整示例代码](#2-完整示例代码)
3. [组件 API 参考](#3-组件-api-参<EFBFBD><EFBFBD>?
4. [Composable API 参考](#4-composable-api-参<EFBFBD><EFBFBD>?
5. [i18n 国际化方案](#5-i18n-国际化方<EFBFBD><EFBFBD>?
3. [组件 API 参考](#3-组件-api-参)
4. [Composable API 参考](#4-composable-api-参)
5. [i18n 国际化方案](#5-i18n-国际化方)
6. [常用场景速查](#6-常用场景速查)
7. [路由配置](#7-路由配置)
8. [API 文件写法](#8-api-文件写法)
9. [旧代码迁移对照](#9-旧代码迁移对<EFBFBD><EFBFBD>?
9. [旧代码迁移对照](#9-旧代码迁移对)
10. [常见问题排查](#10-常见问题排查)
---
@@ -31,12 +31,12 @@ import { useTableButtons } from '@/composables/useTableButtons'
import { i18nMixin } from '@/composables/useI18n'
export default {
mixins: [i18nMixin('page.模块<EFBFBD><EFBFBD>?二级模块.三级模块')],
mixins: [i18nMixin('page.模块名.二级模块.三级模块')],
created () {
// --- 列定<EFBFBD><EFBFBD>?---
// 只需声明 prop <EFBFBD><EFBFBD>?labelidx 自动补齐
// prop: '_actions' 约定为操作列,自动渲<EFBFBD><EFBFBD>?rowButtons
// --- 列定---
// 只需声明 prop labelidx 自动补齐
// prop: '_actions' 约定为操作列,自动渲rowButtons
this.columns = useTableColumns([
{ prop: 'sort', label: this.key('sort'), width: 80 },
{ prop: 'code', label: this.key('code'), minWidth: 120 },
@@ -45,7 +45,7 @@ export default {
])
// --- 按钮定义 ---
// 不再分开<EFBFBD><EFBFBD>?buttonList / tableButtonList
// 不再分开buttonList / tableButtonList
const btns = useTableButtons({
toolbar: [
{ key: 'add', label: this.key('add'), icon: 'el-icon-plus',
@@ -57,7 +57,7 @@ export default {
{ key: 'delete', label: this.key('delete'), icon: 'el-icon-delete',
color: 'danger', auth: '/xxx/delete', onClick: this.handleDelete }
]
}, this.$permission) // <EFBFBD><EFBFBD>?第二个参数传入权限校验函<EFBFBD><EFBFBD>?
}, this.$permission) // 第二个参数传入权限校验函
this.toolbarButtons = btns.toolbarButtons
this.rowButtons = btns.rowButtons
}
@@ -69,7 +69,7 @@ export default {
```vue
<template>
<d2-container>
<!-- 搜索<EFBFBD><EFBFBD>?-->
<!-- 搜索 -->
<template #header>
<el-form :inline="true" size="mini">
<el-form-item :label="$t(key('code'))">
@@ -87,7 +87,7 @@ export default {
</el-form>
</template>
<!-- 表格 + 按钮<EFBFBD><EFBFBD>?+ 分页 -->
<!-- 表格 + 按钮 + 分页 -->
<page-table
:columns="columns"
:data="tableData"
@@ -96,6 +96,7 @@ export default {
:row-buttons="rowButtons"
:pagination="pagination"
help-url="/help/your-page"
:help-text="$t(ckey('help'))"
auto-height
@page-change="onPageChange"
@selection-change="onSelect"
@@ -111,8 +112,8 @@ export default {
:form-data="formData"
:rules="rules"
:submitting="submitting"
:confirm-text="$t(key('confirm'))"
:cancel-text="$t(key('cancel'))"
:confirm-text="key('confirm')"
:cancel-text="key('cancel')"
@submit="onDialogSubmit"
@close="onDialogClose"
/>
@@ -120,7 +121,8 @@ export default {
</template>
```
### 第三步:写业务方法(增删改查<EFBFBD><EFBFBD>?
### 第三步:写业务方法(增删改查
```js
methods: {
// 获取列表数据
@@ -159,7 +161,8 @@ methods: {
})
},
// 编辑:回填表<EFBFBD><EFBFBD>? openEdit (row) {
// 编辑:回填表
openEdit (row) {
this.handleType = 'edit'
this.dialogTitle = this.key('edit_title')
this.editId = row.id
@@ -198,7 +201,7 @@ methods: {
await deleteApi({ id: [row.id] })
this.$message.success(this.$t(this.key('operation_success')))
this.fetchData()
} catch (e) { /* 取消或失<EFBFBD><EFBFBD>?*/ }
} catch (e) { /* 取消或失*/ }
}
}
```
@@ -208,7 +211,8 @@ methods: {
## 2. 完整示例代码
> 📁 `src/views/production-master-data/factory-model/factory-area/index.vue`
> 这是一<EFBFBD><EFBFBD>?*可直接运行的完整 CRUD 页面**,包含搜索栏、表格、分页、新<EFBFBD><EFBFBD>?编辑弹框、删除确认<EFBFBD><EFBFBD>?
> 这是一个**可直接运行的完整 CRUD 页面**,包含搜索栏、表格、分页、新增/编辑弹框、删除确认
### 模板部分
```vue
@@ -246,6 +250,7 @@ methods: {
:row-buttons="rowButtons"
:pagination="pagination"
help-url="/help/factory-area"
:help-text="$t(ckey('help'))"
auto-height
@page-change="onPageChange"
@selection-change="onSelect"
@@ -260,8 +265,8 @@ methods: {
:form-data="formData"
:rules="rules"
:submitting="submitting"
:confirm-text="$t(key('confirm'))"
:cancel-text="$t(key('cancel'))"
:confirm-text="key('confirm')"
:cancel-text="key('cancel')"
@submit="onDialogSubmit"
@close="onDialogClose"
/>
@@ -286,8 +291,6 @@ export default {
mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
data () {
const t = this.$t.bind(this)
const k = (s) => t(this.key(s))
return {
loading: false,
submitting: false,
@@ -302,12 +305,12 @@ export default {
formData: { code: '', name: '', remark: '' },
rules: {
code: [
{ required: true, message: k('enter_code'), trigger: 'blur' },
{ min: 1, max: 100, message: k('remark_length'), trigger: 'blur' }
{ required: true, message: this.key('enter_code'), trigger: 'blur' },
{ min: 1, max: 100, message: this.key('remark_length'), trigger: 'blur' }
],
name: [
{ required: true, message: k('enter_name'), trigger: 'blur' },
{ min: 1, max: 100, message: k('remark_length'), trigger: 'blur' }
{ required: true, message: this.key('enter_name'), trigger: 'blur' },
{ min: 1, max: 100, message: this.key('remark_length'), trigger: 'blur' }
]
},
columns: [],
@@ -316,18 +319,18 @@ export default {
formCols: [
[
{ type: 'input', prop: 'code',
label: k('code'), placeholder: k('enter_code'),
label: this.key('code'), placeholder: this.key('enter_code'),
clearable: true, style: { width: '90%' } }
],
[
{ type: 'input', prop: 'name',
label: k('name'), placeholder: k('enter_name'),
label: this.key('name'), placeholder: this.key('enter_name'),
clearable: true, style: { width: '90%' } }
],
[
{ type: 'input', prop: 'remark', inputType: 'textarea',
autosize: { minRows: 2, maxRows: 6 },
label: k('remark'), placeholder: k('remark_required'),
label: this.key('remark'), placeholder: this.key('remark_required'),
clearable: true, style: { width: '90%' } }
]
]
@@ -436,7 +439,7 @@ export default {
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
)
this.fetchData()
} catch (e) { /* 取消删除 / 请求失败不处<EFBFBD><EFBFBD>?*/ }
} catch (e) { /* 取消删除 / 请求失败不处*/ }
}
}
}
@@ -444,111 +447,115 @@ export default {
---
## 3. 组件 API 参<EFBFBD><EFBFBD>?
### 3.1 `page-table` <20><>?表格 + 按钮<E68C89><E992AE>?+ 分页
## 3. 组件 API 参
### 3.1 `page-table` — 表格 + 按钮栏 + 分页
#### Props
| Prop | 类型 | 默认<EFBFBD><EFBFBD>?| 说明 |
| Prop | 类型 | 默认| 说明 |
|------|------|--------|------|
| `columns` | `Array` | `[]` | 列定义,<EFBFBD><EFBFBD>?`useTableColumns()` 生成 |
| `data` | `Array` | `[]` | 表格行数<EFBFBD><EFBFBD>?|
| `columns` | `Array` | `[]` | 列定义,`useTableColumns()` 生成 |
| `data` | `Array` | `[]` | 表格行数|
| `loading` | `Boolean` | `false` | 是否显示 loading 遮罩 |
| `height` | `String/Number` | <EFBFBD><EFBFBD>?| 表格高度。不传则随内容撑开传具体数值固定高度<EFBFBD><EFBFBD>?`'auto'` 启用自适应 |
| `auto-height` | `Boolean` | `false` | 启用高度自适应,表格自动填满可用空<EFBFBD><EFBFBD>?|
| `border` | `Boolean` | `true` | 是否带边<EFBFBD><EFBFBD>?|
| `height` | `String/Number` | | 表格高度,传 `'auto'` 启用自适应 |
| `auto-height` | `Boolean` | `false` | 启用高度自适应,填满可用空|
| `border` | `Boolean` | `true` | 是否带边|
| `row-key` | `String` | `'id'` | 行唯一 key |
| `toolbar-buttons` | `Array` | `[]` | 顶部工具栏按钮,<EFBFBD><EFBFBD>?`useTableButtons()` 生成 |
| `toolbar-buttons` | `Array` | `[]` | 顶部工具栏按钮,`useTableButtons()` 生成 |
| `row-buttons` | `Array` | `[]` | 行内操作按钮,由 `useTableButtons()` 生成 |
| `pagination` | `Object` | `null` | 分页参数 `{ current, size, total }`,传了才显示分页 |
| `table-attrs` | `Object` | `{}` | 额外透传<EFBFBD><EFBFBD>?`el-table` 的属<EFBFBD><EFBFBD>?|
| `table-listeners` | `Object` | `{}` | 额外透传<EFBFBD><EFBFBD>?`el-table` 的事<EFBFBD><EFBFBD>?|
| `help-url` | `String` | `''` | 帮助文档跳转 URL传了显示工具栏右侧的问号按钮,点击新窗口打开 |
| `help-text` | `String` | `'帮助'` | 帮助按钮文字,支<EFBFBD><EFBFBD>?i18n key组件自<EFBFBD><EFBFBD>?`$t()` 翻译<E7BFBB><E8AF91>?|
| `pagination` | `Object` | `null` | 分页参数 `{ current, size, total }` |
| `table-attrs` | `Object` | `{}` | 额外透传`el-table` 的属|
| `table-listeners` | `Object` | `{}` | 额外透传`el-table` 的事|
| `help-url` | `String` | `''` | 帮助文档 URL传了显示问号按钮 |
| `help-text` | `String` | `'帮助'` | 帮助按钮文字,支i18n key |
#### 事件
| 事件<EFBFBD><EFBFBD>?| 参数 | 说明 |
| 事件| 参数 | 说明 |
|--------|------|------|
| `@page-change` | `{ current, size, total }` | 分页变化切换页<EFBFBD><EFBFBD>?条数<E69DA1><E695B0>?|
| `@selection-change` | `rows: Array` | 选中行变<EFBFBD><EFBFBD>?|
| `@sort-change` | 透传 el-table 原生事件 | 排序变化 |
| `@page-change` | `{ current, size, total }` | 分页变化 |
| `@selection-change` | `rows: Array` | 选中行变|
#### 插槽
| 插槽<EFBFBD><EFBFBD>?| 作用<EFBFBD><EFBFBD>?| 说明 |
| 插槽| 作用| 说明 |
|--------|--------|------|
| `#col-{prop}` | `{ row, index }` | 自定义列渲染(列定义中 prop 需<><E99C80>?`slot: true`<EFBFBD><EFBFBD>?|
| `#toolbar-extra` | <EFBFBD><EFBFBD>?| 工具栏区域追加自定义内容 |
| `#empty` | <EFBFBD><EFBFBD>?| 表格空数据时的占<E79A84><E58DA0>?|
| `#append` | <EFBFBD><EFBFBD>?| 表格最后一行后追加 |
| `#extra` | <EFBFBD><EFBFBD>?| 页面底部追加区域 |
| `#col-{prop}` | `{ row, index }` | 自定义列渲染(列需设 `slot: true` |
| `#toolbar-extra` | | 工具栏追加内容 |
| `#empty` | — | 空数据占位 |
| `#append` | | 表格末尾追加 |
| `#extra` | | 页面底部追加 |
#### 列定义规范
#### 列定义规<E4B989><E8A784>?
```js
// 普通列
{ prop: 'code', label: '编码', minWidth: 120 }
// 操作列约定prop === '_actions'<EFBFBD><EFBFBD>?{ prop: '_actions', label: '操作', width: 160, fixed: 'right' }
// 操作列约定prop === '_actions'
{ prop: '_actions', label: '操作', width: 160, fixed: 'right' }
// 自定义插槽列slot: true<EFBFBD><EFBFBD>?{ prop: 'status', label: '状<><E78AB6>?, slot: true, width: 100 }
// 自定义插槽列slot: true
{ prop: 'status', label: '状态', slot: true, width: 100 }
// 复选框<EFBFBD><EFBFBD>?+ 序号列(通过 useTableColumns 第二个参数)
// 复选框 + 序号列(通过 useTableColumns 第二个参数)
useTableColumns([...], { selectionWidth: 55, indexWidth: 60 })
```
---
### 3.2 `page-dialog-form` <EFBFBD><EFBFBD>?增删改查弹框
### 3.2 `page-dialog-form` 增删改查弹框
#### Props
| Prop | 类型 | 默认<EFBFBD><EFBFBD>?| 说明 |
| Prop | 类型 | 默认| 说明 |
|------|------|--------|------|
| `visible` | `Boolean` | `false` | 弹框显隐,使<EFBFBD><EFBFBD>?`.sync` 修饰<EFBFBD><EFBFBD>?|
| `title` | `String` | `''` | 弹框标题,支<EFBFBD><EFBFBD>?i18n key |
| `visible` | `Boolean` | `false` | 弹框显隐,使`.sync` |
| `title` | `String` | `''` | 弹框标题,支i18n key |
| `width` | `String` | `'35%'` | 弹框宽度 |
| `form-cols` | `Array` | `[]` | 表单字段结构(二维数组,见下方 |
| `form-cols` | `Array` | `[]` | 表单字段结构(二维数组) |
| `form-data` | `Object` | `{}` | 表单数据对象 |
| `rules` | `Object` | `{}` | 校验规则,与 `el-form` rules 一<EFBFBD><EFBFBD>?|
| `rules` | `Object` | `{}` | 校验规则,与 `el-form` rules 一|
| `label-width` | `String` | `'100px'` | label 宽度 |
| `submitting` | `Boolean` | `false` | 提交 loading 状<EFBFBD><EFBFBD>?|
| `confirm-text` | `String` | `'确定'` | 确定按钮文字 |
| `cancel-text` | `String` | `'取消'` | 取消按钮文字 |
| `submitting` | `Boolean` | `false` | 提交 loading 状|
| `confirm-text` | `String` | `'确定'` | 确定按钮文字,支持 i18n key |
| `cancel-text` | `String` | `'取消'` | 取消按钮文字,支持 i18n key |
#### 事件
| 事件<EFBFBD><EFBFBD>?| 说明 |
| 事件| 说明 |
|--------|------|
| `@submit` | 表单验证通过后触发,父组件执行提交逻辑 |
| `@close` | 弹框关闭后触<EFBFBD><EFBFBD>?|
| `@submit` | 表单验证通过后触发 |
| `@close` | 弹框关闭后触|
#### 方法(通过 ref 调用)
#### 方法(通过 ref 调用<E8B083><E794A8>?
| 方法 | 说明 |
|------|------|
| `reset()` | 重置表单 |
| `validate()` | 手动验证,返<EFBFBD><EFBFBD>?`Promise<boolean>` |
| `validate()` | 手动验证,返`Promise<boolean>` |
#### formCols 数据结构
**注意<EFBC9A><E99C80>?`data()` 中用 `k(prop)` 提前完成 i18n 翻译**,不要传 raw key<65><79>?
传入 i18n key`this.key()` 拼接完整 key组件内部自动翻译且跟随语言切换
```js
// 例:两个普通输入框 + 一个多行文本框
formCols: [
[
{ type: 'input', prop: 'code',
label: k('code'), placeholder: k('enter_code'),
label: this.key('code'), placeholder: this.key('enter_code'),
clearable: true, style: { width: '90%' } }
],
[
{ type: 'input', prop: 'name',
label: k('name'), placeholder: k('enter_name'),
label: this.key('name'), placeholder: this.key('enter_name'),
clearable: true, style: { width: '90%' } }
],
[
{ type: 'input', prop: 'remark', inputType: 'textarea',
autosize: { minRows: 2, maxRows: 6 },
label: k('remark'), placeholder: k('remark_required'),
label: this.key('remark'), placeholder: this.key('remark_required'),
clearable: true, style: { width: '90%' } }
]
]
@@ -556,14 +563,14 @@ formCols: [
**字段支持的属性:**
| 属<EFBFBD><EFBFBD>?| 适用类型 | 说明 |
| 属| 适用类型 | 说明 |
|------|---------|------|
| `type` | 全部 | `'input'`(文本输入/ `'select'`(下拉) |
| `type` | 全部 | `'input'`(文本) / `'select'`(下拉) |
| `prop` | 全部 | 绑定 `formData` 中的 key |
| `label` | 全部 | 表单项标<EFBFBD><EFBFBD>?|
| `placeholder` | 全部 | 占位提示 |
| `inputType` | `input` | `'textarea'` 多行 / 不传为普<EFBFBD><EFBFBD>?text |
| `autosize` | `input` | textarea <EFBFBD><EFBFBD>?`{ minRows, maxRows }` |
| `label` | 全部 | 表单项标签,支持 i18n key |
| `placeholder` | 全部 | 占位提示,支持 i18n key |
| `inputType` | `input` | `'textarea'` / 不传为普text |
| `autosize` | `input` | textarea 自适应:`{ minRows, maxRows }` |
| `clearable` | 全部 | 是否可清空,默认 `true` |
| `style` | 全部 | 样式对象 |
| `options` | `select` | 选项数组 `[{ label, value }]` |
@@ -571,13 +578,15 @@ formCols: [
---
## 4. Composable API 参<EFBFBD><EFBFBD>?
## 4. Composable API 参
### 4.1 `useTableColumns(rawColumns, options?)`
**作用**:消除手动分<EFBFBD><EFBFBD>?`idx` 序号,自动识别操作列和插槽列<EFBFBD><EFBFBD>?
| 参数 | 类型 | 默认<E9BB98><E8AEA4>?| 说明 |
**作用**:消除手动分`idx` 序号,自动识别操作列和插槽列
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `rawColumns` | `Array` | <EFBFBD><EFBFBD>?| 列定<EFBFBD><EFBFBD>?|
| `rawColumns` | `Array` | | 列定|
| `options.selectionWidth` | `number` | `55` | 复选框列宽,传 `0` 隐藏 |
| `options.indexWidth` | `number` | `0` | 序号列宽,传 `0` 隐藏 |
@@ -585,37 +594,40 @@ formCols: [
const columns = useTableColumns([
{ prop: 'code', label: '编码', width: 120 },
{ prop: 'name', label: '名称' },
{ prop: '_actions', label: '操作', width: 160, fixed: 'right' } // <EFBFBD><EFBFBD>?操作<E6938D><E4BD9C>?])
{ prop: '_actions', label: '操作', width: 160, fixed: 'right' } // 操作列
])
```
**内部约定**`prop === '_actions'` 自动标记<EFBFBD><EFBFBD>?`slot: '_actions'`,由 `page-table` 渲染为操作列<EFBFBD><EFBFBD>?
**内部约定**`prop === '_actions'` 自动标记`slot: '_actions'`,由 `page-table` 渲染为操作列
---
### 4.2 `useTableButtons(options, permissionCheck?)`
**作用**:统一生成工具栏按钮和行内操作按钮,自动注入权限校验结果<EFBFBD><EFBFBD>?
**作用**:统一生成工具栏按钮和行内操作按钮,自动注入权限校验结果
| 参数 | 类型 | 说明 |
|------|------|------|
| `options.toolbar` | `Array` | 顶部按钮列表 |
| `options.row` | `Array` | 行内按钮列表 |
| `permissionCheck` | `Function` | 权限函数,通常<EFBFBD><EFBFBD>?`this.$permission` |
| `permissionCheck` | `Function` | 权限函数,通常`this.$permission` |
**返回<EFBFBD><EFBFBD>?*`{ toolbarButtons, rowButtons }`
**返回值**`{ toolbarButtons, rowButtons }`
**按钮字段<EFBFBD><EFBFBD>?*
**按钮字段**
| 字段 | toolbar | row | 说明 |
|------|:---:|:---:|------|
| `key` | <EFBFBD><EFBFBD>?| <20><>?| 唯一标识 |
| `label` | <EFBFBD><EFBFBD>?| <20><>?| 显示文本 |
| `icon` | <EFBFBD><EFBFBD>?| <20><>?| Element UI 图标 |
| `type` | <EFBFBD><EFBFBD>?| <20><>?| `primary`/`success`/`warning`/`danger` |
| `color` | <EFBFBD><EFBFBD>?| <20><>?| `'danger'` 使文字变<EFBFBD><EFBFBD>?|
| `auth` | <EFBFBD><EFBFBD>?| <20><>?| 权限 key |
| `cssStyle` | <EFBFBD><EFBFBD>?| <20><>?| 自定义样<EFBFBD><EFBFBD>?|
| `onClick` | <EFBFBD><EFBFBD>?| <20><>?| 点击回调 |
| `hasPermission` | <EFBFBD><EFBFBD>?| <20><>?| **自动注入**,由权限函数计算 |
| `needSelection` | <EFBFBD><EFBFBD>?| <20><>?| 需选中行才能点<EFBFBD><EFBFBD>?|
| `key` | √ | √ | 唯一标识 |
| `label` | √ | √ | 显示文本 |
| `icon` | √ | √ | Element UI 图标 |
| `type` | √ | — | `primary`/`success`/`warning`/`danger` |
| `color` | — | √ | `'danger'` 使文字变|
| `auth` | √ | √ | 权限 key |
| `cssStyle` | √ | √ | 自定义样|
| `onClick` | √ | √ | 点击回调 |
| `hasPermission` | √ | √ | **自动注入**,由权限函数计算 |
| `needSelection` | √ | — | 需选中行才能点|
```js
const btns = useTableButtons({
@@ -636,55 +648,63 @@ const btns = useTableButtons({
### 4.3 `i18nMixin(prefix)`
**作用**:注<EFBFBD><EFBFBD>?`key(suffix)` <EFBFBD><EFBFBD>?`ckey(suffix)` 两个方法,消除每个页面手<EFBFBD><EFBFBD>?`T` 常量<E5B8B8><E9878F>?`tkey` 方法<EFBFBD><EFBFBD>?
| 方法 | 返回<E8BF94><E59B9E>?| 说明 |
**作用**:注`key(suffix)` `ckey(suffix)` 两个方法,消除每个页面手动定义常量和 `tkey` 方法
| 方法 | 返回值 | 说明 |
|------|--------|------|
| `key(suffix)` | `prefix.suffix` | 当前页面<EFBFBD><EFBFBD>?i18n key |
| `ckey(suffix)` | `page.common.suffix` | 公共 i18n key(如"帮助"<22><>?确定"<22><>?取消"<22><>?|
| `key(suffix)` | `prefix.suffix` | 当前页面i18n key |
| `ckey(suffix)` | `page.common.suffix` | 公共 i18n key |
| 参数 | 类型 | 说明 |
|------|------|------|
| `prefix` | `String` | 该页面的 i18n key 前缀,如 `'page.production_master_data.factory_model.factory_area'` |
| `prefix` | `String` | 该页面的 i18n key 前缀 |
```js
import { i18nMixin } from '@/composables/useI18n'
export default {
mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
// 此后模板中用 $t(key('code')) 访问当前页面的翻<EFBFBD><EFBFBD>? // <20><>?$t(ckey('help')) 访问公共翻译 'page.common.help'
// 模板中用 $t(key('code')) 访问当前页面翻译
// 模板中用 $t(ckey('help')) 访问公共翻译 'page.common.help'
// JS 中用 this.$t(this.key('code')) / this.$t(this.ckey('help'))
}
```
**`data()`翻译文本的技<EFBFBD><EFBFBD>?*<2A><>?
**`data()` i18n key 传参技巧**
```js
data () {
const t = this.$t.bind(this) // 绑定 i18n 翻译函数
const k = (s) => t(this.key(s)) // 当前页面翻译key + translate
const ck = (s) => t(this.ckey(s)) // 公共翻译ckey + translate
return {
// 传入完整 i18n key组件内部 $t() 翻译,切换语言自动响应
formCols: [
[{ label: k('code'), placeholder: k('enter_code') }] // 页面<E9A1B5><E99DA2>?key
[{ label: this.key('code'), placeholder: this.key('enter_code') }]
],
helpText: ck('help') // 公共 key
rules: {
code: [{ required: true, message: this.key('enter_code'), trigger: 'blur' }]
}
}
}
```
> **原理**`page-table` / `page-dialog-form` 内部使用 `$t()` 翻译传入的 key。`$t()` 是 Vue I18n 的响应式方法,切换语言时自动返回新的翻译结果,触发组件重新渲染。不要用 `k()` 等辅助函数提前翻译成静态字符串,否则语言切换后不会更新。
---
## 5. i18n 国际化方<EFBFBD><EFBFBD>?
### 5.1 语言包文<E58C85><E69687>?
## 5. i18n 国际化方
### 5.1 语言包文件
| 语言 | 文件路径 |
|------|---------|
| 简体中<EFBFBD><EFBFBD>?| `src/locales/zh-chs.json` |
| 简体中| `src/locales/zh-chs.json` |
| 繁体中文 | `src/locales/zh-cht.json` |
| 英文 | `src/locales/en.json` |
| 日文 | `src/locales/ja.json` |
### 5.2 语言包结<EFBFBD><EFBFBD>?
<EFBFBD><EFBFBD>?`一级模<E7BAA7><E6A8A1>?<3F><>?二级模块 <20><>?三级模块` 三层嵌套<E5B58C><E5A597>?
### 5.2 语言包结
`一级模块 > 二级模块 > 三级模块` 三层嵌套:
```json
{
"page": {
@@ -693,20 +713,23 @@ data () {
"factory_area": {
"search": "查询",
"reset": "重置",
"code": "所区编<EFBFBD><EFBFBD>?,
"name": "所区名<EFBFBD><EFBFBD>?,
"add": "<EFBFBD><EFBFBD>?<3F><>?,
"edit": "<EFBFBD><EFBFBD>?<3F><>?,
"delete": "<EFBFBD><EFBFBD>?<3F><>?,
"enter_code": "请输入所区编<EFBFBD><EFBFBD>?,
"enter_name": "请输入所区名<EFBFBD><EFBFBD>?,
"add_title": "新增所<EFBFBD><EFBFBD>?,
"edit_title": "编辑所<EFBFBD><EFBFBD>?,
"code": "所区编码",
"name": "所区名称",
"add": "新 增",
"edit": "编 辑",
"delete": "删 除",
"enter_code": "请输入所区编码",
"enter_name": "请输入所区名称",
"add_title": "新增所区",
"edit_title": "编辑所区",
"confirm": "确定",
"cancel": "取消",
"operation_success": "操作成功"
}
}
},
"common": {
"help": "帮 助"
}
}
}
@@ -714,17 +737,22 @@ data () {
### 5.3 组件自动翻译
`page-table` <EFBFBD><EFBFBD>?`page-dialog-form` `$t()`<EFBFBD><EFBFBD>?
- **<2A><>?label** <20><>?自动翻译
- **按钮 label** <EFBFBD><EFBFBD>?自动翻译
- **表单 label / placeholder** <20><>?自动翻译
- **弹框 title / confirm / cancel** <20><>?自动翻译
`page-table` `page-dialog-form`部使用 `$t()` 翻译:
- ** label** 自动翻译
- **按钮 label** — 自动翻译
- **表单 label / placeholder** — 自动翻译
- **弹框 title / confirm / cancel** — 自动翻译
- **验证规则 message** — 自动翻译(`translatedRules` 计算属性)
页面只需传入 i18n key组件渲染时自动替换为当前语言的文本。
### 5.4 页面中手动翻译
搜索区、提示消息、`$confirm` 等 UI 文字需手动 `$t()`
因此页面只需传入 i18n key 字符串组件渲染时自动替换为当前语言的文本<E69687><E69CAC>?
### 5.4 页面中手动翻<E58AA8><E7BFBB>?
搜索区、校验消息、`$confirm` <20><>?UI 文字需手动<E6898B><E58AA8>?`$t()`<EFBFBD><EFBFBD>?
```vue
<!-- 模板<EFBFBD><EFBFBD>?<EFBFBD><EFBFBD>?key() 返回 i18n key -->
<!-- 模板key() 返回 i18n key -->
<el-form-item :label="$t(key('code'))">
<el-input :placeholder="$t(key('enter_code'))" />
</el-form-item>
@@ -732,7 +760,8 @@ data () {
```
```js
// JS <EFBFBD><EFBFBD>?this.$message.success(this.$t(this.key('operation_success')))
// JS
this.$message.success(this.$t(this.key('operation_success')))
this.$confirm(this.$t(this.key('confirm_delete')), this.$t(this.key('tip')), { ... })
```
@@ -740,15 +769,18 @@ this.$confirm(this.$t(this.key('confirm_delete')), this.$t(this.key('tip')), { .
## 6. 常用场景速查
### 场景 1自定义列渲染<EFBFBD><EFBFBD>?tag<61><67>?
列定义加<EFBFBD><EFBFBD>?`slot: true`<EFBFBD><EFBFBD>?
### 场景 1自定义列渲染态 tag
列定义加 `slot: true`
```js
useTableColumns([
{ prop: 'status', label: '<EFBFBD><EFBFBD>?, slot: true, width: 100 }
{ prop: 'status', label: '状态', slot: true, width: 100 }
])
```
模板中用 `#col-status` 插槽<EFBFBD><EFBFBD>?
模板中用 `#col-status` 插槽
```vue
<page-table :columns="columns" :data="tableData">
<template #col-status="{ row }">
@@ -759,7 +791,8 @@ useTableColumns([
</page-table>
```
### 场景 2工具栏追加自定义按<EFBFBD><EFBFBD>?
### 场景 2工具栏追加自定义按
```vue
<page-table :columns="columns" :toolbar-buttons="toolbarButtons">
<template #toolbar-extra>
@@ -768,7 +801,8 @@ useTableColumns([
</page-table>
```
### 场景 3复选框<EFBFBD><EFBFBD>?+ 序号<EFBFBD><EFBFBD>?
### 场景 3复选框 + 序号
```js
useTableColumns(
[
@@ -779,47 +813,52 @@ useTableColumns(
)
```
### 场景 4带下拉选择的表<EFBFBD><EFBFBD>?
### 场景 4带下拉选择的表
```js
formCols: [
[
{ type: 'select', prop: 'area_id',
label: k('area'), placeholder: k('select_area'),
label: this.key('area'), placeholder: this.key('select_area'),
options: [{ label: 'A厂区', value: 1 }, { label: 'B厂区', value: 2 }],
clearable: true, style: { width: '90%' } }
]
]
```
### 场景 5批量删除工具<EFBFBD><EFBFBD>?+ 需要选中行)
### 场景 5批量删除工具+ 需要选中行)
```js
useTableButtons({
toolbar: [
{ key: 'batchDelete', label: '批量删除', icon: 'el-icon-delete',
type: 'danger', auth: '/xxx/batch-delete',
needSelection: true, // <EFBFBD><EFBFBD>?需要选中行才能点<EFBFBD><EFBFBD>? onClick: this.batchDelete }
needSelection: true, // 需要选中行才能点
onClick: this.batchDelete }
]
})
```
### 场景 6表格自适应高度
只需<EFBFBD><EFBFBD>?`auto-height` 属性:
只需`auto-height` 属性:
```vue
<page-table ... auto-height />
```
表格高度自动填<EFBFBD><EFBFBD>?`d2-container` <EFBFBD><EFBFBD>?body 区域剩余空间,窗<EFBFBD><EFBFBD>?resize / 侧栏折叠时自动重算<EFBFBD><EFBFBD>?
### 场景 7帮助按<E58AA9><E68C89>?
<EFBFBD><EFBFBD>?`help-url` 传值即可在工具栏最右侧显示问号帮助按钮点击在新窗口打开帮助文档<E69687><E6A1A3>?
表格高度自动填`d2-container` body 区域剩余空间,窗resize / 侧栏折叠时自动重算
### 场景 7帮助按钮
`help-url` 即可在工具栏最右侧显示问号帮助按钮:
```vue
<page-table ... help-url="/help/factory-area" :help-text="$t(ckey('help'))" />
```
按钮文字通过 `help-text` prop 支持 i18n。推荐使用公<EFBFBD><EFBFBD>?key `page.common.help`(模板中<EFBFBD><EFBFBD>?`$t(ckey('help'))`所有页面共享无需每个模块重复定义<EFBFBD><EFBFBD>?
不传 `help-url` 或传空字符串则不显示帮助按钮<E68C89><E992AE>?
推荐使用公key `page.common.help`(模板中`$t(ckey('help'))`。不传 `help-url` 则不显示。
---
## 7. 路由配置
@@ -845,7 +884,8 @@ export default {
}
```
之后<EFBFBD><EFBFBD>?`src/router/routes.js` 中引入该模块并加<EFBFBD><EFBFBD>?`frameIn` 数组<EFBFBD><EFBFBD>?
之后`src/router/routes.js` 中引入该模块并加`frameIn` 数组
```js
import productionConfiguration from './modules/production-master-data'
@@ -908,46 +948,43 @@ export function deleteFactoryArea (data) {
---
## 9. 旧代码迁移对<EFBFBD><EFBFBD>?
| 旧写<E697A7><E58699>?| 新写<E696B0><E58699>?|
## 9. 旧代码迁移对
| 旧写法 | 新写法 |
|--------|--------|
| 手动构建 `columns: [{ idx: 0, attrs: { prop, label } }]` | `useTableColumns([{ prop, label }])` |
| `buttonList: [...]` + `tableButtonList: [...]` 分两<E58886><E4B8A4>?| `useTableButtons({ toolbar: [...], row: [...] })` |
| `<sct-base-table>` + `<sct-base-dialog>` + `<SctBaseForm>` 三层 | `<page-table>` + `<page-dialog-form>` 两层 |
| 手动构建 `columns``idx` 序号 | `useTableColumns([{ prop, label }])` |
| `buttonList` + `tableButtonList` 分开 | `useTableButtons({ toolbar: [...], row: [...] })` |
| `<sct-base-table>` + `<sct-base-dialog>` + `<SctBaseForm>` | `<page-table>` + `<page-dialog-form>` |
| `<sct-form-search>` 组件 | 原生 `<el-form :inline>` |
| `<sct-back-to-top>` 组件 | 需要时自行添加 |
| 分页需要额<EFBFBD><EFBFBD>?`<page-footer>` | `page-table` 内置分页 |
| 每个页面定义 `T` 常量 + `tkey()` 方法 | `mixins: [i18nMixin(prefix)]`,使<EFBFBD><EFBFBD>?`this.key('xxx')` |
| 表单字段<E5AD97><E6AEB5>?i18n raw key子组件翻译 | `data()` 中用 `k(prop)` 提前翻译 |
| 额外 `<page-footer>` 分页 | `page-table` 内置分页 |
| 每个页面定义 `T` 常量 + `tkey()` 方法 | `mixins: [i18nMixin(prefix)]`,使`this.key('xxx')` |
| `data()` 中用 `k()` 提前翻译 |`this.key()` 传 key组件内部 `$t()` 翻译 |
---
## 10. 常见问题排查
### Q1弹框打开后不显示内容<EFBFBD><EFBFBD>?
确认 `formCols` 中的 `label` <20><>?`placeholder` 是否<E698AF><E590A6>?`data()` 阶段已翻译。子组件 `page-dialog-form` 会调<E4BC9A><E8B083>?`$t()` 处理 label但建议<E5BBBA><E8AEAE>?`data()` 阶段就用 `k()` 提前翻译好,避免 webpack HMR 缓存问题<E997AE><E9A298>?
### Q1弹框打开后不显示内容
确认 `formCols` 中的 `label``placeholder` 是否传入了正确的 i18n key。应该用 `this.key()` 拼接完整 key不要用 `k()` 提前翻译成静态字符串。
### Q2表格高度自适应不生效
确认 `d2-container` <EFBFBD><EFBFBD>?body 区域高度被正确约束flex 填充)。<EFBFBD><EFBFBD>?`d2-container` 本身<E69CAC><E8BAAB>?`overflow: auto` 或其他高度约束问题,`page-table` <20><>?`height: 100%` 会失效。`d2-container` <EFBFBD><EFBFBD>?body 部分<EFBFBD><EFBFBD>?`overflow: hidden` 通常可解决<EFBFBD><EFBFBD>?
确认 `d2-container` body 区域高度被正确约束flex 填充)。给 `d2-container` body 部分设置 `overflow: hidden` 通常可解决
### Q3权限校验有按钮不显示
`useTableButtons` 的第二个参数必须<EFBFBD><EFBFBD>?`this.$permission`。如果项目没有注册这个全局方法,可以`useTableButtons` 中传入一个返<E4B8AA><E8BF94>?`true` 的占位函数:`() => true`<EFBFBD><EFBFBD>?
`useTableButtons` 的第二个参数必须传入 `this.$permission`。如果项目没有注册这个全局方法,可以传入 `() => true` 占位。
### Q4新增一条后页码不跳回第一页
<EFBFBD><EFBFBD>?`onDialogSubmit` 成功后调<EFBFBD><EFBFBD>?`this.fetchData()` 前,如果删除当前页最后一行导<EFBFBD><EFBFBD>?`total - 1` 超出范围,需手动修正页码<EFBFBD><EFBFBD>?
```js
this.pagination.current = Math.min(
this.pagination.current,
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
)
```
`onDialogSubmit` 成功后调`this.fetchData()` 前,如果删除当前页最后一行导`total - 1` 超出范围,需手动修正页码
### Q5表单验证不提示错误文字<EFBFBD><EFBFBD>?
确认 `page-dialog-form` <20><>?`<style>` 块存在(默认 22px `margin-bottom` + `position: absolute` 错误文字)。如果表单项之间被其他样式覆盖,检查是否有全局 CSS 重置<E9878D><E7BDAE>?`.el-form-item` <20><>?margin<69><6E>?
### Q6如何新增一<E5A29E><E4B880>?CRUD 页面最快?
### Q5切换语言后弹框/表格内容不更新?
1. 复制 `src/views/production-master-data/factory-model/factory-area/index.vue`
2. 替换 API 引用(`import { getList, create, edit, ... } from '@/api/xxx'`<EFBFBD><EFBFBD>?3. 修改 `i18nMixin` 参数、`columns``formCols``rules`
4. <20><>?`zh-chs.json` <20><>?`en.json` 中添加语言<E8AFAD><E8A880>?5. 添加路由配置
检查 `data()` 中是否使用了 `k()` 等辅助函数提前翻译。应该用 `this.key()` 传 i18n key由组件内部 `$t()` 处理翻译,这样切换语言时才能自动响应。
平均 10 分钟即可完成一个标<E4B8AA><E6A087>?CRUD 页面<E9A1B5><E99DA2>?
### Q6表单验证错误提示显示为原始 i18n key
`page-dialog-form` 会通过 `translatedRules` 计算属性自动翻译验证规则的 `message` 字段。确认传入的 `rules.message` 使用了 `this.key()` 传入完整 key。

View File

@@ -8,7 +8,7 @@
不支持复杂表单联动如有需要通过默认插槽自定义内容
依赖element-ui <el-dialog> <el-form> <el-input> <el-select>
i18n由调用方负责翻译组件直接渲染传入的文本
i18n组件内部通过 $t() 翻译传入的 i18n key切换语言时自动响应
@author 前端团队
@since 2026-05
@@ -22,7 +22,7 @@
-->
<el-dialog
:visible.sync="visibleProxy"
:title="title"
:title="$t(title)"
:width="width"
:close-on-click-modal="false"
:destroy-on-close="true"
@@ -37,7 +37,7 @@
<el-form
ref="form"
:model="formData"
:rules="rules"
:rules="translatedRules"
:label-width="labelWidth || '100px'"
>
<!--
@@ -51,7 +51,7 @@
<el-form-item
v-for="col in flatFormCols"
:key="col.prop"
:label="col.label"
:label="$t(col.label)"
:prop="col.prop"
>
<!-- ===== 输入框类型 ===== -->
@@ -64,7 +64,7 @@
<el-input
v-if="col.type === 'input'"
v-model="formData[col.prop]"
:placeholder="col.placeholder"
:placeholder="$t(col.placeholder)"
:type="col.inputType || 'text'"
:autosize="col.autosize"
:clearable="col.clearable !== false"
@@ -79,7 +79,7 @@
<el-select
v-else-if="col.type === 'select'"
v-model="formData[col.prop]"
:placeholder="col.placeholder"
:placeholder="$t(col.placeholder)"
:clearable="col.clearable !== false"
:style="col.style"
:filterable="col.filterable !== false"
@@ -107,8 +107,8 @@
由调用方负责翻译传已翻译的文本即可
-->
<template #footer>
<el-button @click="onClose">{{ cancelText }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ confirmText }}</el-button>
<el-button @click="onClose">{{ $t(cancelText) }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ $t(confirmText) }}</el-button>
</template>
</el-dialog>
</template>
@@ -243,6 +243,15 @@ export default {
*/
flatFormCols () {
return this.formCols.flat()
},
translatedRules () {
const rules = this.rules || {}
const translated = {}
for (const [field, validators] of Object.entries(rules)) {
translated[field] = validators.map(v => ({ ...v, message: this.$t(v.message) }))
}
return translated
}
},
methods: {
@@ -258,7 +267,6 @@ export default {
onSubmit () {
this.$refs.form.validate((valid, invalidFields) => {
if (!valid) {
// 验证失败:取第一条错误信息提示用户
const firstKey = Object.keys(invalidFields)[0]
if (firstKey && invalidFields[firstKey].length) {
const msg = invalidFields[firstKey][0].message

View File

@@ -35,7 +35,7 @@
:disabled="btn.needSelection && !selectedCount"
@click="btn.onClick"
>
{{ btn.label }}
{{ $t(btn.label) }}
</el-button>
<!-- 自定义工具栏内容如打印按钮列筛选器等 -->
<slot name="toolbar-extra" />
@@ -91,7 +91,7 @@
:key="'idx-' + col.idx"
type="index"
:width="col.width"
:label="col.label || '#'"
:label="$t(col.label) || '#'"
/>
<!-- 3. 操作列自动渲染 rowButtons编辑/删除等行内按钮 -->
<!--
@@ -382,6 +382,9 @@ export default {
delete attrs.idx
delete attrs.slot
delete attrs.headerSlot
if (attrs.label) {
attrs.label = this.$t(attrs.label)
}
return attrs
},

View File

@@ -58,8 +58,8 @@
:rules="rules"
:label-width="'100px'"
:submitting="submitting"
:confirm-text="$t(key('confirm'))"
:cancel-text="$t(key('cancel'))"
:confirm-text="key('confirm')"
:cancel-text="key('cancel')"
@submit="onDialogSubmit"
@close="onDialogClose"
/>
@@ -84,8 +84,6 @@ export default {
components: { PageTable, PageDialogForm },
mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
data () {
const t = this.$t.bind(this)
const k = (s) => t(this.key(s))
return {
loading: false,
submitting: false,
@@ -100,12 +98,12 @@ export default {
formData: { code: '', name: '', remark: '' },
rules: {
code: [
{ required: true, message: k('enter_code'), trigger: 'blur' },
{ min: 1, max: 100, message: k('remark_length'), trigger: 'blur' }
{ required: true, message: this.key('enter_code'), trigger: 'blur' },
{ min: 1, max: 100, message: this.key('remark_length'), trigger: 'blur' }
],
name: [
{ required: true, message: k('enter_name'), trigger: 'blur' },
{ min: 1, max: 100, message: k('remark_length'), trigger: 'blur' }
{ required: true, message: this.key('enter_name'), trigger: 'blur' },
{ min: 1, max: 100, message: this.key('remark_length'), trigger: 'blur' }
]
},
columns: [],
@@ -116,8 +114,8 @@ export default {
{
type: 'input',
prop: 'code',
label: k('code'),
placeholder: k('enter_code'),
label: this.key('code'),
placeholder: this.key('enter_code'),
clearable: true,
style: { width: '90%' }
}
@@ -126,8 +124,8 @@ export default {
{
type: 'input',
prop: 'name',
label: k('name'),
placeholder: k('enter_name'),
label: this.key('name'),
placeholder: this.key('enter_name'),
clearable: true,
style: { width: '90%' }
}
@@ -138,8 +136,8 @@ export default {
prop: 'remark',
inputType: 'textarea',
autosize: { minRows: 2, maxRows: 6 },
label: k('remark'),
placeholder: k('remark_required'),
label: this.key('remark'),
placeholder: this.key('remark_required'),
clearable: true,
style: { width: '90%' }
}