1. 新增角色管理后台页面、路由与国际化文案 2. 重构API请求错误处理逻辑,统一拦截业务与HTTP错误 3. 新增确认弹窗组合式函数,区分取消与请求错误场景 4. 完善表格按钮权限与显示控制逻辑 5. 更新API参数规范与文档说明 6. 修复部分页面分页数据解析问题
42 KiB
表格组件使用说明
基于
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
目录
- 快速开始(三步走)
- 完整示例代码
- 组件 API 参考
- Composable API 参考
- i18n 国际化方案
- 常用场景速查
- 路由配置
- API 文件写法
- 旧代码迁移对照
- 接口请求错误处理规范
- 常见问题排查
1. 快速开始(三步走)
第一步:定义列和按钮(created() 中)
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> 中)
<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>
第三步:写业务方法(增删改查)
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 页面,包含搜索栏、表格、分页、新增/编辑弹框、删除确认。
模板部分
<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 部分
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 |
— | 页面底部追加 |
列定义规范
// 普通列
{ 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),组件内部自动翻译且跟随语言切换:
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 隐藏 |
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 |
√ | — | 需选中行才能点击 |
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 前缀 |
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 传参技巧:
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 语言包结构
按 一级模块 > 二级模块 > 三级模块 三层嵌套:
{
"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():
<!-- 模板中: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 中
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:
useTableColumns([
{ prop: 'status', label: '状态', slot: true, width: 100 }
])
模板中用 #col-status 插槽:
<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:工具栏追加自定义按钮
<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:复选框 + 序号列
useTableColumns(
[
{ prop: 'code', label: '编码' },
{ prop: 'name', label: '名称' }
],
{ selectionWidth: 55, indexWidth: 60 }
)
场景 4:带下拉选择的表单
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:批量删除(工具栏 + 需要选中行)
useTableButtons({
toolbar: [
{ key: 'batchDelete', label: '批量删除', icon: 'el-icon-delete',
type: 'danger', auth: '/xxx/batch-delete',
needSelection: true, // 需要选中行才能点击
onClick: this.batchDelete }
]
})
场景 6:表格自适应高度
只需加 auto-height 属性:
<page-table ... auto-height />
表格高度自动填满 d2-container 的 body 区域剩余空间,窗口 resize / 侧栏折叠时自动重算。
场景 7:帮助按钮
传 help-url 即可在工具栏最右侧显示问号帮助按钮:
<page-table ... help-url="/help/factory-area" :help-text="$t(ckey('help'))" />
推荐使用公共 key page.common.help(模板中用 $t(ckey('help')))。不传 help-url 则不显示。
7. 路由配置
// 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 数组:
import productionConfiguration from './modules/production-master-data'
const frameIn = [
// ... 其他路由 ...
productionConfiguration
]
8. API 文件写法
// 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 使用
页面中引入:
import { confirmMixin } from '@/composables/useConfirmHandle'
export default {
mixins: [
i18nMixin('page.xxx.xxx'),
confirmMixin // ← 新增
]
}
API:
// $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。