Files
mes-ui-d2/docs/表格组件使用说明.md
sheng 3eaea3116d
Some checks failed
Release pipeline / publish (push) Has been cancelled
Release pipeline / Always run job (push) Has been cancelled
feat: 新增工厂区域管理页面,修复Sass废弃警告
1. 新增生产配置-工厂模型-工厂区域完整CRUD页面
2. 新增通用表格、弹窗表单、i18n工具组件
3. 升级sass-loader并修复Sass废弃警告
4. 添加文档记录Sass迁移修复细节
2026-05-26 18:32:57 +08:00

954 lines
30 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 表格组件使用说明
> 基于 `page-table` + `page-dialog-form` 的新一�CRUD 表格方案�
> æº<EFBFBD>ç <EFBFBD>ä½<EFBFBD>置:`src/components/page-table/`ã€<C3A3>`src/components/page-dialog-form/`ã€<C3A3>`src/composables/`
> 完整å<EFBFBD>¯è¿<EFBFBD>行示ä¾ï¼š`src/views/production-master-data/factory-model/factory-area/index.vue`
---
## 目录
1. [快速开始(三步走)](#1-快速开始三步走)
2. [完整示ä¾ä»£ç <EFBFBD>](#2-完整示ä¾ä»£ç <C3A7>)
3. [组件 API å<>考](#3-组件-api-å<>è€?
4. [Composable API å<>考](#4-composable-api-å<>è€?
5. [i18n 国际化方案](#5-i18n-国际化方�
6. [常用场景速查](#6-常用场景速查)
7. [路由é…<EFBFBD>ç½®](#7-路由é…<C3A9>ç½®)
8. [API 文件写法](#8-api-文件写法)
9. [旧代ç <C3A7>è¿<C3A8>移对照](#9-旧代ç <C3A7>è¿<C3A8>移对ç…?
10. [常è§<EFBFBD>é—®é¢˜æŽæŸ¥](#10-常è§<C3A8>é—®é¢˜æŽæŸ¥)
---
## 1. 快速开始(三步走)
### 第一步:定义列和按钮(`created()` 中)
```js
import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
import { i18nMixin } from '@/composables/useI18n'
export default {
mixins: [i18nMixin('page.模å<C2A1>—å<E28094>?二级模å<C2A1>—.三级模å<C2A1>—')],
created () {
// --- 列定�---
// å<>ªéœ€å£°æ˜Ž prop å’?label,idx 自动补é½<C3A9>
// prop: '_actions' 约定为æ“<C3A6>作列,自动渲æŸ?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' }
])
// --- 按钮定义 ---
// ä¸<C3A4>å†<C3A5>分开å†?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) // â†?第二个å<C2AA>æ•°ä¼ å…¥æ<C2A5>ƒé™<C3A9>校验函æ•?
this.toolbarButtons = btns.toolbarButtons
this.rowButtons = btns.rowButtons
}
}
```
### 第二步:写模æ<C2A1>¿ï¼ˆ`<template>` 中)
```vue
<template>
<d2-container>
<!-- æ<EFBFBD>œç´¢åŒ?-->
<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"
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>
```
### 第三步:写业务方法(增删改查�
```js
methods: {
// 获å<C2B7>列表数æ<C2B0>®
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 }
},
// æ<>œç´¢ / é‡<C3A9>ç½®
onSearch () { this.pagination.current = 1; this.fetchData() },
onReset () { this.search = { code: '', name: '' }; this.pagination.current = 1; this.fetchData() },
// 分页å<C2B5>˜åŒ
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
})
},
// ç¼è¾ï¼šåžå¡«è¡¨å<C2A8>? 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
},
// æ<><C3A6>交表å<C2A8>
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. 完整示ä¾ä»£ç <C3A7>
> ðŸ“<EFBFBD> `src/views/production-master-data/factory-model/factory-area/index.vue`
> 这是一ä¸?*å<>¯ç´æŽ¥è¿<C3A8>行的完整 CRUD 页é<C2B5>¢**,包å<E280A6>«æ<C2AB>œç´¢æ <C3A6>ã€<C3A3>表格ã€<C3A3>分页ã€<C3A3>æ°å¢?ç¼è¾å¼¹æ¡†ã€<C3A3>删除确认ã€?
### 模æ<C2A1>¿éƒ¨åˆ
```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) { /* å<>消删除 / 请æ±å¤±è´¥ä¸<C3A4>处ç<E2809E>?*/ }
}
}
}
```
---
## 3. 组件 API å<>è€?
### 3.1 `page-table` �表格 + 按钮�+ 分页
#### Props
| Prop | 类型 | 默认�| 说明 |
|------|------|--------|------|
| `columns` | `Array` | `[]` | 列定义,ç”?`useTableColumns()` 生æˆ<C3A6> |
| `data` | `Array` | `[]` | 表格行数æ<C2B0>?|
| `loading` | `Boolean` | `false` | 是å<C2AF>¦æ˜¾ç¤º loading é<>®ç½© |
| `height` | `String/Number` | â€?| 表格高度ã€ä¸<C3A4>传则éš<C3A9>内容æå¼€ï¼ä¼ å…·ä½“数值åºå®šé«˜åº¦ï¼ä¼?`'auto'` å<>¯ç”¨è‡ªé€åº” |
| `auto-height` | `Boolean` | `false` | å<>¯ç”¨é«˜åº¦è‡ªé€åº”,表格自动填满å<C2A1>¯ç”¨ç©ºé—?|
| `border` | `Boolean` | `true` | 是å<C2AF>¦å¸¦è¾¹æ¡?|
| `row-key` | `String` | `'id'` | 行唯一 key |
| `toolbar-buttons` | `Array` | `[]` | 顶部工具æ <C3A6>按é®ï¼Œç”?`useTableButtons()` 生æˆ<C3A6> |
| `row-buttons` | `Array` | `[]` | 行内æ“<C3A6>作按é®ï¼Œç”± `useTableButtons()` 生æˆ<C3A6> |
| `pagination` | `Object` | `null` | 分页å<C2B5>æ•° `{ current, size, total }`,传了æ‰<EFBFBD>显示分页 |
| `table-attrs` | `Object` | `{}` | é¢<C3A9>å¤é€<C3A9>ä¼ ç»?`el-table` 的属æ€?|
| `table-listeners` | `Object` | `{}` | é¢<C3A9>å¤é€<C3A9>ä¼ ç»?`el-table` 的事ä»?|
| `help-url` | `String` | `''` | 帮助文档跳转 URLã€ä¼ äº†æ‰<C3A6>显示工具æ <C3A6>å<EFBFBD>³ä¾§çš„é—®å<C2AE>·æŒ‰é®ï¼Œç¹å‡»æ°çª—å<E28094>£æ‰“å¼€ |
| `help-text` | `String` | `'帮助'` | 帮助按钮文字,支�i18n key(组件自�`$t()` 翻译�|
#### 事件
| äºä»¶å<C2B6>?| å<>æ•° | 说明 |
|--------|------|------|
| `@page-change` | `{ current, size, total }` | 分页å<C2B5>˜åŒï¼ˆåˆ‡æ<E280A1>¢é¡µç <>¡æ•°ï¼?|
| `@selection-change` | `rows: Array` | 选中行å<C592>˜åŒ?|
| `@sort-change` | é€<C3A9>ä¼  el-table 原生事件 | æŽåº<C3A5>å<EFBFBD>˜åŒ |
#### æ<>æ§½
| æ<>æ§½å<C2BD>?| 作用åŸ?| 说明 |
|--------|--------|------|
| `#col-{prop}` | `{ row, index }` | 自定义列的渲染(列定义中 prop 需�`slot: true`�|
| `#toolbar-extra` | â€?| 工具æ <C3A6>区域追加自定义内容 |
| `#empty` | â€?| 表格空数æ<C2B0>®æ—¶çš„å<E2809E> ä½?|
| `#append` | â€?| 表格最å<E282AC>Žä¸€è¡Œå<C592>Žè¿½åŠ  |
| `#extra` | â€?| 页é<C2B5>¢åº•部追加区域 |
#### 列定义规�
```js
// 普通列
{ prop: 'code', label: 'ç¼ç <C3A7>', minWidth: 120 }
// æ“<C3A6>作列(约定:prop === '_actions'ï¼?{ prop: '_actions', label: 'æ“<C3A6>作', width: 160, fixed: 'right' }
// 自定义æ<E280B0>槽列(slot: trueï¼?{ prop: 'status', label: '状æ€?, slot: true, width: 100 }
// å¤<C3A5>选框åˆ?+ åº<C3A5>å<EFBFBD>·åˆ—(通过 useTableColumns 第二个å<C2AA>数)
useTableColumns([...], { selectionWidth: 55, indexWidth: 60 })
```
---
### 3.2 `page-dialog-form` �增删改查弹框
#### Props
| Prop | 类型 | 默认�| 说明 |
|------|------|--------|------|
| `visible` | `Boolean` | `false` | 弹框显éš<C3A9>,使ç”?`.sync` 修饰ç¬?|
| `title` | `String` | `''` | 弹框标题,支�i18n key |
| `width` | `String` | `'35%'` | 弹框宽度 |
| `form-cols` | `Array` | `[]` | 表å<C2A8>•字段结构(二维数组,è§<C3A8>䏿¹ï¼‰ |
| `form-data` | `Object` | `{}` | 表å<C2A8>•æ•°æ<C2B0>®å¯¹è±¡ |
| `rules` | `Object` | `{}` | 校验规则,与 `el-form` rules 一�|
| `label-width` | `String` | `'100px'` | label 宽度 |
| `submitting` | `Boolean` | `false` | æ<><C3A6>交 loading 状æ€?|
| `confirm-text` | `String` | `'确定'` | 确定按钮文字 |
| `cancel-text` | `String` | `'å<>消'` | å<>æ¶ˆæŒ‰é®æ‡å­— |
#### 事件
| äºä»¶å<C2B6>?| 说明 |
|--------|------|
| `@submit` | 表å<C2A8>•验è¯<C3A8>通过å<E280A1>Žè§¦å<C2A6>,父组件执行æ<C592><C3A6>äº¤é€»è¾ |
| `@close` | 弹框关闭å<C2AD>Žè§¦å<C2A6>?|
#### 方法(通过 ref 调用�
| 方法 | 说明 |
|------|------|
| `reset()` | é‡<C3A9>置表å<C2A8>• |
| `validate()` | æ‰åŠ¨éªŒè¯<C3A8>,返å?`Promise<boolean>` |
#### formCols æ•°æ<C2B0>®ç»“æž„
**注æ„<C3A6>:需åœ?`data()` 中用 `k(prop)` æ<><C3A6>å‰<C3A5>完æˆ<C3A6> i18n 翻译**,ä¸<C3A4>è¦<C3A8>ä¼  raw keyã€?
```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%' } }
]
]
```
**字段支æŒ<C3A6>的属性:**
| 属�| 适用类型 | 说明 |
|------|---------|------|
| `type` | 全部 | `'input'`(文本输入)/ `'select'`(下拉) |
| `prop` | 全部 | 绑定 `formData` 中的 key |
| `label` | 全部 | 表å<C2A8>•项标ç­?|
| `placeholder` | 全部 | å<> ä½<C3A4>æ<EFBFBD><C3A6>示 |
| `inputType` | `input` | `'textarea'` 多行 / ä¸<C3A4>传为普é€?text |
| `autosize` | `input` | textarea çš?`{ minRows, maxRows }` |
| `clearable` | 全部 | 是å<C2AF>¦å<C2A6>¯æ¸…空,默认 `true` |
| `style` | 全部 | æ ·å¼<C3A5>对象 |
| `options` | `select` | 选项数组 `[{ label, value }]` |
| `filterable` | `select` | 是å<C2AF>¦å<C2A6>¯æ<C2AF>œç´¢ï¼Œé»˜è®¤ `true` |
---
## 4. Composable API å<>è€?
### 4.1 `useTableColumns(rawColumns, options?)`
**作用**:消除手动分é…?`idx` åº<C3A5>å<EFBFBD>·ï¼Œè‡ªåŠ¨è¯†åˆ«æ“<C3A6>ä½œåˆ—åŒæ<C592>槽列ã€?
| å<>æ•° | 类型 | 默认å€?| 说明 |
|------|------|--------|------|
| `rawColumns` | `Array` | �| 列定�|
| `options.selectionWidth` | `number` | `55` | å¤<C3A5>选框列宽,传 `0` éš<C3A9>è—<C3A8> |
| `options.indexWidth` | `number` | `0` | åº<C3A5>å<EFBFBD>·åˆ—宽,传 `0` éš<C3A9>è—<C3A8> |
```js
const columns = useTableColumns([
{ prop: 'code', label: 'ç¼ç <C3A7>', width: 120 },
{ prop: 'name', label: <><C3A5>ç§°' },
{ prop: '_actions', label: 'æ“<C3A6>作', width: 160, fixed: 'right' } // â†?æ“<C3A6>作åˆ?])
```
**内部约定**:`prop === '_actions'` 自动标记ä¸?`slot: '_actions'`,由 `page-table` 渲染为æ“<C3A6>作列ã€?
---
### 4.2 `useTableButtons(options, permissionCheck?)`
**作用**:统一生æˆ<C3A6>工具æ <C3A6>按é®åŒè¡Œå†…æ“<C3A6>作按é®ï¼Œè‡ªåŠ¨æ³¨å…¥æ<C2A5>ƒé™<C3A9>校验结果ã€?
| å<>æ•° | 类型 | 说明 |
|------|------|------|
| `options.toolbar` | `Array` | 顶部按钮列表 |
| `options.row` | `Array` | 行内按钮列表 |
| `permissionCheck` | `Function` | æ<>ƒé™<C3A9>函数,通常ä¼?`this.$permission` |
**返回�*:`{ toolbarButtons, rowButtons }`
**按钮字段�*
| 字段 | toolbar | row | 说明 |
|------|:---:|:---:|------|
| `key` | �| �| 唯一标识 |
| `label` | �| �| 显示文本 |
| `icon` | �| �| Element UI 图标 |
| `type` | �| �| `primary`/`success`/`warning`/`danger` |
| `color` | â€?| âœ?| `'danger'` 使æ‡å­—å<E28094>˜çº?|
| `auth` | âœ?| âœ?| æ<>ƒé™<C3A9> key |
| `cssStyle` | �| �| 自定义样�|
| `onClick` | �| �| 点击回调 |
| `hasPermission` | âœ?| âœ?| **自动注入**,由æ<C2B1>ƒé™<C3A9>函数计算 |
| `needSelection` | âœ?| â€?| 需选中行æ‰<C3A6>能ç¹å‡?|
```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)` ä¸¤ä¸ªæ¹æ³•,消除æ¯<C3A6>个页é<C2B5>¢æ‰å†?`T` 常é‡<C3A9>å?`tkey` 方法ã€?
| 方法 | 返回�| 说明 |
|------|--------|------|
| `key(suffix)` | `prefix.suffix` | 当å‰<C3A5>页é<C2B5>¢çš?i18n key |
| `ckey(suffix)` | `page.common.suffix` | 公共 i18n key(å¦"帮助"ã€?确定"ã€?å<>消"ï¼?|
| å<>æ•° | 类型 | 说明 |
|------|------|------|
| `prefix` | `String` | 该页é<C2B5>¢çš„ i18n key å‰<C3A5>ç¼€ï¼Œå¦ `'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')],
// æ­¤å<C2A4>Žæ¨¡æ<C2A1>¿ä¸­ç”¨ $t(key('code')) 访问当å‰<C3A5>页é<C2B5>¢çš„ç¿»è¯? // ç”?$t(ckey('help')) 访问公共翻译 'page.common.help'
// JS 中用 this.$t(this.key('code')) / this.$t(this.ckey('help'))
}
```
**`data()` 中翻译文本的技�*�
```js
data () {
const t = this.$t.bind(this) // 绑定 i18n 翻译函数
const k = (s) => t(this.key(s)) // 当å‰<C3A5>页é<C2B5>¢ç¿»è¯ï¼škey + translate
const ck = (s) => t(this.ckey(s)) // 公共翻è¯ï¼šckey + translate
return {
formCols: [
[{ label: k('code'), placeholder: k('enter_code') }] // 页é<C2B5>¢çº?key
],
helpText: ck('help') // 公共 key
}
}
```
---
## 5. i18n 国际化方�
### 5.1 语言包文�
| 语言 | 文件路径 |
|------|---------|
| 简体中�| `src/locales/zh-chs.json` |
| ç¹<C3A7>ä½“ä¸­æ‡ | `src/locales/zh-cht.json` |
| 英文 | `src/locales/en.json` |
| 日文 | `src/locales/ja.json` |
### 5.2 语言包结�
æŒ?¸€çº§æ¨¡å<C2A1>?â†?二级模å<C2A1>— â†?三级模å<C2A1>—` 三层嵌套ï¼?
```json
{
"page": {
"production_master_data": {
"factory_model": {
"factory_area": {
"search": "查询",
"reset": "é‡<C3A9>ç½®",
"code": "所区编�,
"name": "所区å<EFBFBD><EFBFBD>ç§?,
"add": "��,
"edit": "ç¼?è¾?,
"delete": "��,
"enter_code": "请输入所区编�,
"enter_name": "请输入所区å<C2BA><C3A5>ç§?,
"add_title": "新增所�,
"edit_title": "编辑所�,
"confirm": "确定",
"cancel": "å<EFBFBD>消",
"operation_success": "æ“<EFBFBD>作æˆ<EFBFBD>功"
}
}
}
}
}
```
### 5.3 组件自动翻译
`page-table` �`page-dialog-form` 内置 `$t()`�
- **�label** �自动翻译
- **按钮 label** �自动翻译
- **表å<C2A8>• label / placeholder** â†?自动翻译
- **弹框 title / confirm / cancel** �自动翻译
å æ­¤é¡µé<EFBFBD>¢å<EFBFBD>ªéœ€ä¼ å…¥ i18n key å­—ç¬¦ä¸²ï¼Œç»„ä»¶æ¸²æŸ“æ—¶è‡ªåŠ¨æ¿æ<C2BF>¢ä¸ºå½“å‰<C3A5>è¯­è¨€çš„æ‡æœ¬ã€?
### 5.4 页é<C2B5>¢ä¸­æ‰åŠ¨ç¿»è¯?
æ<EFBFBD>œç´¢åŒºã€<EFBFBD>校验消æ<EFBFBD>¯ã€<EFBFBD>`$confirm` ç­?UI 文字需手动ç”?`$t()`ï¼?
```vue
<!-- 模æ<EFBFBD>¿ä¸?â?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 }
])
```
模æ<EFBFBD>¿ä¸­ç”¨ `#col-status` æ<>æ§½ï¼?
```vue
<page-table :columns="columns" :data="tableData">
<template #col-status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? <>¯ç”¨' : 'ç¦<C3A7>用' }}
</el-tag>
</template>
</page-table>
```
### 场景 2:工具æ <C3A6>追加自定义按é?
```vue
<page-table :columns="columns" :toolbar-buttons="toolbarButtons">
<template #toolbar-extra>
<el-button size="mini" icon="el-icon-printer" @click="print">打å<EFBFBD>°</el-button>
</template>
</page-table>
```
### 场景 3:å¤<C3A5>选框åˆ?+ åº<C3A5>å<EFBFBD>·åˆ?
```js
useTableColumns(
[
{ prop: 'code', label: 'ç¼ç <C3A7>' },
{ prop: 'name', label: <><C3A5>ç§°' }
],
{ selectionWidth: 55, indexWidth: 60 }
)
```
### 场景 4ï¼šå¸¦ä¸æ‰é€‰æ©çš„表å<C2A8>?
```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:批é‡<C3A9>删除(工具æ ?+ 需è¦<C3A8>选中行)
```js
useTableButtons({
toolbar: [
{ key: 'batchDelete', label: '批é‡<C3A9>删除', icon: 'el-icon-delete',
type: 'danger', auth: '/xxx/batch-delete',
needSelection: true, // â†?需è¦<C3A8>选中行æ‰<C3A6>能ç¹å‡? onClick: this.batchDelete }
]
})
```
### 场景 6:表格自é€åº”高度
å<EFBFBD>ªéœ€åŠ?`auto-height` 属性:
```vue
<page-table ... auto-height />
```
表格高度会自动填æ»?`d2-container` çš?body 区域剩余空间,窗å<E28094>?resize / ä¾§æ <C3A6>折å<CB9C> æ—¶è‡ªåЍé‡<C3A9>ç®—ã€?
### 场景 7:帮助按é?
ç»?`help-url` 传值å<C2BC>³å<C2B3>¯åœ¨å·¥å…·æ <C3A6>最å<E282AC>³ä¾§æ˜¾ç¤ºé—®å<C2AE>·å¸®åŠ©æŒ‰é®ï¼Œç¹å‡»åœ¨æ°çª—å<E28094>£æ‰“å¼€å¸®åŠ©æ‡æ¡£ï¼?
```vue
<page-table ... help-url="/help/factory-area" :help-text="$t(ckey('help'))" />
```
按钮文字通过 `help-text` prop 支æŒ<C3A6> i18nã€æŽ¨è<C2A8><C3A8>使用公å…?key `page.common.help`(模æ<EFBFBD>¿ä¸­å†?`$t(ckey('help'))`),所有页é<EFBFBD>¢å…±äº«ï¼Œæ— éœ€æ¯<EFBFBD>个模å<EFBFBD>—é‡<EFBFBD>å¤<EFBFBD>定义ã€?
ä¸<EFBFBD>ä¼  `help-url` æˆä¼ ç©ºå­—符串则ä¸<C3A4>显示帮助按é®ã€?
---
## 7. 路由é…<C3A9>ç½®
```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>Žåœ?`src/router/routes.js` 中引入该模å<C2A1>—并加å…?`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. 旧代ç <C3A7>è¿<C3A8>移对ç…?
| 旧写�| 新写�|
|--------|--------|
| 手动构建 `columns: [{ idx: 0, attrs: { prop, label } }]` | `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>` 组件 | 需è¦<C3A8>时自行添加 |
| 分页需è¦<C3A8>é¢<C3A9>å¤?`<page-footer>` | `page-table` 内置分页 |
| æ¯<C3A6>个页é<C2B5>¢å®šä¹‰ `T` 常é‡<C3A9> + `tkey()` 方法 | `mixins: [i18nMixin(prefix)]`,使ç”?`this.key('xxx')` |
| 表å<C2A8>•字段å­?i18n raw key,å­<C3A5>ç»„ä»¶ç¿»è¯ | `data()` 中用 `k(prop)` æ<><C3A6>å‰<C3A5>ç¿»è¯ |
---
## 10. 常è§<C3A8>é—®é¢˜æŽæŸ¥
### Q1:弹框打开å<E282AC>Žä¸<C3A4>显示内容ï¼?
确认 `formCols` 中的 `label` å’?`placeholder` 是å<C2AF>¦åœ?`data()` 阶段已翻è¯ã€å­<C3A5>组件 `page-dialog-form` 会调ç”?`$t()` 处ç<E2809E>† label,但建议åœ?`data()` 阶段就用 `k()` æ<><C3A6>å‰<C3A5>ç¿»è¯å¥½ï¼Œé<C592>¿å…<C3A5> webpack HMR 缓存问题ã€?
### Q2:表格高度自é€åº”ä¸<C3A4>生效?
确认 `d2-container` çš?body 区域高度被正确约æ<C2A6>Ÿï¼ˆflex 填充)。如æž?`d2-container` 本身æœ?`overflow: auto` æˆå…¶ä»é«˜åº¦çº¦æ<C2A6>Ÿé—®é¢˜ï¼Œ`page-table` çš?`height: 100%` 会失效。给 `d2-container` çš?body 部分è®?`overflow: hidden` 通常å<C2B8>¯è§£å†³ã€?
### Q3:æ<C5A1>ƒé™<C3A9>校验有按é®ä¸<C3A4>显示?
`useTableButtons` 的第二个å<C2AA>æ•°å¿…é¡»ä¼?`this.$permission`ã€å¦æžœé¡¹ç®æ²¡æœ‰æ³¨å†Œè¿™ä¸ªå…¨å±€æ¹æ³•,å<EFBFBD>¯ä»¥åœ¨ `useTableButtons` 中传入一个返å›?`true` çš„å<E2809E> ä½<C3A4>函数:`() => true`ã€?
### Q4:æ°å¢žä¸€æ<E282AC>¡å<C2A1>Žé¡µç <C3A7>ä¸<C3A4>è·³åžç¬¬ä¸€é¡µï¼Ÿ
åœ?`onDialogSubmit` æˆ<C3A6>功å<C5B8>Žè°ƒç”?`this.fetchData()` å‰<C3A5>ï¼Œå¦æžœåˆ é™¤äº†å½“å‰<C3A5>页最å<E282AC>Žä¸€è¡Œå¯¼è‡?`total - 1` 超出范å´ï¼Œéœ€æ‰åŠ¨ä¿®æ­£é¡µç <C3A7>ï¼?
```js
this.pagination.current = Math.min(
this.pagination.current,
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
)
```
### Q5:表å<C2A8>•验è¯<C3A8>ä¸<C3A4>æ<EFBFBD><C3A6>示错误æ‡å­—ï¼?
确认 `page-dialog-form` çš?`<style>` å<>—存在(默认 22px `margin-bottom` + `position: absolute` 错误æ‡å­—)ã€å¦æžœè¡¨å<C2A8>•项ä¹é—´è¢«å…¶ä»æ ·å¼<C3A5>覆ç,检查是å<C2AF>¦æœ‰å…¨å±€ CSS é‡<C3A9>ç½®äº?`.el-form-item` çš?marginã€?
### Q6:å¦ä½•æ°å¢žä¸€ä¸?CRUD 页é<C2B5>¢æœ€å¿«ï¼Ÿ
1. å¤<C3A5>åˆ`src/views/production-master-data/factory-model/factory-area/index.vue`
2. æ¿æ<C2BF>¢ API 引用(`import { getList, create, edit, ... } from '@/api/xxx'`ï¼?3. 修改 `i18nMixin` å<>æ•°ã€<C3A3>`columns`ã€<EFBFBD>`formCols`ã€<EFBFBD>`rules`
4. åœ?`zh-chs.json` å’?`en.json` 中添加语言åŒ?5. 添加路由é…<C3A9>ç½®
å¹³å<EFBFBD>‡ 10 分éŸå<C5B8>³å<C2B3>¯å®Œæˆ<C3A6>一个标å‡?CRUD 页é<C2B5>¢ã€?