1. 新增生产配置-工厂模型-工厂区域完整CRUD页面 2. 新增通用表格、弹窗表单、i18n工具组件 3. 升级sass-loader并修复Sass废弃警告 4. 添加文档记录Sass迁移修复细节
30 KiB
è¡¨æ ¼ç»„ä»¶ä½¿ç”¨è¯´æ˜Ž
基于
page-table+page-dialog-form的新一ä»?CRUD è¡¨æ ¼æ–¹æ¡ˆã€? æº<C3A6>ç <C3A7>ä½<C3A4>置:src/components/page-table/ã€<EFBFBD>src/components/page-dialog-form/ã€<EFBFBD>src/composables/
完整å<C2B4>¯è¿<C3A8>行示例:src/views/production-master-data/factory-model/factory-area/index.vue
目录
- 快速开始(三æ¥èµ°ï¼‰
- 完整示例代ç <EFBFBD>
- [组件 API å<>‚考](#3-组件-api-å<>‚è€?
- [Composable API å<>‚考](#4-composable-api-å<>‚è€?
- [i18n 国际化方案](#5-i18n-国际化方�
- 常用场景速查
- 路由é…<EFBFBD>ç½®
- API 文件写法
- [旧代ç <C3A7>è¿<C3A8>移对照](#9-旧代ç <C3A7>è¿<C3A8>移对ç…?
- 常è§<EFBFBD>问题排查
1. 快速开始(三æ¥èµ°ï¼‰
第一æ¥ï¼šå®šä¹‰åˆ—和按钮(created() ä¸ï¼‰
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
}
}
第二æ¥ï¼šå†™æ¨¡æ<EFBFBD>¿ï¼ˆ<template> ä¸ï¼‰
<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>
第三æ¥ï¼šå†™ä¸šåŠ¡æ–¹æ³•ï¼ˆå¢žåˆ æ”¹æŸ¥ï¼?
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>åˆ é™¤ç¡®è®¤ã€?
模æ<EFBFBD>¿éƒ¨åˆ†
<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 部分
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 |
[] |
è¡¨æ ¼è¡Œæ•°æ<EFBFBD>? |
loading |
Boolean |
false |
是å<EFBFBD>¦æ˜¾ç¤º loading é<>®ç½© |
height |
String/Number |
â€? | è¡¨æ ¼é«˜åº¦ã€‚ä¸<EFBFBD>ä¼ åˆ™éš<EFBFBD>å†…å®¹æ’‘å¼€ï¼›ä¼ å…·ä½“æ•°å€¼å›ºå®šé«˜åº¦ï¼›ä¼?'auto' å<>¯ç”¨è‡ªé€‚应 |
auto-height |
Boolean |
false |
å<EFBFBD>¯ç”¨é«˜åº¦è‡ªé€‚åº”ï¼Œè¡¨æ ¼è‡ªåŠ¨å¡«æ»¡å<EFBFBD>¯ç”¨ç©ºé—? |
border |
Boolean |
true |
是å<EFBFBD>¦å¸¦è¾¹æ¡? |
row-key |
String |
'id' |
行唯一 key |
toolbar-buttons |
Array |
[] |
顶部工具æ <EFBFBD>按钮,ç”?useTableButtons() 生æˆ<C3A6> |
row-buttons |
Array |
[] |
行内æ“<EFBFBD>作按钮,由 useTableButtons() 生æˆ<C3A6> |
pagination |
Object |
null |
分页å<EFBFBD>‚æ•° { current, size, total }ï¼Œä¼ äº†æ‰<EFBFBD>显示分页 |
table-attrs |
Object |
{} |
é¢<EFBFBD>外é€<EFBFBD>ä¼ ç»?el-table 的属æ€? |
table-listeners |
Object |
{} |
é¢<EFBFBD>外é€<EFBFBD>ä¼ ç»?el-table 的事ä»? |
help-url |
String |
'' |
帮助文档跳转 URLã€‚ä¼ äº†æ‰<C3A6>显示工具æ <C3A6>å<EFBFBD>³ä¾§çš„é—®å<C2AE>·æŒ‰é’®ï¼Œç‚¹å‡»æ–°çª—å<E28094>£æ‰“å¼€ |
help-text |
String |
'帮助' |
帮助按钮文å—,支æŒ?i18n key(组件自åŠ?$t() 翻译ï¼? |
事件
| 事件å<EFBFBD>? | å<EFBFBD>‚æ•° | 说明 |
|---|---|---|
@page-change |
{ current, size, total } |
分页å<EFBFBD>˜åŒ–(切æ<EFBFBD>¢é¡µç ?æ<>¡æ•°ï¼? |
@selection-change |
rows: Array |
选ä¸è¡Œå<EFBFBD>˜åŒ? |
@sort-change |
é€<EFBFBD>ä¼ el-table 原生事件 | 排åº<EFBFBD>å<EFBFBD>˜åŒ– |
æ<EFBFBD>’æ§½
| æ<EFBFBD>’æ§½å<EFBFBD>? | 作用åŸ? | 说明 |
|---|---|---|
#col-{prop} |
{ row, index } |
è‡ªå®šä¹‰åˆ—çš„æ¸²æŸ“ï¼ˆåˆ—å®šä¹‰ä¸ prop 需è®?slot: trueï¼? |
#toolbar-extra |
â€? | 工具æ <EFBFBD>åŒºåŸŸè¿½åŠ è‡ªå®šä¹‰å†…å®¹ |
#empty |
â€? | è¡¨æ ¼ç©ºæ•°æ<EFBFBD>®æ—¶çš„å<EFBFBD> ä½? |
#append |
â€? | è¡¨æ ¼æœ€å<EFBFBD>Žä¸€è¡Œå<EFBFBD>Žè¿½åŠ |
#extra |
â€? | 页é<EFBFBD>¢åº•éƒ¨è¿½åŠ åŒºåŸŸ |
列定义规�
// 普通列
{ 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 |
弹框显éš<EFBFBD>,使ç”?.sync 修饰ç¬? |
title |
String |
'' |
å¼¹æ¡†æ ‡é¢˜ï¼Œæ”¯æŒ?i18n key |
width |
String |
'35%' |
弹框宽度 |
form-cols |
Array |
[] |
表å<EFBFBD>•å—æ®µç»“构(二维数组,è§<EFBFBD>下方) |
form-data |
Object |
{} |
表å<EFBFBD>•æ•°æ<EFBFBD>®å¯¹è±¡ |
rules |
Object |
{} |
æ ¡éªŒè§„åˆ™ï¼Œä¸Ž el-form rules 一è‡? |
label-width |
String |
'100px' |
label 宽度 |
submitting |
Boolean |
false |
æ<EFBFBD><EFBFBD>交 loading 状æ€? |
confirm-text |
String |
'确定' |
ç¡®å®šæŒ‰é’®æ–‡å— |
cancel-text |
String |
'å<>–消' |
å<EFBFBD>–æ¶ˆæŒ‰é’®æ–‡å— |
事件
| 事件å<EFBFBD>? | 说明 |
|---|---|
@submit |
表å<EFBFBD>•验è¯<EFBFBD>通过å<EFBFBD>Žè§¦å<EFBFBD>‘,父组件执行æ<EFBFBD><EFBFBD>交逻辑 |
@close |
弹框关é—å<EFBFBD>Žè§¦å<EFBFBD>? |
方法(通过 ref 调用�
| 方法 | 说明 |
|---|---|
reset() |
é‡<EFBFBD>置表å<EFBFBD>• |
validate() |
手动验è¯<EFBFBD>,返å›?Promise<boolean> |
formCols æ•°æ<C2B0>®ç»“æž„
**注æ„<C3A6>:需åœ?data() ä¸ç”¨ k(prop) æ<><C3A6>å‰<C3A5>完æˆ<C3A6> i18n 翻译**,ä¸<C3A4>è¦<C3A8>ä¼ raw keyã€?
// 例:两个普通输入框 + 一个多行文本框
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%' } }
]
]
å—æ®µæ”¯æŒ<EFBFBD>的属性:
| 属� | 适用类型 | 说明 |
|---|---|---|
type |
全部 | 'input'(文本输入)/ 'select'(下拉) |
prop |
全部 | 绑定 formData ä¸çš„ key |
label |
全部 | 表å<EFBFBD>•é¡¹æ ‡ç? |
placeholder |
全部 | å<EFBFBD> ä½<EFBFBD>æ<EFBFBD><EFBFBD>示 |
inputType |
input |
'textarea' 多行 / ä¸<C3A4>ä¼ ä¸ºæ™®é€?text |
autosize |
input |
textarea çš?{ minRows, maxRows } |
clearable |
全部 | 是å<EFBFBD>¦å<EFBFBD>¯æ¸…空,默认 true |
style |
全部 | æ ·å¼<EFBFBD>对象 |
options |
select |
选项数组 [{ label, value }] |
filterable |
select |
是å<EFBFBD>¦å<EFBFBD>¯æ<EFBFBD>œç´¢ï¼Œé»˜è®¤ true |
4. Composable API å<>‚è€?
4.1 useTableColumns(rawColumns, options?)
**作用**:消除手动分é…?idx åº<C3A5>å<EFBFBD>·ï¼Œè‡ªåŠ¨è¯†åˆ«æ“<C3A6>作列和æ<C592>’槽列ã€?
| å<EFBFBD>‚æ•° | 类型 | 默认å€? | 说明 |
|---|---|---|---|
rawColumns |
Array |
� | 列定� |
options.selectionWidth |
number |
55 |
å¤<EFBFBD>é€‰æ¡†åˆ—å®½ï¼Œä¼ 0 éš<C3A9>è—<C3A8> |
options.indexWidth |
number |
0 |
åº<EFBFBD>å<EFBFBD>·åˆ—å®½ï¼Œä¼ 0 éš<C3A9>è—<C3A8> |
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>æ ¡éªŒç»“æžœã€?
| å<EFBFBD>‚æ•° | 类型 | 说明 |
|---|---|---|
options.toolbar |
Array |
顶部按钮列表 |
options.row |
Array |
行内按钮列表 |
permissionCheck |
Function |
æ<EFBFBD>ƒé™<EFBFBD>函数,通常ä¼?this.$permission |
**返回�*:{ toolbarButtons, rowButtons }
*æŒ‰é’®å—æ®µï¼?
| å—æ®µ | toolbar | row | 说明 |
|---|---|---|---|
key |
âœ? | âœ? | å”¯ä¸€æ ‡è¯† |
label |
� | � | 显示文本 |
icon |
âœ? | âœ? | Element UI å›¾æ ‡ |
type |
� | � | primary/success/warning/danger |
color |
â€? | âœ? | 'danger' 使文å—å<E28094>˜çº? |
auth |
âœ? | âœ? | æ<EFBFBD>ƒé™<EFBFBD> key |
cssStyle |
âœ? | âœ? | è‡ªå®šä¹‰æ ·å¼? |
onClick |
� | � | 点击回调 |
hasPermission |
âœ? | âœ? | **自动注入**,由æ<C2B1>ƒé™<C3A9>函数计算 |
needSelection |
âœ? | â€? | 需选ä¸è¡Œæ‰<EFBFBD>能点å‡? |
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 |
当å‰<EFBFBD>页é<EFBFBD>¢çš?i18n key |
ckey(suffix) |
page.common.suffix |
公共 i18n key(如"帮助"ã€?确定"ã€?å<>–消"ï¼? |
| å<EFBFBD>‚æ•° | 类型 | 说明 |
|---|---|---|
prefix |
String |
该页é<EFBFBD>¢çš„ i18n key å‰<C3A5>缀,如 'page.production_master_data.factory_model.factory_area' |
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() ä¸ç¿»è¯‘文本的技å·?*ï¼?
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 |
| ç¹<EFBFBD>体䏿–‡ | src/locales/zh-cht.json |
| 英文 | src/locales/en.json |
| 日文 | src/locales/ja.json |
5.2 è¯è¨€åŒ…结æž?
æŒ?一级模å<EFBFBD>?â†?二级模å<C2A1>— â†?三级模å<C2A1>— 三层嵌套ï¼?
{
"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 �自动翻译
- 表å<EFBFBD>• label / placeholder â†?自动翻译
- 弹框 title / confirm / cancel �自动翻译
å› æ¤é¡µé<EFBFBD>¢å<EFBFBD>ªéœ€ä¼ å…¥ i18n key å—符串,组件渲染时自动替æ<C2BF>¢ä¸ºå½“å‰<C3A5>è¯è¨€çš„æ–‡æœ¬ã€?
5.4 页é<C2B5>¢ä¸æ‰‹åŠ¨ç¿»è¯?
æ<EFBFBD>œç´¢åŒºã€<EFBFBD>æ ¡éªŒæ¶ˆæ<EFBFBD>¯ã€<EFBFBD>$confirm ç?UI æ–‡å—需手动ç”?$t()ï¼?
<!-- 模æ<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 �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 }
])
模æ<EFBFBD>¿ä¸ç”¨ #col-status æ<>’æ§½ï¼?
<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>è¿½åŠ è‡ªå®šä¹‰æŒ‰é’?
<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>·åˆ?
useTableColumns(
[
{ prop: 'code', label: 'ç¼–ç <C3A7>' },
{ prop: 'name', label: 'å<><C3A5>ç§°' }
],
{ selectionWidth: 55, indexWidth: 60 }
)
场景 4:带下拉选择的表å<C2A8>?
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>选ä¸è¡Œï¼‰
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 属性:
<page-table ... auto-height />
è¡¨æ ¼é«˜åº¦ä¼šè‡ªåŠ¨å¡«æ»?d2-container çš?body 区域剩余空间,窗å<E28094>?resize / ä¾§æ <C3A6>折å<CB9C> 时自动é‡<C3A9>ç®—ã€?
场景 7:帮助按�
ç»?help-url ä¼ å€¼å<C2BC>³å<C2B3>¯åœ¨å·¥å…·æ <C3A6>最å<E282AC>³ä¾§æ˜¾ç¤ºé—®å<C2AE>·å¸®åŠ©æŒ‰é’®ï¼Œç‚¹å‡»åœ¨æ–°çª—å<E28094>£æ‰“开帮助文档ï¼?
<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>定义ã€?
ä¸<C3A4>ä¼ help-url æˆ–ä¼ ç©ºå—符串则ä¸<C3A4>显示帮助按钮ã€?
7. 路由é…<C3A9>ç½®
// 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 数组ï¼?
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. 旧代ç <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> 组件 |
需è¦<EFBFBD>æ—¶è‡ªè¡Œæ·»åŠ |
分页需è¦<EFBFBD>é¢<EFBFBD>å¤?<page-footer> |
page-table 内置分页 |
æ¯<EFBFBD>个页é<EFBFBD>¢å®šä¹‰ T 常é‡<C3A9> + tkey() 方法 |
mixins: [i18nMixin(prefix)],使�this.key('xxx') |
| 表å<EFBFBD>•å—æ®µå?i18n raw key,å<C3A5>组件翻译 | data() ä¸ç”¨ k(prop) æ<><C3A6>å‰<C3A5>翻译 |
10. 常è§<C3A8>问题排查
Q1:弹框打开å<EFBFBD>Žä¸<EFBFBD>显示内容ï¼?
确认 formCols ä¸çš„ label å’?placeholder 是å<C2AF>¦åœ?data() 阶段已翻译。å<C3A5>组件 page-dialog-form 会调ç”?$t() 处ç<E2809E>† label,但建议åœ?data() 阶段就用 k() æ<><C3A6>å‰<C3A5>翻译好,é<C592>¿å…<C3A5> webpack HMR 缓å˜é—®é¢˜ã€?
Q2ï¼šè¡¨æ ¼é«˜åº¦è‡ªé€‚åº”ä¸<EFBFBD>生效?
确认 d2-container çš?body 区域高度被æ£ç¡®çº¦æ<C2A6>Ÿï¼ˆflex 填充)。如æž?d2-container 本身æœ?overflow: auto 或其他高度约æ<C2A6>Ÿé—®é¢˜ï¼Œpage-table çš?height: 100% 会失效。给 d2-container çš?body 部分è®?overflow: hidden 通常å<C2B8>¯è§£å†³ã€?
Q3:æ<EFBFBD>ƒé™<EFBFBD>æ ¡éªŒæœ‰æŒ‰é’®ä¸<EFBFBD>显示?
useTableButtons 的第二个å<C2AA>‚æ•°å¿…é¡»ä¼?this.$permission。如果项目没有注册这个全局方法,å<EFBFBD>¯ä»¥åœ¨ useTableButtons ä¸ä¼ 入一个返å›?true çš„å<E2809E> ä½<C3A4>函数:() => trueã€?
Q4:新增一æ<EFBFBD>¡å<EFBFBD>Žé¡µç <EFBFBD>ä¸<EFBFBD>跳回第一页?
åœ?onDialogSubmit æˆ<C3A6>功å<C5B8>Žè°ƒç”?this.fetchData() å‰<C3A5>ï¼Œå¦‚æžœåˆ é™¤äº†å½“å‰<C3A5>页最å<E282AC>Žä¸€è¡Œå¯¼è‡?total - 1 超出范围,需手动修æ£é¡µç <C3A7>ï¼?
this.pagination.current = Math.min(
this.pagination.current,
Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1
)
Q5:表å<EFBFBD>•验è¯<EFBFBD>ä¸<EFBFBD>æ<EFBFBD><EFBFBD>示错误文å—ï¼?
确认 page-dialog-form çš?<style> å<>—å˜åœ¨ï¼ˆé»˜è®¤ 22px margin-bottom + position: absolute 错误文å—)。如果表å<C2A8>•é¡¹ä¹‹é—´è¢«å…¶ä»–æ ·å¼<C3A5>覆盖,检查是å<C2AF>¦æœ‰å…¨å±€ CSS é‡<C3A9>ç½®äº?.el-form-item çš?marginã€?
Q6:如何新增一ä¸?CRUD 页é<C2B5>¢æœ€å¿«ï¼Ÿ
- å¤<EFBFBD>制
src/views/production-master-data/factory-model/factory-area/index.vue - 替æ<EFBFBD>¢ API 引用(
import { getList, create, edit, ... } from '@/api/xxx'ï¼?3. 修改i18nMixinå<>‚æ•°ã€<C3A3>columnsã€<EFBFBD>formColsã€<EFBFBD>rules - åœ?
zh-chs.jsonå’?en.json䏿·»åŠ è¯è¨€åŒ?5. æ·»åŠ è·¯ç”±é…<C3A9>ç½®
å¹³å<EFBFBD>‡ 10 分钟å<C5B8>³å<C2B3>¯å®Œæˆ<C3A6>ä¸€ä¸ªæ ‡å‡?CRUD 页é<C2B5>¢ã€?