Files
mes-ui-d2/docs/sct-base-table-refactor-design.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

1242 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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.

# sct-base-table 组件架构重构方案(方案 B
> 设计人:前端团队
> 日期2026-05-26
> 状态待评审RFC
---
## 目录
1. [背景与现状](#1-背景与现状)
2. [方案总览](#2-方案总览)
3. [架构对照:旧 vs 新](#3-架构对照旧-vs-新)
4. [新组件详细设计](#4-新组件详细设计)
5. [页面迁移对照示例](#5-页面迁移对照示例)
6. [文件清单](#6-文件清单)
7. [风险与回滚策略](#7-风险与回滚策略)
8. [工时估算](#8-工时估算)
---
## 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` 等。
**示例:**
```vue
<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 项"文字 |
**按钮定义结构:**
```js
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,
},
]
```
**示例:**
```vue
<sct-toolbar
:buttons="toolbarButtons"
:selected-count="selectedRows.length"
/>
```
---
### 4.3 `sct-action-buttons` — 行内操作按钮
**定位**:表格每行末尾的"编辑/删除/查看"等操作按钮。
**Props**
| Prop | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| row | Object | — | 当前行数据 |
| buttons | Array | `[]` | 按钮列表 |
| maxVisible | Number | 3 | 超过此数量折叠到"更多"下拉菜单 |
**按钮定义结构:**
```js
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 布局展示的详情信息。
```vue
<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` 基础上内置了工具栏和分页。
```vue
<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)`
```js
// 旧方式:每个页面手写
this.columns = [
{ idx: 0, attrs: { prop: 'sort', label: '序号' } },
{ idx: 1, attrs: { prop: 'code', label: '编码' } },
]
```
```js
// 新方式声明列定义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(...)`
```js
// 生成 toolbar 按钮 + row 按钮,自动注入权限判断
const { toolbarButtons, rowButtons } = useTableButtons({
toolbar: [
{ key: 'add', label: '新增', ... },
],
row: [
{ key: 'edit', label: '编辑', ... },
],
}, permissionCheck)
```
#### `usePagination()`
```js
const { pagination, onPageChange, resetPage } = usePagination({
defaultPageSize: 20,
})
```
#### `usePageTable(fetchFn)`
```js
// 封装表格数据 + 分页 + loading + 刷新
const { data, loading, pagination, refresh, search } = usePageTable(fetchList)
```
---
## 5. 页面迁移对照示例
### 5.1 标准 CRUD 页面(产线管理)
> 源文件:`views/production_configuration/factory_model/factory_line/components/PageMain/index.vue`
#### 旧代码(简化)
```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` 便捷组合体)
```vue
<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`
#### 新代码(使用底层组件自由组合)
```vue
<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 带展开行的页面(库位管理)
```vue
<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 映射
```js
// 旧 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 结构变化
```js
// 旧 —— 命令式
{ 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` 声明按钮:
```js
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>`
```vue
<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 / 增删改),和旧代码完全一致:
```js
methods: {
async fetchData () {
this.loading = true
try {
const res = await getList({ ...this.search, page_no: this.pagination.current, page_size: this.pagination.size })
this.tableData = res.data || []
this.pagination.total = res.count || 0
} finally { this.loading = false }
},
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`
```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**
```vue
<page-table :columns="columns" :data="tableData">
<template #col-status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</page-table>
```
列定义中加入 `slot: true`
```js
useTableColumns([
{ prop: 'status', label: '状态', slot: true, width: 100 },
])
```
**场景 2工具栏追加自定义内容**
```vue
<page-table ...>
<template #toolbar-extra>
<el-button size="mini" icon="el-icon-printer" @click="print">打印</el-button>
</template>
</page-table>
```
**场景 3序号列**
```js
useTableColumns(
[{ prop: 'code', label: '编码' }],
{ selectionWidth: 55, indexWidth: 60 } // 开启复选框 + 序号列
)
```
### 路由配置
```js
// 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()` 会原样返回,不会报错。
**页面层用法**
```js
// 约定:每个页面定义一个 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 }
}
```
```vue
<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` 中按页面维护:
```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('...'))` 手动处理。