Files
mes-ui-d2/docs/表格组件使用说明.md

954 lines
30 KiB
Markdown
Raw Normal View History

# 表格组件使用说明
> 基于 `page-table` + `page-dialog-form` 的新一<E696B0><E4B880>?CRUD 表格方案<E696B9><E6A188>?
> 源码位置:`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-参<><E58F82>?
4. [Composable API 参考](#4-composable-api-参<><E58F82>?
5. [i18n 国际化方案](#5-i18n-国际化方<E58C96><E696B9>?
6. [常用场景速查](#6-常用场景速查)
7. [路由配置](#7-路由配置)
8. [API 文件写法](#8-api-文件写法)
9. [旧代码迁移对照](#9-旧代码迁移对<E7A7BB><E5AFB9>?
10. [常见问题排查](#10-常见问题排查)
---
## 1. 快速开始(三步走)
### 第一步:定义列和按钮(`created()` 中)
```js
import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
import { i18nMixin } from '@/composables/useI18n'
export default {
mixins: [i18nMixin('page.模块<E6A8A1><E59D97>?二级模块.三级模块')],
created () {
// --- 列定<E58897><E5AE9A>?---
// 只需声明 prop <20><>?labelidx 自动补齐
// prop: '_actions' 约定为操作列自动渲<E58AA8><E6B8B2>?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' }
])
// --- 按钮定义 ---
// 不再分开<E58886><E5BC80>?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) // <20><>?第二个参数传入权限校验函<E9AA8C><E587BD>?
this.toolbarButtons = btns.toolbarButtons
this.rowButtons = btns.rowButtons
}
}
```
### 第二步:写模板(`<template>` 中)
```vue
<template>
<d2-container>
<!-- 搜索<E6909C><E7B4A2>?-->
<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>
<!-- 表格 + 按钮<E68C89><E992AE>?+ 分页 -->
<page-table
:columns="columns"
:data="tableData"
:loading="loading"
:toolbar-buttons="toolbarButtons"
:row-buttons="rowButtons"
:pagination="pagination"
help-url="/help/your-page"
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="$t(key('confirm'))"
:cancel-text="$t(key('cancel'))"
@submit="onDialogSubmit"
@close="onDialogClose"
/>
</d2-container>
</template>
```
### 第三步写业务方法增删改查<E694B9><E69FA5>?
```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
})
},
// 编辑回填表<E5A1AB><E8A1A8>? 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) { /* 取消或失<E68896><E5A4B1>?*/ }
}
}
```
---
## 2. 完整示例代码
> 📁 `src/views/production-master-data/factory-model/factory-area/index.vue`
> 这是一<EFBFBD><EFBFBD>?*可直接运行的完整 CRUD 页面**包含搜索栏、表格、分页、新<E38081><E696B0>?编辑弹框、删除确认<E7A1AE><E8AEA4>?
### 模板部分
```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"
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="$t(key('confirm'))"
:cancel-text="$t(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 () {
const t = this.$t.bind(this)
const k = (s) => t(this.key(s))
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: k('enter_code'), trigger: 'blur' },
{ min: 1, max: 100, message: k('remark_length'), trigger: 'blur' }
],
name: [
{ required: true, message: k('enter_name'), trigger: 'blur' },
{ min: 1, max: 100, message: k('remark_length'), trigger: 'blur' }
]
},
columns: [],
toolbarButtons: [],
rowButtons: [],
formCols: [
[
{ type: 'input', prop: 'code',
label: k('code'), placeholder: k('enter_code'),
clearable: true, style: { width: '90%' } }
],
[
{ type: 'input', prop: 'name',
label: k('name'), placeholder: k('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'),
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) { /* 取消删除 / 请求失败不处<E4B88D><E5A484>?*/ }
}
}
}
```
---
## 3. 组件 API 参<><E58F82>?
### 3.1 `page-table` <20><>?表格 + 按钮<E68C89><E992AE>?+ 分页
#### Props
| Prop | 类型 | 默认<E9BB98><E8AEA4>?| 说明 |
|------|------|--------|------|
| `columns` | `Array` | `[]` | 列定义<E4B989><EFBC8C>?`useTableColumns()` 生成 |
| `data` | `Array` | `[]` | 表格行数<E8A18C><E695B0>?|
| `loading` | `Boolean` | `false` | 是否显示 loading 遮罩 |
| `height` | `String/Number` | <20><>?| 表格高度。不传则随内容撑开传具体数值固定高度<E5BAA6><EFBC9B>?`'auto'` 启用自适应 |
| `auto-height` | `Boolean` | `false` | 启用高度自适应表格自动填满可用空<E794A8><E7A9BA>?|
| `border` | `Boolean` | `true` | 是否带边<E5B8A6><E8BEB9>?|
| `row-key` | `String` | `'id'` | 行唯一 key |
| `toolbar-buttons` | `Array` | `[]` | 顶部工具栏按钮<E992AE><EFBC8C>?`useTableButtons()` 生成 |
| `row-buttons` | `Array` | `[]` | 行内操作按钮,由 `useTableButtons()` 生成 |
| `pagination` | `Object` | `null` | 分页参数 `{ current, size, total }`,传了才显示分页 |
| `table-attrs` | `Object` | `{}` | 额外透传<E9808F><E4BCA0>?`el-table` 的属<E79A84><E5B19E>?|
| `table-listeners` | `Object` | `{}` | 额外透传<E9808F><E4BCA0>?`el-table` 的事<E79A84><E4BA8B>?|
| `help-url` | `String` | `''` | 帮助文档跳转 URL。传了才显示工具栏右侧的问号按钮点击新窗口打开 |
| `help-text` | `String` | `'帮助'` | 帮助按钮文字<EFBC8C><E694AF>?i18n key组件自<E4BBB6><E887AA>?`$t()` 翻译<E7BFBB><E8AF91>?|
#### 事件
| 事件<E4BA8B><E4BBB6>?| 参数 | 说明 |
|--------|------|------|
| `@page-change` | `{ current, size, total }` | 分页变化切换页<E68DA2><E9A1B5>?条数<E69DA1><E695B0>?|
| `@selection-change` | `rows: Array` | 选中行变<E8A18C><E58F98>?|
| `@sort-change` | 透传 el-table 原生事件 | 排序变化 |
#### 插槽
| 插槽<E68F92><E6A7BD>?| 作用<E4BD9C><E794A8>?| 说明 |
|--------|--------|------|
| `#col-{prop}` | `{ row, index }` | 自定义列的渲染(列定义中 prop 需<><E99C80>?`slot: true`<EFBFBD><EFBFBD>?|
| `#toolbar-extra` | <20><>?| 工具栏区域追加自定义内容 |
| `#empty` | <20><>?| 表格空数据时的占<E79A84><E58DA0>?|
| `#append` | <20><>?| 表格最后一行后追加 |
| `#extra` | <20><>?| 页面底部追加区域 |
#### 列定义规<E4B989><E8A784>?
```js
// 普通列
{ prop: 'code', label: '编码', minWidth: 120 }
// 操作列约定prop === '_actions'<27><>?{ prop: '_actions', label: '操作', width: 160, fixed: 'right' }
// 自定义插槽列slot: true<75><65>?{ prop: 'status', label: '状<><E78AB6>?, slot: true, width: 100 }
// 复选框<E98089><E6A186>?+ 序号列(通过 useTableColumns 第二个参数)
useTableColumns([...], { selectionWidth: 55, indexWidth: 60 })
```
---
### 3.2 `page-dialog-form` <20><>?增删改查弹框
#### Props
| Prop | 类型 | 默认<E9BB98><E8AEA4>?| 说明 |
|------|------|--------|------|
| `visible` | `Boolean` | `false` | 弹框显隐使<EFBC8C><E4BDBF>?`.sync` 修饰<E4BFAE><E9A5B0>?|
| `title` | `String` | `''` | 弹框标题<EFBC8C><E694AF>?i18n key |
| `width` | `String` | `'35%'` | 弹框宽度 |
| `form-cols` | `Array` | `[]` | 表单字段结构(二维数组,见下方) |
| `form-data` | `Object` | `{}` | 表单数据对象 |
| `rules` | `Object` | `{}` | 校验规则,与 `el-form` rules 一<><E4B880>?|
| `label-width` | `String` | `'100px'` | label 宽度 |
| `submitting` | `Boolean` | `false` | 提交 loading 状<><E78AB6>?|
| `confirm-text` | `String` | `'确定'` | 确定按钮文字 |
| `cancel-text` | `String` | `'取消'` | 取消按钮文字 |
#### 事件
| 事件<E4BA8B><E4BBB6>?| 说明 |
|--------|------|
| `@submit` | 表单验证通过后触发,父组件执行提交逻辑 |
| `@close` | 弹框关闭后触<E5908E><E8A7A6>?|
#### 方法(通过 ref 调用<E8B083><E794A8>?
| 方法 | 说明 |
|------|------|
| `reset()` | 重置表单 |
| `validate()` | 手动验证<EFBC8C><E8BF94>?`Promise<boolean>` |
#### formCols 数据结构
**注意<EFBC9A><E99C80>?`data()` 中用 `k(prop)` 提前完成 i18n 翻译**,不要传 raw key<65><79>?
```js
// 例:两个普通输入框 + 一个多行文本框
formCols: [
[
{ type: 'input', prop: 'code',
label: k('code'), placeholder: k('enter_code'),
clearable: true, style: { width: '90%' } }
],
[
{ type: 'input', prop: 'name',
label: k('name'), placeholder: k('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'),
clearable: true, style: { width: '90%' } }
]
]
```
**字段支持的属性:**
| 属<><E5B19E>?| 适用类型 | 说明 |
|------|---------|------|
| `type` | 全部 | `'input'`(文本输入)/ `'select'`(下拉) |
| `prop` | 全部 | 绑定 `formData` 中的 key |
| `label` | 全部 | 表单项标<E9A1B9><E6A087>?|
| `placeholder` | 全部 | 占位提示 |
| `inputType` | `input` | `'textarea'` 多行 / 不传为普<E4B8BA><E699AE>?text |
| `autosize` | `input` | textarea <20><>?`{ minRows, maxRows }` |
| `clearable` | 全部 | 是否可清空,默认 `true` |
| `style` | 全部 | 样式对象 |
| `options` | `select` | 选项数组 `[{ label, value }]` |
| `filterable` | `select` | 是否可搜索,默认 `true` |
---
## 4. Composable API 参<><E58F82>?
### 4.1 `useTableColumns(rawColumns, options?)`
**作用**消除手动分<E58AA8><E58886>?`idx` 序号自动识别操作列和插槽列<E6A7BD><E58897>?
| 参数 | 类型 | 默认<E9BB98><E8AEA4>?| 说明 |
|------|------|--------|------|
| `rawColumns` | `Array` | <20><>?| 列定<E58897><E5AE9A>?|
| `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' } // <20><>?操作<E6938D><E4BD9C>?])
```
**内部约定**`prop === '_actions'` 自动标记<E6A087><E8AEB0>?`slot: '_actions'`,由 `page-table` 渲染为操作列<E4BD9C><E58897>?
---
### 4.2 `useTableButtons(options, permissionCheck?)`
**作用**统一生成工具栏按钮和行内操作按钮自动注入权限校验结果<E7BB93><E69E9C>?
| 参数 | 类型 | 说明 |
|------|------|------|
| `options.toolbar` | `Array` | 顶部按钮列表 |
| `options.row` | `Array` | 行内按钮列表 |
| `permissionCheck` | `Function` | 权限函数通常<E9809A><E5B8B8>?`this.$permission` |
**返回<E8BF94><E59B9E>?*`{ toolbarButtons, rowButtons }`
**按钮字段<E5AD97><E6AEB5>?*
| 字段 | toolbar | row | 说明 |
|------|:---:|:---:|------|
| `key` | <20><>?| <20><>?| 唯一标识 |
| `label` | <20><>?| <20><>?| 显示文本 |
| `icon` | <20><>?| <20><>?| Element UI 图标 |
| `type` | <20><>?| <20><>?| `primary`/`success`/`warning`/`danger` |
| `color` | <20><>?| <20><>?| `'danger'` 使文字变<E5AD97><E58F98>?|
| `auth` | <20><>?| <20><>?| 权限 key |
| `cssStyle` | <20><>?| <20><>?| 自定义样<E4B989><E6A0B7>?|
| `onClick` | <20><>?| <20><>?| 点击回调 |
| `hasPermission` | <20><>?| <20><>?| **自动注入**,由权限函数计算 |
| `needSelection` | <20><>?| <20><>?| 需选中行才能点<E883BD><E782B9>?|
```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)`
**作用**<EFBC9A><E6B3A8>?`key(suffix)` <20><>?`ckey(suffix)` 两个方法消除每个页面手<E99DA2><E6898B>?`T` 常量<E5B8B8><E9878F>?`tkey` 方法<E696B9><E6B395>?
| 方法 | 返回<E8BF94><E59B9E>?| 说明 |
|------|--------|------|
| `key(suffix)` | `prefix.suffix` | 当前页面<E9A1B5><E99DA2>?i18n key |
| `ckey(suffix)` | `page.common.suffix` | 公共 i18n key如"帮助"<22><>?确定"<22><>?取消"<22><>?|
| 参数 | 类型 | 说明 |
|------|------|------|
| `prefix` | `String` | 该页面的 i18n key 前缀,如 `'page.production_master_data.factory_model.factory_area'` |
```js
import { i18nMixin } from '@/composables/useI18n'
export default {
mixins: [i18nMixin('page.production_master_data.factory_model.factory_area')],
// 此后模板中用 $t(key('code')) 访问当前页面的翻<E79A84><E7BFBB>? // <20><>?$t(ckey('help')) 访问公共翻译 'page.common.help'
// JS 中用 this.$t(this.key('code')) / this.$t(this.ckey('help'))
}
```
**`data()` 中翻译文本的技<E79A84><E68A80>?*<2A><>?
```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 {
formCols: [
[{ label: k('code'), placeholder: k('enter_code') }] // 页面<E9A1B5><E99DA2>?key
],
helpText: ck('help') // 公共 key
}
}
```
---
## 5. i18n 国际化方<E58C96><E696B9>?
### 5.1 语言包文<E58C85><E69687>?
| 语言 | 文件路径 |
|------|---------|
| 简体中<E4BD93><E4B8AD>?| `src/locales/zh-chs.json` |
| 繁体中文 | `src/locales/zh-cht.json` |
| 英文 | `src/locales/en.json` |
| 日文 | `src/locales/ja.json` |
### 5.2 语言包结<E58C85><E7BB93>?
<EFBFBD><EFBFBD>?`一级模<E7BAA7><E6A8A1>?<3F><>?二级模块 <20><>?三级模块` 三层嵌套<E5B58C><E5A597>?
```json
{
"page": {
"production_master_data": {
"factory_model": {
"factory_area": {
"search": "查询",
"reset": "重置",
"code": "所区编<E58CBA><E7BC96>?,
"name": "所区名<E58CBA><E5908D>?,
"add": "<22><>?<3F><>?,
"edit": "<22><>?<3F><>?,
"delete": "<22><>?<3F><>?,
"enter_code": "请输入所区编<E58CBA><E7BC96>?,
"enter_name": "请输入所区名<E58CBA><E5908D>?,
"add_title": "新增所<E5A29E><E68980>?,
"edit_title": "编辑所<E8BE91><E68980>?,
"confirm": "确定",
"cancel": "取消",
"operation_success": "操作成功"
}
}
}
}
}
```
### 5.3 组件自动翻译
`page-table` <20><>?`page-dialog-form` 内置 `$t()`<EFBFBD><EFBFBD>?
- **<2A><>?label** <20><>?自动翻译
- **按钮 label** <20><>?自动翻译
- **表单 label / placeholder** <20><>?自动翻译
- **弹框 title / confirm / cancel** <20><>?自动翻译
因此页面只需传入 i18n key 字符串组件渲染时自动替换为当前语言的文本<E69687><E69CAC>?
### 5.4 页面中手动翻<E58AA8><E7BFBB>?
搜索区、校验消息、`$confirm` <20><>?UI 文字需手动<E6898B><E58AA8>?`$t()`<EFBFBD><EFBFBD>?
```vue
<!-- 模板<E6A8A1><E69DBF>?<3F><>?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 <20><>?this.$message.success(this.$t(this.key('operation_success')))
this.$confirm(this.$t(this.key('confirm_delete')), this.$t(this.key('tip')), { ... })
```
---
## 6. 常用场景速查
### 场景 1自定义列渲染<EFBC88><E78AB6>?tag<61><67>?
列定义加<EFBFBD><EFBFBD>?`slot: true`<EFBFBD><EFBFBD>?
```js
useTableColumns([
{ prop: 'status', label: '状<><E78AB6>?, slot: true, width: 100 }
])
```
模板中用 `#col-status` 插槽<E68F92><E6A7BD>?
```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工具栏追加自定义按<E4B989><E68C89>?
```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复选框<E98089><E6A186>?+ 序号<E5BA8F><E58FB7>?
```js
useTableColumns(
[
{ prop: 'code', label: '编码' },
{ prop: 'name', label: '名称' }
],
{ selectionWidth: 55, indexWidth: 60 }
)
```
### 场景 4带下拉选择的表<E79A84><E8A1A8>?
```js
formCols: [
[
{ type: 'select', prop: 'area_id',
label: k('area'), placeholder: k('select_area'),
options: [{ label: 'A厂区', value: 1 }, { label: 'B厂区', value: 2 }],
clearable: true, style: { width: '90%' } }
]
]
```
### 场景 5批量删除工具<E5B7A5><E585B7>?+ 需要选中行)
```js
useTableButtons({
toolbar: [
{ key: 'batchDelete', label: '批量删除', icon: 'el-icon-delete',
type: 'danger', auth: '/xxx/batch-delete',
needSelection: true, // <20><>?需要选中行才能点<E883BD><E782B9>? onClick: this.batchDelete }
]
})
```
### 场景 6表格自适应高度
只需<EFBFBD><EFBFBD>?`auto-height` 属性:
```vue
<page-table ... auto-height />
```
表格高度会自动填<EFBFBD><EFBFBD>?`d2-container` <20><>?body 区域剩余空间<EFBC8C><E7AA97>?resize / 侧栏折叠时自动重算<E9878D><E7AE97>?
### 场景 7帮助按<E58AA9><E68C89>?
<EFBFBD><EFBFBD>?`help-url` 传值即可在工具栏最右侧显示问号帮助按钮点击在新窗口打开帮助文档<E69687><E6A1A3>?
```vue
<page-table ... help-url="/help/factory-area" :help-text="$t(ckey('help'))" />
```
按钮文字通过 `help-text` prop 支持 i18n。推荐使用公<E794A8><E585AC>?key `page.common.help`模板中<EFBFBD><EFBFBD>?`$t(ckey('help'))`所有页面共享无需每个模块重复定义<EFBFBD><EFBFBD>?
不传 `help-url` 或传空字符串则不显示帮助按钮<E68C89><E992AE>?
---
## 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-')
}
```
之后<EFBFBD><EFBFBD>?`src/router/routes.js` 中引入该模块并加<E5B9B6><E58AA0>?`frameIn` 数组<E695B0><E7BB84>?
```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. 旧代码迁移对<E7A7BB><E5AFB9>?
| 旧写<E697A7><E58699>?| 新写<E696B0><E58699>?|
|--------|--------|
| 手动构建 `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>` 两层 |
| `<sct-form-search>` 组件 | 原生 `<el-form :inline>` |
| `<sct-back-to-top>` 组件 | 需要时自行添加 |
| 分页需要额<E8A681><E9A29D>?`<page-footer>` | `page-table` 内置分页 |
| 每个页面定义 `T` 常量 + `tkey()` 方法 | `mixins: [i18nMixin(prefix)]`使<EFBFBD><EFBFBD>?`this.key('xxx')` |
| 表单字段<E5AD97><E6AEB5>?i18n raw key子组件翻译 | `data()` 中用 `k(prop)` 提前翻译 |
---
## 10. 常见问题排查
### Q1弹框打开后不显示内容<E58685><E5AEB9>?
确认 `formCols` 中的 `label` <20><>?`placeholder` 是否<E698AF><E590A6>?`data()` 阶段已翻译。子组件 `page-dialog-form` 会调<E4BC9A><E8B083>?`$t()` 处理 label但建议<E5BBBA><E8AEAE>?`data()` 阶段就用 `k()` 提前翻译好,避免 webpack HMR 缓存问题<E997AE><E9A298>?
### Q2表格高度自适应不生效
确认 `d2-container` <20><>?body 区域高度被正确约束flex 填充。如<E38082><E5A682>?`d2-container` 本身<E69CAC><E8BAAB>?`overflow: auto` 或其他高度约束问题,`page-table` <20><>?`height: 100%` 会失效。给 `d2-container` <20><>?body 部分<E983A8><E58886>?`overflow: hidden` 通常可解决<E8A7A3><E586B3>?
### Q3权限校验有按钮不显示
`useTableButtons` 的第二个参数必须<E5BF85><E9A1BB>?`this.$permission`。如果项目没有注册这个全局方法,可以在 `useTableButtons` 中传入一个返<E4B8AA><E8BF94>?`true` 的占位函数:`() => true`<EFBFBD><EFBFBD>?
### Q4新增一条后页码不跳回第一页
<EFBFBD><EFBFBD>?`onDialogSubmit` 成功后调<E5908E><E8B083>?`this.fetchData()`如果删除了当前页最后一行导<E8A18C><E5AFBC>?`total - 1` 超出范围需手动修正页码<E9A1B5><E7A081>?
```js
this.pagination.current = Math.min(
this.pagination.current,
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
)
```
### Q5表单验证不提示错误文字<E69687><E5AD97>?
确认 `page-dialog-form` <20><>?`<style>` 块存在(默认 22px `margin-bottom` + `position: absolute` 错误文字)。如果表单项之间被其他样式覆盖,检查是否有全局 CSS 重置<E9878D><E7BDAE>?`.el-form-item` <20><>?margin<69><6E>?
### Q6如何新增一<E5A29E><E4B880>?CRUD 页面最快?
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. 添加路由配置
平均 10 分钟即可完成一个标<E4B8AA><E6A087>?CRUD 页面<E9A1B5><E99DA2>?