# 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('...'))` 手动处理。
