1. 新增角色管理后台页面、路由与国际化文案 2. 重构API请求错误处理逻辑,统一拦截业务与HTTP错误 3. 新增确认弹窗组合式函数,区分取消与请求错误场景 4. 完善表格按钮权限与显示控制逻辑 5. 更新API参数规范与文档说明 6. 修复部分页面分页数据解析问题
1200 lines
42 KiB
Markdown
1200 lines
42 KiB
Markdown
# 表格组件使用说明
|
||
|
||
> 基于 `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`
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [快速开始(三步走)](#1-快速开始三步走)
|
||
2. [完整示例代码](#2-完整示例代码)
|
||
3. [组件 API 参考](#3-组件-api-参考)
|
||
4. [Composable API 参考](#4-composable-api-参考)
|
||
5. [i18n 国际化方案](#5-i18n-国际化方案)
|
||
6. [常用场景速查](#6-常用场景速查)
|
||
7. [路由配置](#7-路由配置)
|
||
8. [API 文件写法](#8-api-文件写法)
|
||
9. [旧代码迁移对照](#9-旧代码迁移对照)
|
||
10. [接口请求错误处理规范](#10-接口请求错误处理规范)
|
||
11. [常见问题排查](#11-常见问题排查)
|
||
|
||
---
|
||
|
||
## 1. 快速开始(三步走)
|
||
|
||
### 第一步:定义列和按钮(`created()` 中)
|
||
|
||
```js
|
||
import { useTableColumns } from '@/composables/useTableColumns'
|
||
import { useTableButtons } from '@/composables/useTableButtons'
|
||
import { i18nMixin } from '@/composables/useI18n'
|
||
|
||
export default {
|
||
mixins: [i18nMixin('page.模块名.二级模块.三级模块')],
|
||
|
||
created () {
|
||
// --- 列定义 ---
|
||
// 只需声明 prop 和 label,idx 自动补齐
|
||
// prop: '_actions' 约定为操作列,自动渲染 rowButtons
|
||
this.columns = useTableColumns([
|
||
{ prop: 'sort', label: this.key('sort'), width: 80 },
|
||
{ prop: 'code', label: this.key('code'), minWidth: 120 },
|
||
{ prop: 'name', label: this.key('name') },
|
||
{ prop: '_actions', label: this.key('operation'), width: 160, fixed: 'right' }
|
||
])
|
||
|
||
// --- 按钮定义 ---
|
||
// 不再分开写 buttonList / tableButtonList
|
||
const btns = useTableButtons({
|
||
toolbar: [
|
||
{ key: 'add', label: this.key('add'), icon: 'el-icon-plus',
|
||
type: 'primary', auth: '/xxx/create', onClick: this.openAdd }
|
||
],
|
||
row: [
|
||
{ key: 'edit', label: this.key('edit'), icon: 'el-icon-edit',
|
||
auth: '/xxx/edit', onClick: this.openEdit },
|
||
{ key: 'delete', label: this.key('delete'), icon: 'el-icon-delete',
|
||
color: 'danger', auth: '/xxx/delete', onClick: this.handleDelete }
|
||
]
|
||
}, this.$permission) // 第二个参数传入权限校验函数
|
||
this.toolbarButtons = btns.toolbarButtons
|
||
this.rowButtons = btns.rowButtons
|
||
}
|
||
}
|
||
```
|
||
|
||
### 第二步:写模板(`<template>` 中)
|
||
|
||
```vue
|
||
<template>
|
||
<d2-container>
|
||
<!-- 搜索区 -->
|
||
<template #header>
|
||
<el-form :inline="true" size="mini">
|
||
<el-form-item :label="$t(key('code'))">
|
||
<el-input v-model="search.code" :placeholder="$t(key('enter_code'))"
|
||
clearable style="width:200px" @keyup.enter.native="onSearch" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="el-icon-search" @click="onSearch">
|
||
{{ $t(key('search')) }}
|
||
</el-button>
|
||
<el-button icon="el-icon-refresh" @click="onReset">
|
||
{{ $t(key('reset')) }}
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</template>
|
||
|
||
<!-- 表格 + 按钮栏 + 分页 -->
|
||
<page-table
|
||
:columns="columns"
|
||
:data="tableData"
|
||
:loading="loading"
|
||
:toolbar-buttons="toolbarButtons"
|
||
:row-buttons="rowButtons"
|
||
:pagination="pagination"
|
||
help-url="/help/your-page"
|
||
:help-text="$t(ckey('help'))"
|
||
auto-height
|
||
@page-change="onPageChange"
|
||
@selection-change="onSelect"
|
||
/>
|
||
|
||
<!-- 新增/编辑弹框 -->
|
||
<page-dialog-form
|
||
ref="dialogForm"
|
||
:visible.sync="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="35%"
|
||
:form-cols="formCols"
|
||
:form-data="formData"
|
||
:rules="rules"
|
||
:submitting="submitting"
|
||
:confirm-text="key('confirm')"
|
||
:cancel-text="key('cancel')"
|
||
@submit="onDialogSubmit"
|
||
@close="onDialogClose"
|
||
/>
|
||
</d2-container>
|
||
</template>
|
||
```
|
||
|
||
### 第三步:写业务方法(增删改查)
|
||
|
||
```js
|
||
methods: {
|
||
// 获取列表数据
|
||
async fetchData () {
|
||
this.loading = true
|
||
try {
|
||
const res = await getList({
|
||
...this.search,
|
||
page_no: this.pagination.current,
|
||
page_size: this.pagination.size
|
||
})
|
||
this.tableData = res.data || []
|
||
this.pagination.total = res.count || 0
|
||
} finally { this.loading = false }
|
||
},
|
||
|
||
// 搜索 / 重置
|
||
onSearch () { this.pagination.current = 1; this.fetchData() },
|
||
onReset () { this.search = { code: '', name: '' }; this.pagination.current = 1; this.fetchData() },
|
||
|
||
// 分页变化
|
||
onPageChange (page) {
|
||
this.pagination.current = page.current
|
||
this.pagination.size = page.size
|
||
this.fetchData()
|
||
},
|
||
|
||
// 新增:打开弹窗
|
||
openAdd () {
|
||
this.handleType = 'create'
|
||
this.dialogTitle = this.key('add_title')
|
||
this.$nextTick(() => {
|
||
this.$refs.dialogForm.reset()
|
||
this.resetForm()
|
||
this.dialogVisible = true
|
||
})
|
||
},
|
||
|
||
// 编辑:回填表单
|
||
openEdit (row) {
|
||
this.handleType = 'edit'
|
||
this.dialogTitle = this.key('edit_title')
|
||
this.editId = row.id
|
||
this.formData = { code: row.code, name: row.name }
|
||
this.dialogVisible = true
|
||
},
|
||
|
||
// 提交表单
|
||
async onDialogSubmit () {
|
||
this.submitting = true
|
||
try {
|
||
if (this.handleType === 'create') {
|
||
await createApi(this.formData)
|
||
} else {
|
||
await editApi({ ...this.formData, id: this.editId })
|
||
}
|
||
this.$message.success(this.$t(this.key('operation_success')))
|
||
this.dialogVisible = false
|
||
this.fetchData()
|
||
} finally { this.submitting = false }
|
||
},
|
||
|
||
// 关闭弹窗
|
||
onDialogClose () { this.resetForm() },
|
||
|
||
// 删除
|
||
async handleDelete (row) {
|
||
try {
|
||
await this.$confirm(
|
||
this.$t(this.key('confirm_delete')),
|
||
this.$t(this.key('tip')),
|
||
{ confirmButtonText: this.$t(this.key('confirm')),
|
||
cancelButtonText: this.$t(this.key('cancel')),
|
||
type: 'warning', closeOnClickModal: false }
|
||
)
|
||
await deleteApi({ id: [row.id] })
|
||
this.$message.success(this.$t(this.key('operation_success')))
|
||
this.fetchData()
|
||
} catch (e) { /* 取消或失败 */ }
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 完整示例代码
|
||
|
||
> 📁 `src/views/production-master-data/factory-model/factory-area/index.vue`
|
||
> 这是一个**可直接运行的完整 CRUD 页面**,包含搜索栏、表格、分页、新增/编辑弹框、删除确认。
|
||
|
||
### 模板部分
|
||
|
||
```vue
|
||
<template>
|
||
<d2-container>
|
||
<template #header>
|
||
<div class="search-bar">
|
||
<el-form :inline="true" size="mini">
|
||
<el-form-item :label="$t(key('code'))">
|
||
<el-input v-model="search.code" :placeholder="$t(key('enter_code'))"
|
||
clearable style="width:200px" @keyup.enter.native="onSearch" />
|
||
</el-form-item>
|
||
<el-form-item :label="$t(key('name'))">
|
||
<el-input v-model="search.name" :placeholder="$t(key('enter_name'))"
|
||
clearable style="width:200px" @keyup.enter.native="onSearch" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="el-icon-search" @click="onSearch">
|
||
{{ $t(key('search')) }}
|
||
</el-button>
|
||
<el-button icon="el-icon-refresh" @click="onReset">
|
||
{{ $t(key('reset')) }}
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</template>
|
||
|
||
<page-table
|
||
ref="pageTable"
|
||
:columns="columns"
|
||
:data="tableData"
|
||
:loading="loading"
|
||
:toolbar-buttons="toolbarButtons"
|
||
:row-buttons="rowButtons"
|
||
:pagination="pagination"
|
||
help-url="/help/factory-area"
|
||
:help-text="$t(ckey('help'))"
|
||
auto-height
|
||
@page-change="onPageChange"
|
||
@selection-change="onSelect"
|
||
/>
|
||
|
||
<page-dialog-form
|
||
ref="dialogForm"
|
||
:visible.sync="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="35%"
|
||
:form-cols="formCols"
|
||
:form-data="formData"
|
||
:rules="rules"
|
||
:submitting="submitting"
|
||
:confirm-text="key('confirm')"
|
||
:cancel-text="key('cancel')"
|
||
@submit="onDialogSubmit"
|
||
@close="onDialogClose"
|
||
/>
|
||
</d2-container>
|
||
</template>
|
||
```
|
||
|
||
### Script 部分
|
||
|
||
```js
|
||
import { useTableColumns } from '@/composables/useTableColumns'
|
||
import { useTableButtons } from '@/composables/useTableButtons'
|
||
import { i18nMixin } from '@/composables/useI18n'
|
||
import { getFactoryAreaList, createFactoryArea, editFactoryArea, deleteFactoryArea }
|
||
from '@/api/production-master-data/factory-area'
|
||
import PageTable from '@/components/page-table'
|
||
import PageDialogForm from '@/components/page-dialog-form'
|
||
|
||
export default {
|
||
name: 'factory-area',
|
||
components: { PageTable, PageDialogForm },
|
||
mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
|
||
|
||
data () {
|
||
return {
|
||
loading: false,
|
||
submitting: false,
|
||
tableData: [],
|
||
selectedRows: [],
|
||
dialogVisible: false,
|
||
dialogTitle: '',
|
||
editId: '',
|
||
handleType: 'create',
|
||
search: { code: '', name: '' },
|
||
pagination: { current: 1, size: 10, total: 0 },
|
||
formData: { code: '', name: '', remark: '' },
|
||
rules: {
|
||
code: [
|
||
{ required: true, message: this.key('enter_code'), trigger: 'blur' },
|
||
{ min: 1, max: 100, message: this.key('remark_length'), trigger: 'blur' }
|
||
],
|
||
name: [
|
||
{ required: true, message: this.key('enter_name'), trigger: 'blur' },
|
||
{ min: 1, max: 100, message: this.key('remark_length'), trigger: 'blur' }
|
||
]
|
||
},
|
||
columns: [],
|
||
toolbarButtons: [],
|
||
rowButtons: [],
|
||
formCols: [
|
||
[
|
||
{ type: 'input', prop: 'code',
|
||
label: this.key('code'), placeholder: this.key('enter_code'),
|
||
clearable: true, style: { width: '90%' } }
|
||
],
|
||
[
|
||
{ type: 'input', prop: '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: this.key('remark'), placeholder: this.key('remark_required'),
|
||
clearable: true, style: { width: '90%' } }
|
||
]
|
||
]
|
||
}
|
||
},
|
||
|
||
created () {
|
||
this.columns = useTableColumns([
|
||
{ prop: 'sort', label: this.key('sort'), width: 80 },
|
||
{ prop: 'code', label: this.key('code'), minWidth: 120 },
|
||
{ prop: 'name', label: this.key('name'), minWidth: 120 },
|
||
{ prop: 'remark', label: this.key('remark') },
|
||
{ prop: '_actions', label: this.key('operation'), width: 160, fixed: 'right' }
|
||
])
|
||
const btns = useTableButtons({
|
||
toolbar: [
|
||
{ key: 'add', label: this.key('add'), icon: 'el-icon-plus', type: 'primary',
|
||
auth: '/production_master_data/factory_model/factory_area/create',
|
||
onClick: this.openAdd }
|
||
],
|
||
row: [
|
||
{ key: 'edit', label: this.key('edit'), icon: 'el-icon-edit',
|
||
auth: '/production_master_data/factory_model/factory_area/edit',
|
||
onClick: this.openEdit },
|
||
{ key: 'delete', label: this.key('delete'), icon: 'el-icon-delete',
|
||
color: 'danger',
|
||
auth: '/production_master_data/factory_model/factory_area/delete',
|
||
onClick: this.handleDelete }
|
||
]
|
||
}, this.$permission)
|
||
this.toolbarButtons = btns.toolbarButtons
|
||
this.rowButtons = btns.rowButtons
|
||
this.fetchData()
|
||
},
|
||
|
||
methods: {
|
||
async fetchData () {
|
||
this.loading = true
|
||
try {
|
||
const res = await getFactoryAreaList({
|
||
...this.search, page_no: this.pagination.current, page_size: this.pagination.size
|
||
})
|
||
this.tableData = res.data || []
|
||
this.pagination.total = res.count || 0
|
||
} finally { this.loading = false }
|
||
},
|
||
|
||
onSearch () { this.pagination.current = 1; this.fetchData() },
|
||
onReset () { this.search = { code: '', name: '' }; this.pagination.current = 1; this.fetchData() },
|
||
onPageChange (page) {
|
||
this.pagination.current = page.current
|
||
this.pagination.size = page.size
|
||
this.fetchData()
|
||
},
|
||
onSelect (rows) { this.selectedRows = rows },
|
||
|
||
resetForm () { this.formData = { code: '', name: '', remark: '' }; this.editId = '' },
|
||
|
||
openAdd () {
|
||
this.handleType = 'create'
|
||
this.dialogTitle = this.key('add_title')
|
||
this.$nextTick(() => {
|
||
this.$refs.dialogForm && this.$refs.dialogForm.reset()
|
||
this.resetForm()
|
||
this.dialogVisible = true
|
||
})
|
||
},
|
||
|
||
openEdit (row) {
|
||
this.handleType = 'edit'
|
||
this.dialogTitle = this.key('edit_title')
|
||
this.editId = row.id
|
||
this.formData = { code: row.code, name: row.name, remark: row.remark || '' }
|
||
this.dialogVisible = true
|
||
},
|
||
|
||
async onDialogSubmit () {
|
||
this.submitting = true
|
||
try {
|
||
if (this.handleType === 'create') {
|
||
await createFactoryArea(this.formData)
|
||
} else {
|
||
await editFactoryArea({ ...this.formData, id: this.editId })
|
||
}
|
||
this.$message.success(this.$t(this.key('operation_success')))
|
||
this.dialogVisible = false
|
||
this.fetchData()
|
||
} finally { this.submitting = false }
|
||
},
|
||
|
||
onDialogClose () { this.resetForm() },
|
||
|
||
async handleDelete (row) {
|
||
try {
|
||
await this.$confirm(
|
||
this.$t(this.key('confirm_delete')),
|
||
this.$t(this.key('tip')),
|
||
{ confirmButtonText: this.$t(this.key('confirm')),
|
||
cancelButtonText: this.$t(this.key('cancel')),
|
||
type: 'warning', closeOnClickModal: false }
|
||
)
|
||
await deleteFactoryArea({ id: [row.id] })
|
||
this.$message.success(this.$t(this.key('operation_success')))
|
||
this.pagination.current = Math.min(
|
||
this.pagination.current,
|
||
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
|
||
)
|
||
this.fetchData()
|
||
} catch (e) { /* 取消删除 / 请求失败不处理 */ }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 组件 API 参考
|
||
|
||
### 3.1 `page-table` — 表格 + 按钮栏 + 分页
|
||
|
||
#### Props
|
||
|
||
| Prop | 类型 | 默认值 | 说明 |
|
||
|------|------|--------|------|
|
||
| `columns` | `Array` | `[]` | 列定义,由 `useTableColumns()` 生成 |
|
||
| `data` | `Array` | `[]` | 表格行数据 |
|
||
| `loading` | `Boolean` | `false` | 是否显示 loading 遮罩 |
|
||
| `height` | `String/Number` | — | 表格高度,传 `'auto'` 启用自适应 |
|
||
| `auto-height` | `Boolean` | `false` | 启用高度自适应,填满可用空间 |
|
||
| `border` | `Boolean` | `true` | 是否带边框 |
|
||
| `row-key` | `String` | `'id'` | 行唯一 key |
|
||
| `toolbar-buttons` | `Array` | `[]` | 顶部工具栏按钮,由 `useTableButtons()` 生成 |
|
||
| `row-buttons` | `Array` | `[]` | 行内操作按钮,由 `useTableButtons()` 生成 |
|
||
| `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 |
|
||
|
||
#### 事件
|
||
|
||
| 事件名 | 参数 | 说明 |
|
||
|--------|------|------|
|
||
| `@page-change` | `{ current, size, total }` | 分页变化 |
|
||
| `@selection-change` | `rows: Array` | 选中行变化 |
|
||
|
||
#### 插槽
|
||
|
||
| 插槽名 | 作用域 | 说明 |
|
||
|--------|--------|------|
|
||
| `#col-{prop}` | `{ row, index }` | 自定义列渲染(列需设 `slot: true`) |
|
||
| `#toolbar-extra` | — | 工具栏追加内容 |
|
||
| `#empty` | — | 空数据占位 |
|
||
| `#append` | — | 表格末尾追加 |
|
||
| `#extra` | — | 页面底部追加 |
|
||
|
||
#### 列定义规范
|
||
|
||
```js
|
||
// 普通列
|
||
{ prop: 'code', label: '编码', minWidth: 120 }
|
||
|
||
// 操作列(约定:prop === '_actions')
|
||
{ prop: '_actions', label: '操作', width: 160, fixed: 'right' }
|
||
|
||
// 自定义插槽列(slot: true)
|
||
{ prop: 'status', label: '状态', slot: true, width: 100 }
|
||
|
||
// 复选框 + 序号列(通过 useTableColumns 第二个参数)
|
||
useTableColumns([...], { selectionWidth: 55, indexWidth: 60 })
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 `page-dialog-form` — 增删改查弹框
|
||
|
||
#### Props
|
||
|
||
| Prop | 类型 | 默认值 | 说明 |
|
||
|------|------|--------|------|
|
||
| `visible` | `Boolean` | `false` | 弹框显隐,使用 `.sync` |
|
||
| `title` | `String` | `''` | 弹框标题,支持 i18n key |
|
||
| `width` | `String` | `'35%'` | 弹框宽度 |
|
||
| `form-cols` | `Array` | `[]` | 表单字段结构(二维数组) |
|
||
| `form-data` | `Object` | `{}` | 表单数据对象 |
|
||
| `rules` | `Object` | `{}` | 校验规则,与 `el-form` rules 一致 |
|
||
| `label-width` | `String` | `'100px'` | label 宽度 |
|
||
| `submitting` | `Boolean` | `false` | 提交 loading 状态 |
|
||
| `confirm-text` | `String` | `'确定'` | 确定按钮文字,支持 i18n key |
|
||
| `cancel-text` | `String` | `'取消'` | 取消按钮文字,支持 i18n key |
|
||
|
||
#### 事件
|
||
|
||
| 事件名 | 说明 |
|
||
|--------|------|
|
||
| `@submit` | 表单验证通过后触发 |
|
||
| `@close` | 弹框关闭后触发 |
|
||
|
||
#### 方法(通过 ref 调用)
|
||
|
||
| 方法 | 说明 |
|
||
|------|------|
|
||
| `reset()` | 重置表单 |
|
||
| `validate()` | 手动验证,返回 `Promise<boolean>` |
|
||
|
||
#### formCols 数据结构
|
||
|
||
传入 i18n key(用 `this.key()` 拼接完整 key),组件内部自动翻译且跟随语言切换:
|
||
|
||
```js
|
||
formCols: [
|
||
[
|
||
{ type: 'input', prop: 'code',
|
||
label: this.key('code'), placeholder: this.key('enter_code'),
|
||
clearable: true, style: { width: '90%' } }
|
||
],
|
||
[
|
||
{ type: 'input', prop: '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: this.key('remark'), placeholder: this.key('remark_required'),
|
||
clearable: true, style: { width: '90%' } }
|
||
]
|
||
]
|
||
```
|
||
|
||
**字段支持的属性:**
|
||
|
||
| 属性 | 适用类型 | 说明 |
|
||
|------|---------|------|
|
||
| `type` | 全部 | `'input'`(文本) / `'select'`(下拉) |
|
||
| `prop` | 全部 | 绑定 `formData` 中的 key |
|
||
| `label` | 全部 | 表单项标签,支持 i18n key |
|
||
| `placeholder` | 全部 | 占位提示,支持 i18n key |
|
||
| `inputType` | `input` | `'textarea'` / 不传为普通 text |
|
||
| `autosize` | `input` | textarea 自适应:`{ minRows, maxRows }` |
|
||
| `clearable` | 全部 | 是否可清空,默认 `true` |
|
||
| `style` | 全部 | 样式对象 |
|
||
| `options` | `select` | 选项数组 `[{ label, value }]` |
|
||
| `filterable` | `select` | 是否可搜索,默认 `true` |
|
||
|
||
---
|
||
|
||
## 4. Composable API 参考
|
||
|
||
### 4.1 `useTableColumns(rawColumns, options?)`
|
||
|
||
**作用**:消除手动分配 `idx` 序号,自动识别操作列和插槽列。
|
||
|
||
| 参数 | 类型 | 默认值 | 说明 |
|
||
|------|------|--------|------|
|
||
| `rawColumns` | `Array` | — | 列定义 |
|
||
| `options.selectionWidth` | `number` | `55` | 复选框列宽,传 `0` 隐藏 |
|
||
| `options.indexWidth` | `number` | `0` | 序号列宽,传 `0` 隐藏 |
|
||
|
||
```js
|
||
const columns = useTableColumns([
|
||
{ prop: 'code', label: '编码', width: 120 },
|
||
{ prop: 'name', label: '名称' },
|
||
{ prop: '_actions', label: '操作', width: 160, fixed: 'right' } // 操作列
|
||
])
|
||
```
|
||
|
||
**内部约定**:`prop === '_actions'` 自动标记为 `slot: '_actions'`,由 `page-table` 渲染为操作列。
|
||
|
||
---
|
||
|
||
### 4.2 `useTableButtons(options, permissionCheck?)`
|
||
|
||
**作用**:统一生成工具栏按钮和行内操作按钮,自动注入权限校验结果。
|
||
|
||
| 参数 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `options.toolbar` | `Array` | 顶部按钮列表 |
|
||
| `options.row` | `Array` | 行内按钮列表 |
|
||
| `permissionCheck` | `Function` | 权限函数,通常为 `this.$permission` |
|
||
|
||
**返回值**:`{ toolbarButtons, rowButtons }`
|
||
|
||
**按钮字段:**
|
||
|
||
| 字段 | toolbar | row | 说明 |
|
||
|------|:---:|:---:|------|
|
||
| `key` | √ | √ | 唯一标识 |
|
||
| `label` | √ | √ | 显示文本 |
|
||
| `icon` | √ | √ | Element UI 图标 |
|
||
| `type` | √ | — | `primary`/`success`/`warning`/`danger` |
|
||
| `color` | — | √ | `'danger'` 使文字变红 |
|
||
| `auth` | √ | √ | 权限 key |
|
||
| `cssStyle` | √ | √ | 自定义样式 |
|
||
| `onClick` | √ | √ | 点击回调 |
|
||
| `hasPermission` | √ | √ | **自动注入**,由权限函数计算 |
|
||
| `needSelection` | √ | — | 需选中行才能点击 |
|
||
|
||
```js
|
||
const btns = useTableButtons({
|
||
toolbar: [
|
||
{ key: 'add', label: '新增', icon: 'el-icon-plus', type: 'primary',
|
||
auth: '/xxx/create', onClick: this.openAdd }
|
||
],
|
||
row: [
|
||
{ key: 'edit', label: '编辑', icon: 'el-icon-edit',
|
||
auth: '/xxx/edit', onClick: this.openEdit },
|
||
{ key: 'delete', label: '删除', icon: 'el-icon-delete',
|
||
color: 'danger', auth: '/xxx/delete', onClick: this.handleDelete }
|
||
]
|
||
}, this.$permission)
|
||
```
|
||
|
||
---
|
||
|
||
### 4.3 `i18nMixin(prefix)`
|
||
|
||
**作用**:注入 `key(suffix)` 和 `ckey(suffix)` 两个方法,消除每个页面手动定义常量和 `tkey` 方法。
|
||
|
||
| 方法 | 返回值 | 说明 |
|
||
|------|--------|------|
|
||
| `key(suffix)` | `prefix.suffix` | 当前页面的 i18n key |
|
||
| `ckey(suffix)` | `page.common.suffix` | 公共 i18n key |
|
||
|
||
| 参数 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `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')) 访问当前页面翻译
|
||
// 模板中用 $t(ckey('help')) 访问公共翻译 'page.common.help'
|
||
// JS 中用 this.$t(this.key('code')) / this.$t(this.ckey('help'))
|
||
}
|
||
```
|
||
|
||
**`data()` 中 i18n key 传参技巧**:
|
||
|
||
```js
|
||
data () {
|
||
return {
|
||
// 传入完整 i18n key,组件内部 $t() 翻译,切换语言自动响应
|
||
formCols: [
|
||
[{ label: this.key('code'), placeholder: this.key('enter_code') }]
|
||
],
|
||
rules: {
|
||
code: [{ required: true, message: this.key('enter_code'), trigger: 'blur' }]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
> **原理**:`page-table` / `page-dialog-form` 内部使用 `$t()` 翻译传入的 key。`$t()` 是 Vue I18n 的响应式方法,切换语言时自动返回新的翻译结果,触发组件重新渲染。不要用 `k()` 等辅助函数提前翻译成静态字符串,否则语言切换后不会更新。
|
||
|
||
---
|
||
|
||
## 5. i18n 国际化方案
|
||
|
||
### 5.1 语言包文件
|
||
|
||
| 语言 | 文件路径 |
|
||
|------|---------|
|
||
| 简体中文 | `src/locales/zh-chs.json` |
|
||
| 繁体中文 | `src/locales/zh-cht.json` |
|
||
| 英文 | `src/locales/en.json` |
|
||
| 日文 | `src/locales/ja.json` |
|
||
|
||
### 5.2 语言包结构
|
||
|
||
按 `一级模块 > 二级模块 > 三级模块` 三层嵌套:
|
||
|
||
```json
|
||
{
|
||
"page": {
|
||
"production_master_data": {
|
||
"factory_model": {
|
||
"factory_area": {
|
||
"search": "查询",
|
||
"reset": "重置",
|
||
"code": "所区编码",
|
||
"name": "所区名称",
|
||
"add": "新 增",
|
||
"edit": "编 辑",
|
||
"delete": "删 除",
|
||
"enter_code": "请输入所区编码",
|
||
"enter_name": "请输入所区名称",
|
||
"add_title": "新增所区",
|
||
"edit_title": "编辑所区",
|
||
"confirm": "确定",
|
||
"cancel": "取消",
|
||
"operation_success": "操作成功"
|
||
}
|
||
}
|
||
},
|
||
"common": {
|
||
"help": "帮 助"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.3 组件自动翻译
|
||
|
||
`page-table` 和 `page-dialog-form` 内部使用 `$t()` 翻译:
|
||
|
||
- **列 label** — 自动翻译
|
||
- **按钮 label** — 自动翻译
|
||
- **表单 label / placeholder** — 自动翻译
|
||
- **弹框 title / confirm / cancel** — 自动翻译
|
||
- **验证规则 message** — 自动翻译(`translatedRules` 计算属性)
|
||
|
||
页面只需传入 i18n key,组件渲染时自动替换为当前语言的文本。
|
||
|
||
### 5.4 页面中手动翻译
|
||
|
||
搜索区、提示消息、`$confirm` 等 UI 文字需手动 `$t()`:
|
||
|
||
```vue
|
||
<!-- 模板中:key() 返回 i18n key -->
|
||
<el-form-item :label="$t(key('code'))">
|
||
<el-input :placeholder="$t(key('enter_code'))" />
|
||
</el-form-item>
|
||
<el-button>{{ $t(key('search')) }}</el-button>
|
||
```
|
||
|
||
```js
|
||
// JS 中
|
||
this.$message.success(this.$t(this.key('operation_success')))
|
||
this.$confirm(this.$t(this.key('confirm_delete')), this.$t(this.key('tip')), { ... })
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 常用场景速查
|
||
|
||
### 场景 1:自定义列渲染(状态 tag)
|
||
|
||
列定义加 `slot: true`:
|
||
|
||
```js
|
||
useTableColumns([
|
||
{ prop: 'status', label: '状态', slot: true, width: 100 }
|
||
])
|
||
```
|
||
|
||
模板中用 `#col-status` 插槽:
|
||
|
||
```vue
|
||
<page-table :columns="columns" :data="tableData">
|
||
<template #col-status="{ row }">
|
||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||
</el-tag>
|
||
</template>
|
||
</page-table>
|
||
```
|
||
|
||
### 场景 2:工具栏追加自定义按钮
|
||
|
||
```vue
|
||
<page-table :columns="columns" :toolbar-buttons="toolbarButtons">
|
||
<template #toolbar-extra>
|
||
<el-button size="mini" icon="el-icon-printer" @click="print">打印</el-button>
|
||
</template>
|
||
</page-table>
|
||
```
|
||
|
||
### 场景 3:复选框 + 序号列
|
||
|
||
```js
|
||
useTableColumns(
|
||
[
|
||
{ prop: 'code', label: '编码' },
|
||
{ prop: 'name', label: '名称' }
|
||
],
|
||
{ selectionWidth: 55, indexWidth: 60 }
|
||
)
|
||
```
|
||
|
||
### 场景 4:带下拉选择的表单
|
||
|
||
```js
|
||
formCols: [
|
||
[
|
||
{ type: 'select', prop: 'area_id',
|
||
label: this.key('area'), placeholder: this.key('select_area'),
|
||
options: [{ label: 'A厂区', value: 1 }, { label: 'B厂区', value: 2 }],
|
||
clearable: true, style: { width: '90%' } }
|
||
]
|
||
]
|
||
```
|
||
|
||
### 场景 5:批量删除(工具栏 + 需要选中行)
|
||
|
||
```js
|
||
useTableButtons({
|
||
toolbar: [
|
||
{ key: 'batchDelete', label: '批量删除', icon: 'el-icon-delete',
|
||
type: 'danger', auth: '/xxx/batch-delete',
|
||
needSelection: true, // 需要选中行才能点击
|
||
onClick: this.batchDelete }
|
||
]
|
||
})
|
||
```
|
||
|
||
### 场景 6:表格自适应高度
|
||
|
||
只需加 `auto-height` 属性:
|
||
|
||
```vue
|
||
<page-table ... auto-height />
|
||
```
|
||
|
||
表格高度自动填满 `d2-container` 的 body 区域剩余空间,窗口 resize / 侧栏折叠时自动重算。
|
||
|
||
### 场景 7:帮助按钮
|
||
|
||
传 `help-url` 即可在工具栏最右侧显示问号帮助按钮:
|
||
|
||
```vue
|
||
<page-table ... help-url="/help/factory-area" :help-text="$t(ckey('help'))" />
|
||
```
|
||
|
||
推荐使用公共 key `page.common.help`(模板中用 `$t(ckey('help'))`)。不传 `help-url` 则不显示。
|
||
|
||
---
|
||
|
||
## 7. 路由配置
|
||
|
||
```js
|
||
// src/router/modules/production-master-data.js
|
||
import layoutHeaderAside from '@/layout/header-aside'
|
||
|
||
const meta = { auth: true }
|
||
const _import = require('@/libs/util.import.' + process.env.NODE_ENV)
|
||
|
||
export default {
|
||
path: '/production_master_data',
|
||
component: layoutHeaderAside,
|
||
children: (pre => [
|
||
{
|
||
path: 'factory_model/factory_area',
|
||
name: `${pre}factory_model-factory_area`,
|
||
meta: { ...meta, cache: true, title: '工厂区域' },
|
||
component: _import('production-master-data/factory-model/factory-area')
|
||
}
|
||
])('production_master_data-')
|
||
}
|
||
```
|
||
|
||
之后在 `src/router/routes.js` 中引入该模块并加入 `frameIn` 数组:
|
||
|
||
```js
|
||
import productionConfiguration from './modules/production-master-data'
|
||
|
||
const frameIn = [
|
||
// ... 其他路由 ...
|
||
productionConfiguration
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
## 8. API 文件写法
|
||
|
||
```js
|
||
// src/api/production-master-data/factory-area.js
|
||
import { request } from '@/api/_service'
|
||
|
||
const BASE = 'production_master_data/factory_model/factory_area/'
|
||
|
||
function apiParams (method, data = {}) {
|
||
return {
|
||
method: `production_master_data_factory_model_factory_area_${method}`,
|
||
platform: 'background',
|
||
...data
|
||
}
|
||
}
|
||
|
||
export function getFactoryAreaList (data) {
|
||
return request({
|
||
url: BASE + 'list',
|
||
method: 'get',
|
||
params: apiParams('list', data)
|
||
})
|
||
}
|
||
|
||
export function createFactoryArea (data) {
|
||
return request({
|
||
url: BASE + 'create',
|
||
method: 'post',
|
||
data: apiParams('create', data)
|
||
})
|
||
}
|
||
|
||
export function editFactoryArea (data) {
|
||
return request({
|
||
url: BASE + 'edit',
|
||
method: 'put',
|
||
data: apiParams('edit', data)
|
||
})
|
||
}
|
||
|
||
export function deleteFactoryArea (data) {
|
||
return request({
|
||
url: BASE + 'delete',
|
||
method: 'delete',
|
||
data: apiParams('delete', data)
|
||
})
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 旧代码迁移对照
|
||
|
||
| 旧写法 | 新写法 |
|
||
|--------|--------|
|
||
| 手动构建 `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>` 组件 | 需要时自行添加 |
|
||
| 额外 `<page-footer>` 分页 | `page-table` 内置分页 |
|
||
| 每个页面定义 `T` 常量 + `tkey()` 方法 | `mixins: [i18nMixin(prefix)]`,使用 `this.key('xxx')` |
|
||
| `data()` 中用 `k()` 提前翻译 | 用 `this.key()` 传 key,组件内部 `$t()` 翻译 |
|
||
|
||
---
|
||
|
||
## 10. 接口请求错误处理规范
|
||
|
||
> 错误处理采用 **三层架构**:拦截器全局兜底 → confirmMixin 区分取消/错误 → 页面标准模式。
|
||
|
||
### 10.1 错误处理总流程
|
||
|
||
```
|
||
┌─────────────────────┐
|
||
│ 页面发起 API 请求 │
|
||
└──────────┬──────────┘
|
||
│
|
||
┌──────────▼──────────┐
|
||
│ 是否需 confirm ? │
|
||
└─────┬────────┬──────┘
|
||
│ 否 │ 是
|
||
│ │
|
||
│ ┌────▼───────────┐
|
||
│ │ $confirmAction() │
|
||
│ │ (confirmMixin) │
|
||
│ └────┬──────┬─────┘
|
||
│ 取消 │ │ 确认
|
||
│ ┌────▼──┐ │
|
||
│ │return │ │
|
||
│ │ true │ │
|
||
│ └───────┘ │
|
||
│ │
|
||
┌──────────▼────────────────▼──┐
|
||
│ axios 请求 │
|
||
└──────────┬───────────────────┘
|
||
│
|
||
┌───────────▼────────────┐
|
||
│ HTTP 状态码是什么? │
|
||
└──┬──────────────┬──────┘
|
||
200 │ │ 非 200
|
||
│ │
|
||
┌───────────▼──────┐ ┌───▼─────────────┐
|
||
│ response.data. │ │ HTTP 拦截器 │
|
||
│ code 是什么? │ │ error.message = │
|
||
└──┬───────────┬───┘ │ '未授权/超时/...' │
|
||
0 │ │≠0 └───┬──────────────┘
|
||
│ │ │
|
||
┌────▼──┐ ┌─────▼──────┐ │
|
||
│resolve│ │handleError()│◄─┘
|
||
│ data │ │Message.error│
|
||
└───────┘ └─────┬──────┘
|
||
│
|
||
┌────▼────┐
|
||
│ throw │
|
||
│ error │
|
||
└────┬────┘
|
||
│
|
||
┌─────────▼─────────┐
|
||
│ 页面层 catch 到了吗?│
|
||
└────┬──────────┬───┘
|
||
没写 │ │ 写了
|
||
│ │
|
||
┌─────────▼──┐ ┌────▼─────────────┐
|
||
│ 什么都不发生 │ │ finally 关锁 │
|
||
│(依赖拦截器 │ │ loading/submitting│
|
||
│ Message) │ │ = false │
|
||
└────────────┘ └──────────────────┘
|
||
```
|
||
|
||
### 10.2 三层架构详图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ 第一层:全局拦截器 │
|
||
│ src/api/_service.js │
|
||
│ │
|
||
│ 拦截所有 axios 响应,统一处理两类错误: │
|
||
│ │
|
||
│ HTTP 错误 (4xx/5xx) │
|
||
│ ├─ 400 → error.message = '请求错误' │
|
||
│ ├─ 401 → error.message = '未授权,请登录' │
|
||
│ ├─ 403 → error.message = '拒绝访问' │
|
||
│ ├─ 404 → error.message = '请求地址出错' │
|
||
│ ├─ 408 → error.message = '请求超时' │
|
||
│ ├─ 500 → error.message = '服务器内部错误' │
|
||
│ ├─ 502 → error.message = '网关错误' │
|
||
│ ├─ ... 其它状态码 │
|
||
│ │ │
|
||
│ └─ handleError(error) │
|
||
│ ├─ store.dispatch('d2admin/log/push', ...) │
|
||
│ ├─ console.log(error) (dev only) │
|
||
│ └─ Message.error(error.message) ← 弹红色提示 │
|
||
│ │
|
||
│ 业务错误 (code ≠ 0, 如 code=500 "参数错误") │
|
||
│ ├─ 取出 response.data.msg 或 '请求失败' │
|
||
│ ├─ const err = new Error(`${msg}: ${url}`) │
|
||
│ ├─ handleError(err) │
|
||
│ └─ throw err ← 继续向上抛给页面层 │
|
||
└──────────────────────┬──────────────────────────┘
|
||
│
|
||
┌──────────────────────▼──────────────────────────┐
|
||
│ 第二层:confirmMixin │
|
||
│ src/composables/useConfirmHandle.js │
|
||
│ │
|
||
│ $confirmAction(confirmOpts, action) │
|
||
│ ├─ 第一层 try/catch │
|
||
│ │ await this.$confirm(...) │
|
||
│ │ ├─ 用户点"确定" → 继续 │
|
||
│ │ └─ 用户点"取消" → catch → return true │
|
||
│ │ │
|
||
│ └─ 第二层 try/catch │
|
||
│ await action() ← 执行 API 调用 │
|
||
│ ├─ 成功 → return false │
|
||
│ └─ 失败 → catch → return false │
|
||
│ (拦截器已弹出 Message,此处静默吞掉) │
|
||
└──────────────────────┬──────────────────────────┘
|
||
│
|
||
┌──────────────────────▼──────────────────────────┐
|
||
│ 第三层:页面标准模式 │
|
||
│ │
|
||
│ 场景A — 查询 (fetchData) │
|
||
│ ┌─────────────────────────────────────────────┐ │
|
||
│ │ async fetchData () { │ │
|
||
│ │ this.loading = true │ │
|
||
│ │ try { │ │
|
||
│ │ const res = await getList(...) │ │
|
||
│ │ this.tableData = res.data │ │
|
||
│ │ } finally { this.loading = false } │ │
|
||
│ │ } │ │
|
||
│ └─────────────────────────────────────────────┘ │
|
||
│ • try 内无 catch:拦截器已弹错误提示 │
|
||
│ • finally 始终执行:loading 无论成败都关闭 │
|
||
│ │
|
||
│ 场景B — 提交 (onDialogSubmit) │
|
||
│ ┌─────────────────────────────────────────────┐ │
|
||
│ │ async onDialogSubmit () { │ │
|
||
│ │ this.submitting = true │ │
|
||
│ │ try { │ │
|
||
│ │ await createApi(this.formData) │ │
|
||
│ │ this.$message.success(...) ← 成功才执行 │ │
|
||
│ │ this.dialogVisible = false │ │
|
||
│ │ this.fetchData() │ │
|
||
│ │ } finally { this.submitting = false } │ │
|
||
│ │ } │ │
|
||
│ └─────────────────────────────────────────────┘ │
|
||
│ • 出错时弹窗不关,用户可修改后重试 │
|
||
│ │
|
||
│ 场景C — 删除 (handleDelete) │
|
||
│ ┌─────────────────────────────────────────────┐ │
|
||
│ │ async handleDelete (row) { │ │
|
||
│ │ const cancelled = await this.$confirmAction(│ │
|
||
│ │ { message: this.key('confirm_delete'), │ │
|
||
│ │ title: this.key('tip') }, │ │
|
||
│ │ () => deleteApi({ id: row.id }) │ │
|
||
│ │ ) │ │
|
||
│ │ if (cancelled) return ← 用户取消,静默 │ │
|
||
│ │ this.$message.success(...) ← 成功才执行 │ │
|
||
│ │ this.fetchData() │ │
|
||
│ │ } │ │
|
||
│ └─────────────────────────────────────────────┘ │
|
||
│ • $confirmAction 内已处理错误 Message,不额外 catch│
|
||
│ • 没有 try/catch 包裹 → 代码更简洁直观 │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 10.3 关键原则
|
||
|
||
| 原则 | 说明 |
|
||
|------|------|
|
||
| **拦截器兜底** | 所有 API 错误(HTTP + 业务)统一在 `_service.js` 中弹出 `Message.error` |
|
||
| **finally 关锁** | `loading` / `submitting` 用 `try { ... } finally { lock = false }`,不论成败都关闭 |
|
||
| **只用 catch 不动** | 成功后的 `$message.success` / `dialogVisible = false` / `fetchData` 不放在 `finally`,只有成功才执行 |
|
||
| **$confirmAction 包装** | 删除/批量操作类的 confirm + API 用 `$confirmAction()` 区分取消和错误 |
|
||
| **不吞业务错误** | `fetchData` / `onDialogSubmit` 的 try 只写 `finally`,不写 `catch`,错误由拦截器处理 |
|
||
|
||
### 10.4 confirmMixin 使用
|
||
|
||
页面中引入:
|
||
|
||
```js
|
||
import { confirmMixin } from '@/composables/useConfirmHandle'
|
||
|
||
export default {
|
||
mixins: [
|
||
i18nMixin('page.xxx.xxx'),
|
||
confirmMixin // ← 新增
|
||
]
|
||
}
|
||
```
|
||
|
||
API:
|
||
|
||
```js
|
||
// $confirmAction(confirmOpts, actionFn) → Promise<boolean>
|
||
// 返回 true = 用户点了取消
|
||
// 返回 false = action 已执行完成(无论成功失败)
|
||
|
||
const cancelled = await this.$confirmAction(
|
||
{
|
||
message: this.key('confirm_delete'), // confirm 正文
|
||
title: this.key('tip'), // confirm 标题
|
||
// type: 'warning', // 可选,默认 'warning'
|
||
// confirmButtonText / cancelButtonText 可选,默认用 page.common 的
|
||
},
|
||
() => deleteApi({ id: row.id }) // 确认后执行的异步函数
|
||
)
|
||
if (cancelled) return
|
||
// 以下只在成功时执行
|
||
this.$message.success(...)
|
||
this.fetchData()
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 常见问题排查
|
||
|
||
### Q1:弹框打开后不显示内容?
|
||
|
||
确认 `formCols` 中的 `label` 和 `placeholder` 是否传入了正确的 i18n key。应该用 `this.key()` 拼接完整 key,不要用 `k()` 提前翻译成静态字符串。
|
||
|
||
### Q2:表格高度自适应不生效?
|
||
|
||
确认 `d2-container` 的 body 区域高度被正确约束(flex 填充)。给 `d2-container` 的 body 部分设置 `overflow: hidden` 通常可解决。
|
||
|
||
### Q3:权限校验有按钮不显示?
|
||
|
||
`useTableButtons` 的第二个参数必须传入 `this.$permission`。如果项目没有注册这个全局方法,可以传入 `() => true` 占位。
|
||
|
||
### Q4:新增一条后页码不跳回第一页?
|
||
|
||
在 `onDialogSubmit` 成功后调用 `this.fetchData()` 前,如果删除当前页最后一行导致 `total - 1` 超出范围,需手动修正页码。
|
||
|
||
### Q5:切换语言后弹框/表格内容不更新?
|
||
|
||
检查 `data()` 中是否使用了 `k()` 等辅助函数提前翻译。应该用 `this.key()` 传 i18n key,由组件内部 `$t()` 处理翻译,这样切换语言时才能自动响应。
|
||
|
||
### Q6:表单验证错误提示显示为原始 i18n key?
|
||
|
||
`page-dialog-form` 会通过 `translatedRules` 计算属性自动翻译验证规则的 `message` 字段。确认传入的 `rules.message` 使用了 `this.key()` 传入完整 key。
|