1. 新增生产配置-工厂模型-工厂区域完整CRUD页面 2. 新增通用表格、弹窗表单、i18n工具组件 3. 升级sass-loader并修复Sass废弃警告 4. 添加文档记录Sass迁移修复细节
40 KiB
sct-base-table 组件架构重构方案(方案 B)
设计人:前端团队
日期:2026-05-26
状态:待评审(RFC)
目录
1. 背景与现状
1.1 当前使用规模
src/components/sct-base-table/index.vue 是一个基于 el-table 的二次封装组件,在旧项目中被 100+ 个页面引用,是使用频率最高的公共组件。
1.2 现有问题
| # | 问题 | 严重程度 | 说明 |
|---|---|---|---|
| 1 | 引入了 yargs(Node.js CLI 库)到浏览器代码 |
🔴 致命 | import { boolean } from 'yargs' 会导致打包体积膨胀,且 yargs 依赖 Node API |
| 2 | 60% CSS 为注释掉的死代码 | 🔴 | 100+ 行 /* ... */ 注释占据组件内容 |
| 3 | 包含备份文件 index - 副本.vue |
🔴 | 源码应无备份文件 |
| 4 | v-bind="$attrs" 无白名单过滤 |
🟡 | 任何父组件属性都会污染 el-table DOM |
| 5 | 按钮栏与表格强耦合 | 🟡 | 无法单独复用表格或按钮栏 |
| 6 | 每个页面手动分配 idx 序号 |
🟡 | 100+ 个页面都在重复 { idx: 0, attrs: {...} } |
| 7 | 每个页面重复定义 buttonList / tableButtonList |
🟡 | 结构完全相同,仅在权限和回调上有差异 |
| 8 | __judge 命名不规范 |
🟢 | 双下划线命名、语义不清 |
| 9 | 无分页能力 | 🟡 | 100+ 页面需要额外包装 <el-pagination> |
| 10 | 与 sct-base-dialog、sct-base-form、sct-back-to-top 四件套重复组合 |
🟡 | 每个页面都要手动组装这四个组件 |
1.3 当前使用模式
每个页面的 PageMain 组件:
├── <sct-base-table> ← 表格(含顶部按钮栏)
├── <sct-base-dialog> ← 新增/编辑弹框
│ └── <SctBaseForm> ← 表单
└── <sct-back-to-top> ← 回到顶部
2. 方案总览
2.1 核心理念
从「一个万能组件」拆分为「组合式工具函数 + 职责单一的组件」。
2.2 三层架构
┌─────────────────────────────────────────────────────────────┐
│ 组合层:各页面 PageMain 组件 │
│ 通过 <page-table> 或 <sct-table> + <sct-toolbar> 自由组合 │
├─────────────────────────────────────────────────────────────┤
│ 组件层:8 个职责单一的 UI 组件 │
│ sct-table / sct-toolbar / sct-action-buttons / sct-expand │
│ sct-batch-actions / page-dialog-form / page-search / │
│ page-table(便捷组合体) │
├─────────────────────────────────────────────────────────────┤
│ 逻辑层:5 个可复用的 composable 函数 │
│ useTableColumns / useTableButtons / useTableSelection / │
│ usePagination / usePageTable │
└─────────────────────────────────────────────────────────────┘
2.3 改造范围
| 不改造 | 改造 |
|---|---|
| API 调用的业务逻辑 (methods) | 表格/按钮/列的组装方式 |
| sct-base-form(表单组件) | sct-base-table → 拆为多个小组件 |
| sct-base-dialog(弹框组件) | 页面模板结构 |
| sct-back-to-top | columns/buttonList 的定义方式 |
3. 架构对照:旧 vs 新
3.1 组件拆分对照
旧架构(一个组件包揽一切):
sct-base-table/index.vue
├── 模板:按钮栏+表格(215 行)
├── 逻辑:权限校验+按钮显隐+表头样式
└── 样式:60% 注释掉
新架构(按职责拆分):
src/
├── components/
│ ├── sct-table/index.vue 纯表格封装(~40 行)
│ ├── sct-toolbar/index.vue 顶部工具栏(按钮+筛选,~60 行)
│ ├── sct-action-buttons/index.vue 行内操作按钮(~30 行)
│ ├── sct-expand/index.vue 展开行内容(~40 行)
│ ├── sct-batch-actions/index.vue 批量操作栏(~30 行)
│ ├── page-dialog-form/index.vue 增删改查弹框一体(~80 行)
│ └── page-table/index.vue 便捷组合体(表格+工具栏+分页,~80 行)
└── composables/
├── useTableColumns.js 列定义工厂(~30 行)
├── useTableButtons.js 按钮定义工厂(~50 行)
├── useTableSelection.js 选中行管理(~30 行)
├── usePagination.js 分页逻辑(~40 行)
└── usePageTable.js 表格数据+分页+刷新(~60 行)
3.2 数据流对照
旧模式(单向强耦合):
data() 手写 { idx, attrs: { prop, label, ... } } → columns prop → el-table-column 遍历
新模式(声明式 + 自动推导):
columns: [{ prop: 'code', label: '编码' }] → composable 自动补全 idx → sct-table
4. 新组件详细设计
4.1 sct-table — 纯表格封装
定位:最底层的表格组件,只负责渲染列和数据,没有任何业务逻辑。
Props:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| columns | Array | [] |
列定义列表,直接透传 el-table-column 属性 |
| data | Array | [] |
表格数据 |
| loading | Boolean | false | 加载状态 |
| height | String/Number | — | 表格高度 |
| border | Boolean | true | 是否带边框 |
| rowKey | String | 'id' |
行数据的 Key |
| expandable | Boolean | false | 是否启用展开行 |
Slots:
| 插槽名 | 作用 | 作用域 |
|---|---|---|
#col-{prop} |
自定义列的默认插槽 | { row, $index } |
#col-{prop}-header |
自定义列表头 | { column } |
#expand |
展开行内容 | { row, $index } |
#empty |
空数据占位 | — |
#append |
表格最后一行追加内容 | — |
事件: 与 el-table 一致,透传 @selection-change、@sort-change 等。
示例:
<sct-table
:columns="columns"
:data="tableData"
:loading="loading"
expandable
@selection-change="onSelect"
>
<template #col-status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
<template #col-actions="{ row }">
<sct-action-buttons :row="row" :buttons="rowButtons" />
</template>
<template #expand="{ row }">
<sct-expand :row="row" :fields="expandFields" />
</template>
</sct-table>
4.2 sct-toolbar — 顶部工具栏
定位:表格上方的操作按钮区域,支持权限控制、loading、下拉菜单分组。
Props:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| buttons | Array | [] |
按钮列表(定义见下方) |
| selectedCount | Number | 0 | 当前选中行数 |
| showSelectedHint | Boolean | true | 是否显示"已选 N 项"文字 |
按钮定义结构:
const toolbarButtons = [
{
key: 'add',
label: '新增',
icon: 'el-icon-plus',
type: 'primary',
auth: '/xxx/create',
onClick: handleAdd,
},
{
key: 'delete',
label: '批量删除',
icon: 'el-icon-delete',
type: 'danger',
auth: '/xxx/batch-delete',
needSelection: true, // 需要选中行才能点击
confirmMsg: '确认删除选中项?',
onClick: handleBatchDelete,
},
{
key: 'export',
label: '导出',
icon: 'el-icon-download',
type: 'success',
auth: '/xxx/export',
onClick: handleExport,
},
]
示例:
<sct-toolbar
:buttons="toolbarButtons"
:selected-count="selectedRows.length"
/>
4.3 sct-action-buttons — 行内操作按钮
定位:表格每行末尾的"编辑/删除/查看"等操作按钮。
Props:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| row | Object | — | 当前行数据 |
| buttons | Array | [] |
按钮列表 |
| maxVisible | Number | 3 | 超过此数量折叠到"更多"下拉菜单 |
按钮定义结构:
const rowButtons = [
{ key: 'edit', label: '编辑', icon: 'el-icon-edit', auth: '/xxx/edit', onClick: handleEdit },
{ key: 'delete', label: '删除', icon: 'el-icon-delete', color: 'danger', auth: '/xxx/delete', confirm: true, onClick: handleDelete },
{ key: 'print', label: '打印', icon: 'el-icon-printer', auth: '/xxx/print', onClick: handlePrint },
]
4.4 sct-expand — 展开行内容
定位:表格展开行中以 form 布局展示的详情信息。
<sct-expand :row="row" :fields="[
{ label: '长度(m)', prop: 'length' },
{ label: '宽度(m)', prop: 'width' },
{ label: '高度(m)', prop: 'height' },
{ label: '容积(m³)', prop: 'volume' },
]" />
4.5 page-table — 便捷组合体(向下兼容)
定位:为快速迁移提供一个与旧组件心智模型接近的便捷组合体。在 sct-table 基础上内置了工具栏和分页。
<page-table
:columns="columns"
:data="tableData"
:loading="loading"
:toolbar-buttons="toolbarButtons"
:pagination="pagination"
:row-buttons="rowButtons"
@action="onToolbarAction"
@row-action="onRowAction"
@page-change="onPageChange"
@selection-change="onSelect"
>
<!-- 自定义列插槽透传至 sct-table -->
<template #col-status="{ row }">
<el-tag>...</el-tag>
</template>
<template #expand="{ row }">
<sct-expand ... />
</template>
</page-table>
page-table 只服务于"标准 CRUD 页面"场景(占比 ~80% 的页面)。特殊页面(如检验单管理那种自定义工具栏)直接使用 sct-table + sct-toolbar 组合。
4.6 Composable 层:消除重复代码
useTableColumns(columns)
// 旧方式:每个页面手写
this.columns = [
{ idx: 0, attrs: { prop: 'sort', label: '序号' } },
{ idx: 1, attrs: { prop: 'code', label: '编码' } },
]
// 新方式:声明列定义,idx 自动生成
import { useTableColumns } from '@/composables/useTableColumns'
const columns = useTableColumns([
{ prop: 'sort', label: '序号', width: 80 },
{ prop: 'code', label: '编码', minWidth: 120 },
{ prop: 'name', label: '名称' },
{ prop: '_actions', label: '操作', width: 200, fixed: 'right' },
])
// 自动补齐 idx,_actions 约定为操作列插槽
useTableButtons(...)
// 生成 toolbar 按钮 + row 按钮,自动注入权限判断
const { toolbarButtons, rowButtons } = useTableButtons({
toolbar: [
{ key: 'add', label: '新增', ... },
],
row: [
{ key: 'edit', label: '编辑', ... },
],
}, permissionCheck)
usePagination()
const { pagination, onPageChange, resetPage } = usePagination({
defaultPageSize: 20,
})
usePageTable(fetchFn)
// 封装表格数据 + 分页 + loading + 刷新
const { data, loading, pagination, refresh, search } = usePageTable(fetchList)
5. 页面迁移对照示例
5.1 标准 CRUD 页面(产线管理)
源文件:
views/production_configuration/factory_model/factory_line/components/PageMain/index.vue
旧代码(简化)
<template>
<div v-loading="loading">
<sct-base-table
ref="sctBaseTable"
:columns="columns"
:data="currentTableData"
:button-list="buttonList"
@selection-change="handleSelectionChange">
<template #handle="{ scope: { row, $index } }">
<strong v-for="(item, index) in tableButtonList" :key="index">
<i :class="item.icon" :style="item.cssStyle"
v-if="$permission(item.auth)"
@click="item.handle(row, $index)">{{ $t(item.label) }}</i>
</strong>
</template>
</sct-base-table>
<sct-base-dialog ...>
<SctBaseForm ... />
</sct-base-dialog>
<sct-back-to-top />
</div>
</template>
<script>
export default {
data() {
return {
currentTableData: [],
columns: [], // ← 手动构建
buttonList: [...], // ← 重复定义
tableButtonList: [...],
// ... 弹框相关 fields
}
},
mounted() {
this.getColumns()
this.getFormData()
},
methods: {
getColumns() {
this.columns = [
{ idx: 0, attrs: { prop: 'sort', label: '排序' } },
{ idx: 1, attrs: { prop: 'code', label: '编码' } },
{ idx: 2, attrs: { prop: 'name', label: '名称' } },
{ idx: 3, slot: 'handle', attrs: { label: '操作' } },
]
},
// ... 20+ 个方法
}
}
</script>
新代码(使用 page-table 便捷组合体)
<template>
<page-table
:columns="columns"
:data="tableData"
:loading="loading"
:toolbar-buttons="toolbarButtons"
:row-buttons="rowButtons"
:pagination="pagination"
@action="onToolbarAction"
@row-action="onRowAction"
@page-change="onPageChange"
@selection-change="onSelect"
/>
<page-dialog-form
v-model="dialogVisible"
:title="dialogTitle"
:form-cols="formCols"
:form-data="formData"
:rules="rules"
@submit="handleSubmit"
/>
</template>
<script>
import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
import { usePageTable } from '@/composables/usePageTable'
export default {
setup() {
// 列定义 —— 一次声明,永不手动分配 idx
const columns = useTableColumns([
{ prop: 'sort', label: '排序', width: 80 },
{ prop: 'code', label: '产线编码', minWidth: 120 },
{ prop: 'name', label: '产线名称' },
{ prop: 'area_name', label: '所属区域' },
{ prop: 'remark', label: '备注' },
{ prop: '_actions', label: '操作', width: 180, fixed: 'right' },
])
// 按钮定义 —— 不用再写 tableButtonList 和 buttonList 两套了
const { toolbarButtons, rowButtons } = useTableButtons({
toolbar: [
{ key: 'add', label: '新增', type: 'primary', icon: 'el-icon-plus', auth: 'create', onClick: openDialog },
],
row: [
{ key: 'edit', label: '编辑', icon: 'el-icon-edit', auth: 'edit', onClick: handleEdit },
{ key: 'delete', label: '删除', icon: 'el-icon-delete', color: 'danger', auth: 'delete', confirm: true, onClick: handleDelete },
],
})
// 表格数据 + 分页
const { data: tableData, loading, pagination, refresh } = usePageTable(fetchLineList)
return {
columns,
toolbarButtons,
rowButtons,
tableData,
loading,
pagination,
}
},
}
</script>
5.2 复杂自定义页面(检验单管理)
源文件:
views/quality_control/xqc/inspection_order_manage/components/PageMain/index.vue
这个页面工具栏有 10 个自定义按钮 + 列筛选器 + 多处自定义事件,旧代码直接用了 vxe-table 绕过了 sct-base-table。
新代码(使用底层组件自由组合)
<template>
<div>
<!-- 自定义工具栏 -->
<sct-toolbar :buttons="toolbarButtons" :selected-count="selectedCount">
<template #extra>
<el-popover placement="bottom-start" width="280" trigger="click">
<div v-for="item in columnPickerItems" :key="item.key" class="col-picker-row">
<el-checkbox v-model="columnVisible[item.key]">{{ item.label }}</el-checkbox>
</div>
<el-button slot="reference" size="mini" icon="el-icon-s-operation">列显示</el-button>
</el-popover>
</template>
</sct-toolbar>
<!-- 纯表格(vxe-table 或 el-table) -->
<vxe-table
ref="xTable"
border resizable show-header-overflow
:data="tableData"
:height="tableHeight"
@checkbox-change="syncCheckbox"
>
<vxe-column type="checkbox" width="55" fixed="left" />
<vxe-column
v-for="col in visibleColumns"
:key="col.field"
v-bind="col"
/>
</vxe-table>
</div>
</template>
5.3 带展开行的页面(库位管理)
<template>
<page-table
:columns="columns"
:data="tableData"
:toolbar-buttons="toolbarButtons"
:row-buttons="rowButtons"
:pagination="pagination"
>
<template #col-status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
<template #expand="{ row }">
<sct-expand :row="row" :fields="expandFields" />
</template>
</page-table>
</template>
6. 文件清单
6.1 新增文件
| 文件 | 类型 | 行数估计 | 说明 |
|---|---|---|---|
src/composables/useTableColumns.js |
逻辑 | ~40 | 列定义工厂,自动补 idx |
src/composables/useTableButtons.js |
逻辑 | ~60 | 按钮定义工厂,自动权限过滤 |
src/composables/useTableSelection.js |
逻辑 | ~35 | 选中行管理 |
src/composables/usePagination.js |
逻辑 | ~50 | 分页逻辑 |
src/composables/usePageTable.js |
逻辑 | ~70 | 表格数据+分页+刷新一站式 |
src/composables/index.js |
逻辑 | ~10 | 统一导出 |
src/components/sct-table/index.vue |
组件 | ~60 | 纯表格 |
src/components/sct-toolbar/index.vue |
组件 | ~80 | 工具栏 |
src/components/sct-action-buttons/index.vue |
组件 | ~50 | 行内操作按钮 |
src/components/sct-expand/index.vue |
组件 | ~50 | 展开行详情 |
src/components/sct-batch-actions/index.vue |
组件 | ~40 | 批量操作栏 |
src/components/page-dialog-form/index.vue |
组件 | ~100 | 增删改查弹框一体 |
src/components/page-table/index.vue |
组件 | ~100 | 便捷组合体(80% 页面用这个) |
总计:约 745 行新代码(旧 sct-base-table 215 行,删除 ~100 行死代码后实际上只有 ~100 行有效代码)
6.2 修改文件
| 文件 | 修改内容 |
|---|---|
src/components/sct-base-table/index.vue |
删除或标记 @deprecated,重定向到新组件 |
| 100+ 个 PageMain/index.vue | 按页面类型批量替换为 page-table / sct-table+toolbar |
7. 风险与回滚策略
7.1 风险
| 风险 | 概率 | 影响 | 缓解措施 |
|---|---|---|---|
| 新组件 bug 影响线上 | 低 | 高 | 分模块灰度迁移,先迁移"系统设置"模块验证 |
| 性能回退(多一层组件) | 极低 | 低 | 新组件均为无渲染函数式组件,零额外开销 |
| 开发者学习成本 | 中 | 中 | page-table API 刻意靠近旧组件,降低迁移心智负担 |
7.2 回滚策略
- 旧
sct-base-table保留不删,标记@deprecated - 每个模块迁移后在路由配置中可独立切回旧组件
- 使用 Vue 的
defineAsyncComponent做新旧切换的 feature flag
8. 工时估算
| 阶段 | 内容 | 预估时间 |
|---|---|---|
| 阶段 0 | 创建 composables + 核心组件 | 0.5 天 |
| 阶段 1 | 创建 page-table 便捷组合体 | 0.5 天 |
| 阶段 2 | 迁移「系统设置」模块(6 页)验证 | 0.5 天 |
| 阶段 3 | 迁移「生产配置」(15 页) | 1 天 |
| 阶段 4 | 迁移「设备模型」(14 页) | 1 天 |
| 阶段 5 | 迁移「计划与生产」(10 页) | 1 天 |
| 阶段 6 | 迁移「质量管理」(26 页) | 2 天 |
| 阶段 7 | 迁移「数据中台」(8 页) | 0.5 天 |
| 阶段 8 | 迁移「仓储管理」(25 页) | 2 天 |
| 阶段 9 | 迁移「SCADA 管理」(10 页) | 1 天 |
| 阶段 10 | 迁移特殊页面(检验单、鹰眼等) | 1 天 |
| 合计 | 约 11 天 |
注:与搬迁工作并行进行,每个模块搬迁时顺便替换。
附录 A:完整的 buttonList 到 toolbarButtons 映射
// 旧 buttonList 结构
buttonList: [
{
label: '新增',
size: 'mini',
icon: 'el-icon-plus',
type: 'primary',
auth: '/xxx/create',
handle: this.openDialog,
cssStyle: { background: '#3CBA92' },
noShow: this.someCondition,
}
]
// 新 toolbarButtons 结构
toolbarButtons: [
{
key: 'add',
label: this.$t('新增'),
icon: 'el-icon-plus',
type: 'primary',
auth: '/xxx/create',
onClick: openDialog,
// cssStyle 通过 type 预设色系,自定义色系用 color prop
visible: computed(() => someCondition),
}
]
附录 B:完整的 columns 结构变化
// 旧 —— 命令式
{ idx: 0, attrs: { prop: 'sort', label: '排序' } }
{ idx: 5, slot: 'handle', attrs: { label: '操作' } }
{ idx: 3, slot: 'status', headerSlot: 'status_header', attrs: { prop: 'status', label: '状态' } }
// 新 —— 声明式
{ prop: 'sort', label: '排序' }
{ prop: '_actions', label: '操作' } // _actions 约定为操作列
{ prop: 'status', label: '状态', slot: true, headerSlot: 'status_header' } // slot: true 启用插槽
📖 使用指南
本节基于已落地的实际代码编写。源码位置:
src/components/page-table/、src/components/page-dialog-form/、src/composables/。完整可运行示例参考:
src/views/production-configuration/factory-model/factory-area/index.vue
快速开始(三步走)
第一步:在 created() 中用 useTableColumns 声明列、useTableButtons 声明按钮:
import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
created () {
// 列定义 —— 不再手动分配 idx
this.columns = useTableColumns([
{ prop: 'sort', label: '排序', width: 80 },
{ prop: 'code', label: '编码', minWidth: 120 },
{ prop: 'name', label: '名称', minWidth: 120 },
{ prop: 'remark', label: '备注' },
{ prop: '_actions', label: '操作', width: 160, fixed: 'right' } // ← 操作列
])
// 按钮定义 —— 不再分开写 buttonList / tableButtonList
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)
this.toolbarButtons = btns.toolbarButtons
this.rowButtons = btns.rowButtons
}
第二步:模板中直接用 <page-table> + <page-dialog-form>:
<template>
<d2-container>
<template #header>
<!-- 搜索区:直接用 el-form 内联表单 -->
<el-form :inline="true" size="mini">
<el-form-item label="编码">
<el-input v-model="search.code" placeholder="输入编码" clearable
style="width:200px" @keyup.enter.native="onSearch" />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="search.name" placeholder="输入名称" clearable
style="width:200px" @keyup.enter.native="onSearch" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="onSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</template>
<!-- 表格 + 按钮栏 + 分页 -->
<page-table
:columns="columns"
:data="tableData"
:loading="loading"
:toolbar-buttons="toolbarButtons"
:row-buttons="rowButtons"
:pagination="pagination"
@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"
@submit="onDialogSubmit"
@close="onDialogClose"
/>
</d2-container>
</template>
第三步:写业务方法(fetchData / 增删改),和旧代码完全一致:
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 }
},
onPageChange (page) {
this.pagination.current = page.current
this.pagination.size = page.size
this.fetchData()
},
// 新增
openAdd () {
this.handleType = 'create'
this.dialogTitle = '新 增'
this.$nextTick(() => {
this.$refs.dialogForm.reset()
this.formData = { code: '', name: '' }
this.dialogVisible = true
})
},
// 编辑
openEdit (row) {
this.handleType = 'edit'
this.dialogTitle = '编 辑'
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 create(this.formData)
} else {
await edit({ ...this.formData, id: this.editId })
}
this.$message.success('操作成功')
this.dialogVisible = false
this.fetchData()
} finally { this.submitting = false }
},
// 删除
async handleDelete (row) {
try {
await this.$confirm('确定要执行该操作吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
await del({ id: [row.id] })
this.$message.success('操作成功')
this.fetchData()
} catch (e) { /* 用户取消不处理 */ }
}
}
完整可运行示例
📁
src/views/production-configuration/factory-model/factory-area/index.vue
<template>
<d2-container>
<template #header>
<div class="search-bar">
<el-form :inline="true" size="mini">
<el-form-item label="编码">
<el-input v-model="search.code" placeholder="输入编码" clearable
style="width:200px" @keyup.enter.native="onSearch" />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="search.name" placeholder="输入名称" clearable
style="width:200px" @keyup.enter.native="onSearch" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="onSearch">查询</el-button>
<el-button icon="el-icon-refresh" @click="onReset">重置</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"
@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"
@submit="onDialogSubmit"
@close="onDialogClose"
/>
</d2-container>
</template>
<script>
import { useTableColumns } from '@/composables/useTableColumns'
import { useTableButtons } from '@/composables/useTableButtons'
import { getFactoryAreaList, createFactoryArea, editFactoryArea, deleteFactoryArea } from '@/api/production-configuration/factory-area'
import PageTable from '@/components/page-table'
import PageDialogForm from '@/components/page-dialog-form'
export default {
name: 'factory-area',
components: { PageTable, PageDialogForm },
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: '请输入编码', trigger: 'blur' }],
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
},
columns: [],
toolbarButtons: [],
rowButtons: [],
formCols: [
[{ type: 'input', prop: 'code', label: '编码', placeholder: '请输入编码', clearable: true, style: { width: '90%' } }],
[{ type: 'input', prop: 'name', label: '名称', placeholder: '请输入名称', clearable: true, style: { width: '90%' } }],
[{ type: 'input', prop: 'remark', label: '备注', inputType: 'textarea', autosize: { minRows: 2, maxRows: 6 }, placeholder: '请输入备注', clearable: true, style: { width: '90%' } }]
]
}
},
created () {
this.columns = useTableColumns([
{ prop: 'sort', label: '排序', width: 80 },
{ prop: 'code', label: '编码', minWidth: 120 },
{ prop: 'name', label: '名称', minWidth: 120 },
{ prop: 'remark', label: '备注' },
{ prop: '_actions', label: '操作', width: 160, fixed: 'right' }
])
const btns = useTableButtons({
toolbar: [{ key: 'add', label: '新 增', icon: 'el-icon-plus', type: 'primary', auth: '/production_configuration/factory_model/factory_area/create', onClick: this.openAdd }],
row: [
{ key: 'edit', label: '编辑', icon: 'el-icon-edit', auth: '/production_configuration/factory_model/factory_area/edit', onClick: this.openEdit },
{ key: 'delete', label: '删除', icon: 'el-icon-delete', color: 'danger', auth: '/production_configuration/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 },
openAdd () {
this.handleType = 'create'; this.dialogTitle = '新 增'
this.$nextTick(() => { this.$refs.dialogForm.reset(); this.formData = { code: '', name: '', remark: '' }; this.editId = ''; this.dialogVisible = true })
},
openEdit (row) {
this.handleType = 'edit'; this.dialogTitle = '编 辑'; 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.dialogVisible = false; this.fetchData()
} finally { this.submitting = false }
},
onDialogClose () { this.formData = { code: '', name: '', remark: '' }; this.editId = '' },
async handleDelete (row) {
try {
await this.$confirm('确定要执行该操作吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
await deleteFactoryArea({ id: [row.id] })
this.$message.success('操作成功')
this.pagination.current = Math.min(this.pagination.current, Math.ceil((this.pagination.total - 1) / this.pagination.size) || 1)
this.fetchData()
} catch (e) { /* 取消删除不处理 */ }
}
}
}
</script>
API 速查
useTableColumns(rawColumns, options?)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| rawColumns | Array | — | 列定义,字段同 el-table-column 属性 |
| options.selectionWidth | number | 55 | 复选框列宽,传 0 不显示 |
| options.indexWidth | number | 0 | 序号列宽,传 0 不显示 |
约定:prop: '_actions' 自动映射为操作列插槽,由 page-table 自动渲染 rowButtons。
useTableButtons(options, permissionCheck?)
| 参数 | 类型 | 说明 |
|---|---|---|
| options.toolbar | Array | 顶部工具栏按钮 |
| options.row | Array | 行内操作按钮 |
| permissionCheck | Function | 权限函数,默认 () => true,通常传 this.$permission |
返回值 { toolbarButtons, rowButtons }。
按钮字段:
| 字段 | 说明 |
|---|---|
| key | 唯一标识 |
| label | 显示文本 |
| icon | Element UI 图标名 |
| type | primary / success / warning / danger |
| color | 行内按钮专用:'danger' 显示红色 |
| auth | 权限 key(传给 $permission 检查) |
| onClick | 点击回调 |
| hasPermission | 自动注入,由 permissionCheck(auth) 计算 |
page-table — 组件 Props
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| columns | Array | [] |
列定义(由 useTableColumns 生成) |
| data | Array | [] |
表格数据 |
| loading | Boolean | false | loading 遮罩 |
| height | String/Number | — | 表格高度 |
| toolbarButtons | Array | [] |
顶部按钮(由 useTableButtons 生成) |
| rowButtons | Array | [] |
行内按钮(由 useTableButtons 生成) |
| pagination | Object | null | 传 { current, size, total } 才显示分页 |
| border | Boolean | true | 是否带边框 |
| rowKey | String | 'id' |
行 key |
事件:@page-change、@selection-change,同时透传 el-table 原生事件。
插槽:#col-{prop} 自定义列渲染、#toolbar-extra 工具栏追加、#empty / #append / #extra。
page-dialog-form — 组件 Props
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| visible | Boolean | false | .sync 绑定 |
| title | String | '' |
弹框标题 |
| width | String | '35%' |
弹框宽度 |
| formCols | Array | [] |
表单字段(二维数组,每行一个子数组) |
| formData | Object | {} |
表单数据 |
| rules | Object | {} |
校验规则(el-form rules) |
| labelWidth | String | '100px' |
label 宽度 |
| submitting | Boolean | false | 提交中 loading |
| confirmText | String | '确定' |
确认按钮文字 |
| cancelText | String | '取消' |
取消按钮文字 |
方法(通过 ref 调用):reset() 重置表单、validate() 返回 Promise。
formCols 字段项:
| 字段 | 说明 |
|---|---|
| type | 'input'(默认 input)/ 'select' |
| prop | 绑定 formData.prop 的 key |
| label | 表单项标签 |
| placeholder | 占位文本 |
| inputType | textarea 时启用多行输入 |
| autosize | textarea 的 { minRows, maxRows } |
| style | 样式对象,如 { width: '90%' } |
| options | type 为 'select' 时的选项数组 [{ label, value }] |
常用场景速查
场景 1:自定义列渲染(如状态显示 tag)
<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>
列定义中加入 slot: true:
useTableColumns([
{ prop: 'status', label: '状态', slot: true, width: 100 },
])
场景 2:工具栏追加自定义内容
<page-table ...>
<template #toolbar-extra>
<el-button size="mini" icon="el-icon-printer" @click="print">打印</el-button>
</template>
</page-table>
场景 3:序号列
useTableColumns(
[{ prop: 'code', label: '编码' }],
{ selectionWidth: 55, indexWidth: 60 } // 开启复选框 + 序号列
)
路由配置
// src/router/modules/production-configuration.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_configuration',
component: layoutHeaderAside,
children: (pre => [
{
path: 'factory_model/factory_area',
name: `${pre}factory_model-factory_area`,
meta: { ...meta, cache: true, title: '工厂区域' },
component: _import('production-configuration/factory-model/factory-area')
}
])('production_configuration-')
}
旧代码迁移对照
| 旧写法 | 新写法 |
|---|---|
手动 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 组件 |
需要时自行添加 |
分页需额外 page-footer 组件 |
page-table 内置分页 |
国际化 (i18n) 使用说明
组件层自动翻译:page-table 和 page-dialog-form 对所有 label、placeholder、title 等文本属性自动调用 $t() 翻译。因此页面只需传入 i18n key,组件会自动渲染翻译后的文本。如果传入的是普通字符串(非 i18n key),$t() 会原样返回,不会报错。
页面层用法:
// 约定:每个页面定义一个 T 常量作为该页面的 i18n key 前缀
const T = 'page.production_configuration.factory_model.factory_area'
// 列定义 —— 传 i18n key,组件自动翻译
columns = useTableColumns([
{ prop: 'sort', label: T + '.sort', width: 80 },
{ prop: 'code', label: T + '.code', minWidth: 120 },
{ prop: '_actions', label: T + '.operation', width: 160, fixed: 'right' }
])
// 按钮定义 —— 传 i18n key
const btns = useTableButtons({
toolbar: [{ key: 'add', label: T + '.add', icon: 'el-icon-plus', ... }],
row: [{ key: 'edit', label: T + '.edit', ... }]
})
// 表单定义 —— 传 i18n key
formCols: [
[{ type: 'input', prop: 'code', label: T + '.code', placeholder: T + '.enter_code' }]
]
// 模板中直接用 $t() 或 $t(tkey('key'))
methods: {
tkey (key) { return T + '.' + key }
}
<template>
<el-form-item :label="$t(tkey('code'))">
<el-input :placeholder="$t(tkey('enter_code'))" />
</el-form-item>
<el-button>{{ $t(tkey('search')) }}</el-button>
</template>
语言包文件:在 src/locales/zh-chs.json 和 src/locales/en.json 中按页面维护:
{
"page": {
"production_configuration": {
"factory_model": {
"factory_area": {
"search": "查询",
"code": "所区编码",
"name": "所区名称",
"add": "新 增",
"edit": "编 辑",
"delete": "删 除",
"operation_success": "操作成功"
}
}
}
}
}
语言包目录命名规则:
| 语言 | 文件名 |
|---|---|
| 简体中文 | src/locales/zh-chs.json |
| 繁体中文 | src/locales/zh-cht.json |
| 英文 | src/locales/en.json |
| 日文 | src/locales/ja.json |
注意:
page-table和page-dialog-form已内置$t()调用,页面传 i18n key 即可自动翻译。搜索区等其他 UI 文字需要用$t(tkey('...'))手动处理。