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

40 KiB
Raw Blame History

sct-base-table 组件架构重构方案(方案 B

设计人:前端团队
日期2026-05-26
状态待评审RFC


目录

  1. 背景与现状
  2. 方案总览
  3. 架构对照:旧 vs 新
  4. 新组件详细设计
  5. 页面迁移对照示例
  6. 文件清单
  7. 风险与回滚策略
  8. 工时估算

1. 背景与现状

1.1 当前使用规模

src/components/sct-base-table/index.vue 是一个基于 el-table 的二次封装组件,在旧项目中被 100+ 个页面引用,是使用频率最高的公共组件。

1.2 现有问题

# 问题 严重程度 说明
1 引入了 yargsNode.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-dialogsct-base-formsct-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-tablepage-dialog-form 对所有 labelplaceholdertitle 等文本属性自动调用 $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.jsonsrc/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-tablepage-dialog-form 已内置 $t() 调用,页面传 i18n key 即可自动翻译。搜索区等其他 UI 文字需要用 $t(tkey('...')) 手动处理。