# ProTable 表格组件

基于 Element UI Table 的二次封装，参考 [Vben Admin BasicTable](https://doc.vvbin.cn/components/table.html)，对齐其 API 设计和使用体验，支持：

- 列配置（`ProColumn`）与多级表头
- API 请求 / 本地数据双模式
- 分页、选择列、序号列、操作列
- 跨分页选择、树形表格、合并单元格
- `useProTable` 编程式控制（`tableAction`）
- 组件默认配置（`useComponentSetting`）

---

## 基础用法（本地数据）

传入 `dataSource` 即为本地模式，数据由前端管理，不走接口请求。配合 `pagination` 使用时，分页变化会自动对全量数据做切片，每次只渲染当前页数据。

```vue
<template>
  <ProTable
    title="本地分页示例"
    :columns="columns"
    :data-source="data"
    :pagination="{ pageSize: 10 }"
  />
</template>

<script setup lang="ts">
const data = Array.from({ length: 100 }, (_, i) => ({
  id: i + 1,
  name: `用户${i + 1}`,
  age: 20 + (i % 50),
}))
</script>
```

### 本地筛选后重新注入

搜索/筛选后，用 `tableAction.setTableData` 重新注入过滤后的全量数据，`total` 会自动同步：

```ts
const handleSearch = (keyword: string) => {
  const filtered = allData.filter(item => item.name.includes(keyword))
  tableAction.setTableData(filtered)
}
```

---

## 列宽配置

`ProColumn.width` 支持三种形式：

| 类型 | 示例 | 说明 |
|------|------|------|
| **固定像素** | `width: 120` 或 `width: '120px'` | 精确指定列宽 |
| **比例分配** | `width: 1` / `width: 2` | 按比例均分可用宽度（见下方说明） |
| **最小宽度** | `minWidth: 200` | 列不撑满时保持该宽度，超出时自适应 |

### 比例宽度（number）

当 `width` 为 `number` 类型时，视为比例权重，会根据容器总宽度**自动计算**像素值：

```
某列像素宽度 = (容器宽度 - 固定列宽 - 固定数据列宽) × (该列 width / 所有比例列 width 总和)
```

- **固定列**包括：选择列（rowSelection）、序号列（showIndexColumn）、操作列（actionColumn）
- **固定数据列**包括：`width` 为 `string` 类型的列

```ts
const columns: ProColumn[] = [
  // 三列按 1:1:1 比例分配可用宽度
  { title: '区域', dataIndex: 'region', width: 1 },
  { title: '姓名', dataIndex: 'name', width: 1 },
  { title: '销量', dataIndex: 'sales', width: 1 },
]
```

> **提示**：比例宽度适合数据列均分场景；若希望列宽更宽，可使用更大的权重（如 `width: 100` 三列均分）。

### minWidth / maxWidth 约束

所有类型列均可通过 `minWidth` 和 `maxWidth` 设置边界：

```ts
// 最小宽度约束（比例列默认最小 60px）
{ title: '姓名', dataIndex: 'name', width: 1, minWidth: 100 }

// 最大宽度约束
{ title: '备注', dataIndex: 'remark', width: 1, maxWidth: 300 }
```

---

## API 请求数据

`api` 返回值约定为 `{ list, total }`（与 Vben Admin 一致）：

```vue
<template>
  <ProTable
    title="API 示例"
    :columns="columns"
    :api="fetchTableData"
    row-key="id"
    :pagination="{ pageSize: 10 }"
  />
</template>

<script setup lang="ts">
import { ProTable } from 'element-component-pro'
import type { ProColumn, FetchParams } from 'element-component-pro'

const columns: ProColumn[] = [
  { title: 'ID', dataIndex: 'id', width: 80 },
  { title: '姓名', dataIndex: 'name', width: 120 },
  { title: '年龄', dataIndex: 'age', width: 80 },
]

const fetchTableData = async (params: FetchParams & Record<string, unknown>) => {
  const res = await api.getUserList(params)
  return { list: res.list, total: res.total }
}
</script>
```

---

## useProTable（tableAction）

通过 `useProTable` 获取 `tableAction`，方便在外部控制表格行为。

### 不传 props

```vue
<template>
  <div>
    <el-button type="primary" size="small" @click="handleReload">刷新第一页</el-button>
    <ProTable
      @register="registerTable"
      title="useProTable 示例"
      :columns="columns"
      :api="fetchTableData"
      row-key="id"
      :row-selection="{ type: 'checkbox' }"
    />
  </div>
</template>

<script setup lang="ts">
import { ProTable, useProTable } from 'element-component-pro'
import type { ProColumn, FetchParams } from 'element-component-pro'

const columns: ProColumn[] = [
  { title: 'ID', dataIndex: 'id', width: 80 },
  { title: '姓名', dataIndex: 'name', width: 120 },
  { title: '年龄', dataIndex: 'age', width: 80 },
]

const [registerTable, tableAction] = useProTable()

const fetchTableData = async (params: FetchParams & Record<string, unknown>) => {
  const res = await api.getUserList(params)
  return { list: res.list, total: res.total }
}

const handleReload = () => tableAction.reload({ page: 1 })
</script>
```

### 传入 props（响应式同步）

```ts
// 方式一：静态 props，注册时一次性应用
const [registerTable, tableAction] = useProTable({ size: 'small', bordered: true })

// 方式二：响应式 props，后续修改会自动 setProps 到表格
const tableProps = ref({ size: 'small', bordered: true })
const [registerTable2, tableAction2] = useProTable(tableProps)
// 之后修改 tableProps.value.size = 'medium' 会自动同步到表格
```

### TableActionType 方法

| 方法 | 说明 |
|------|------|
| `setProps(props)` | 动态更新 ProTable Props |
| `reload(opt?)` | 重新请求数据，支持 `{ page, pageSize, searchInfo }` |
| `redoHeight()` | 重新计算表格高度/布局 |
| `setLoading(loading)` | 手动切换 loading |
| `getDataSource()` | 获取全量数据（本地模式为原始数据，API 模式为服务端返回的当前页） |
| `getRawDataSource()` | 获取接口原始返回数据 |
| `setTableData(data)` | 设置本地全量数据，自动同步 total |
| `getColumns()` / `setColumns(cols)` | 获取/设置列配置 |
| `setPagination(info)` | 设置分页（page / pageSize / total） |
| `getSelectRowKeys()` / `getSelectRows()` | 获取选中行 keys 或数据 |
| `setSelectedRowKeys(keys)` | 设置选中行 keys（从全量数据中匹配） |
| `clearSelectedRowKeys()` | 清空选中行 |
| `deleteSelectRowByKey(key)` | 取消选中某个 key |
| `updateTableData(index, key, value)` | 按当前页索引更新单格数据（自动转换为全量索引） |
| `updateTableDataRecord(rowKey, record)` | 按 rowKey 更新整行（全量数据中查找） |
| `deleteTableDataRecord(rowKey)` | 按 rowKey 删除指定行（全量数据），自动同步 total |
| `insertTableDataRecord(record, index?)` | 插入一行数据（全量数据），自动同步 total |
| `getPaginationRef()` | 获取当前分页信息 |
| `getShowPagination()` / `setShowPagination(show)` | 获取/设置是否显示分页 |
| `getRowSelection()` | 获取当前行选择配置 |
| `expandAll()` / `collapseAll()` | 展开/折叠树形表格所有行 |
| `scrollToColumn(column)` | 滚动到指定列，`column` 为 `dataIndex` 字符串或列索引数字，平滑滚动到目标列 |

---

## 多级表头

通过 `columns.children` 嵌套配置子列，实现分组表头：

```vue
<script setup lang="ts">
import type { ProColumn } from 'element-component-pro'

const columns: ProColumn[] = [
  {
    title: '基本信息',
    children: [
      { title: '姓名', dataIndex: 'name', width: 120, fixed: 'left' },
      { title: '年龄', dataIndex: 'age', width: 80, align: 'center' },
      { title: '性别', dataIndex: 'gender', width: 80, align: 'center' },
    ],
  },
  {
    title: '联系方式',
    children: [
      { title: '电话', dataIndex: 'phone', width: 150 },
      { title: '邮箱', dataIndex: 'email', minWidth: 200 },
    ],
  },
  {
    title: '地址',
    dataIndex: 'address',
    minWidth: 200,
  },
  {
    title: '状态',
    dataIndex: 'status',
    width: 100,
    align: 'center',
    valueEnum: { 1: { text: '在职' }, 0: { text: '离职' } },
  },
]
</script>
```

子列支持所有 `ProColumn` 配置（`width`、`minWidth`、`align`、`sortable`、`valueEnum` 等），支持任意层级嵌套。

---

## 合并单元格

通过 `spanMethod` 返回 `[rowspan, colspan]` 或 `{ rowspan, colspan }` 实现行/列合并，返回 `[0, 0]` 隐藏单元格：

```vue
<template>
  <ProTable
    title="合并示例"
    :columns="columns"
    :data-source="data"
    :span-method="spanMethod"
    :show-index-column="false"
  />
</template>

<script setup lang="ts">
import type { ProColumn } from 'element-component-pro'

const columns: ProColumn[] = [
  { title: '区域', dataIndex: 'region', width: 1 },
  { title: '姓名', dataIndex: 'name', width: 1 },
  { title: '销量', dataIndex: 'sales', width: 1 },
]

const data = [
  { id: 1, region: '华东', name: '张三', sales: 100 },
  { id: 2, region: '华东', name: '李四', sales: 80 },
  { id: 3, region: '华南', name: '王五', sales: 120 },
  { id: 4, region: '华南', name: '赵六', sales: 90 },
]

const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
  if (columnIndex === 0) {
    if (rowIndex === 0) return [2, 1]
    if (rowIndex === 1) return [0, 0]  // 隐藏
    if (rowIndex === 2) return [2, 1]
    if (rowIndex === 3) return [0, 0]  // 隐藏
  }
  return [1, 1]
}
</script>
```

`spanMethod` 回调参数（`column` 为原始 `ProColumn`）：

| 参数 | 说明 |
|------|------|
| `row` | 当前行数据 |
| `column` | 原始 `ProColumn` 配置 |
| `rowIndex` | 行索引（从 0 开始） |
| `columnIndex` | 列索引（从 0 开始，含选择列/序号列） |

---

## 跨分页选择

默认支持跨分页保留勾选（切换页码不清空）。如需切换页码时清空：

```vue
<ProTable
  :columns="columns"
  :api="fetchTableData"
  row-key="id"
  :row-selection="{ type: 'checkbox' }"
  :clear-select-on-page-change="true"
/>
```

通过 `tableAction` 获取已选 keys：

```ts
const selectedKeys = tableAction.getSelectRowKeys()
const selectedRows = tableAction.getSelectRows()
```

---

## 树形表格

通过 `treeProps` 配置树形数据，通过 `load` 实现懒加载：

```vue
<ProTable
  :columns="columns"
  :data-source="treeData"
  row-key="id"
  :tree-props="{ hasChildren: 'hasChildren', children: 'children' }"
  :default-expand-all="true"
/>
```

懒加载模式：

```vue
<ProTable
  :columns="columns"
  :data-source="treeData"
  row-key="id"
  :tree-props="{ hasChildren: 'hasChildren' }"
  :lazy="true"
  :load="loadChildren"
/>
```

```ts
const loadChildren = (row, treeNode, resolve) => {
  fetchChildren(row.id).then(res => resolve(res))
}
```

---

## 操作列 + TableAction

`TableAction` 用于渲染右侧操作列：

```vue
<template #bodyCell="{ column, row }">
  <template v-if="column.dataIndex === 'action'">
    <TableAction
      :actions="[
        { label: '编辑', onClick: () => handleEdit(row) },
        { label: '查看', type: 'text', onClick: () => handleView(row) },
      ]"
      :drop-down-actions="[
        {
          label: '删除',
          color: 'error',
          popConfirm: {
            title: '确认删除？',
            confirm: () => handleDelete(row),
          },
        },
      ]"
    />
  </template>
</template>
```

> 操作列也可使用 `#action` 插槽：`#action="{ row, record, column, index }"`

`TableActionItem` 支持字段：

| 字段 | 类型 | 说明 |
|------|------|------|
| `label` | `string` | 按钮文本 |
| `icon` | `string` | 图标 class |
| `color` | `'success' \| 'error' \| 'warning'` | 颜色，内部映射到按钮类型 |
| `type` | `string` | Element UI 按钮 type |
| `disabled` | `boolean` | 是否禁用 |
| `divider` | `boolean` | 是否显示垂直分隔线 |
| `ifShow` | `boolean \| (action) => boolean` | 是否显示 |
| `tooltip` | `string \| TooltipProps` | Tooltip 配置 |
| `popConfirm` | `{ title, okText?, confirm, cancel? }` | 气泡确认 |
| `onClick` | `(e: MouseEvent) => void` | 点击回调 |

---

## Slots

| 名称 | 说明 |
|------|------|
| `tableTitle` | 标题左侧区域 |
| `toolbar` | 工具栏区域 |
| `bodyCell` | 单元格渲染插槽：`{ column, row, record, index, value }`；返回空内容时自动回退到默认渲染 |
| `action` | 操作列插槽：`{ row, record, column, index }` |
| `[dataIndex]` | 以 `dataIndex` 命名的单元格插槽：`{ row, column, index, value }`；优先级高于 `bodyCell` |
| `header-[dataIndex]` | 表头单元格插槽：`{ column }` |

> **行数据命名**：所有涉及行数据的插槽均同时提供 `row` 和 `record` 两种命名方式，完全兼容。

---

## Events

| 事件名 | 参数 | 说明 |
|--------|------|------|
| `fetch-success` | `{ items, total }` | 接口请求成功 |
| `fetch-error` | `error` | 接口请求失败 |
| `selection-change` | `{ keys, rows }` | 勾选变化 |
| `row-click` | `(record, event)` | 行点击 |
| `row-dblclick` | `(record, event)` | 行双击 |
| `sort-change` | `{ prop, order }` | 排序变化 |
| `expand-change` | `(row, expanded)` | 展开行变化（树形表格） |

---

## ProTable Props

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `columns` | `ProColumn[]` | - | 列配置 |
| `dataSource` | `Record<string, unknown>[]` | - | 本地数据源 |
| `api` | `(params) => Promise<{ list, total }>` | - | 请求函数 |
| `rowKey` | `string` | `'id'` | 行主键字段 |
| `title` | `string` | - | 表格标题 |
| `titleHelpMessage` | `string \| string[]` | - | 标题右侧帮助文案 |
| `bordered` | `boolean` | `false` | 是否显示边框 |
| `striped` | `boolean` | `true` | 是否显示斑马纹 |
| `size` | `'medium' \| 'small' \| 'large'` | `'medium'` | 表格尺寸 |
| `loading` | `boolean` | `false` | 手动控制 loading |
| `maxHeight` | `number \| string` | - | 表格最大高度 |
| `height` | `number \| string` | - | 表格高度 |
| `ellipsis` | `boolean` | `true` | 文本超出时省略并显示 tooltip |
| `showIndexColumn` | `boolean` | `true` | 是否显示序号列 |
| `indexColumnProps` | `Partial<ProColumn>` | - | 序号列配置 |
| `actionColumn` | `Partial<ProColumn>` | - | 操作列配置 |
| `rowSelection` | `{ type?, width?, fixed?, getCheckboxProps?, getRadioProps? }` | - | 选择列配置 |
| `clearSelectOnPageChange` | `boolean` | `false` | 切换页码时清空勾选 |
| `pagination` | `false \| object` | `{ pageSize: 10 }` | 分页配置，`false` 不显示 |
| `fetchSetting` | `FetchSetting` | 见下方 | 接口字段映射 |
| `beforeFetch` | `(params) => params` | - | 请求前处理参数 |
| `afterFetch` | `(data) => data` | - | 请求后处理返回值 |
| `immediate` | `boolean` | `true` | 挂载后自动请求（有 api 时） |
| `searchInfo` | `Record<string, unknown>` | - | 额外请求参数 |
| `spanMethod` | `(params) => [rs, cs] \| { rowspan, colspan }` | - | 合并单元格 |
| `treeProps` | `TreeProps` | - | 树形表格配置 |
| `defaultExpandAll` | `boolean` | `false` | 默认展开所有树节点 |
| `expandRowKeys` | `(string \| number)[]` | - | 默认展开的行 |
| `lazy` | `boolean` | `false` | 是否懒加载子节点 |
| `load` | `(row, treeNode, resolve) => void` | - | 懒加载函数 |

`fetchSetting` 默认值：

```ts
{
  pageField: 'page',
  sizeField: 'pageSize',
  listField: 'list',
  totalField: 'total',
}
```

---

## ProColumn

| 字段 | 类型 | 说明 |
|------|------|------|
| `title` | `string` | 列标题 |
| `dataIndex` | `string` | 对应数据字段名 |
| `key` | `string` | 列唯一标识 |
| `width` | `number \| string` | 列宽：`number` 为比例，`string` 为 px |
| `minWidth` | `number \| string` | 最小宽度 |
| `maxWidth` | `number \| string` | 最大宽度 |
| `fixed` | `'left' \| 'right'` | 固定列 |
| `sortable` | `boolean` | 是否可排序 |
| `align` | `'left' \| 'center' \| 'right'` | 对齐方式 |
| `resizable` | `boolean` | 是否可调整宽度 |
| `ellipsis` | `boolean` | 是否启用省略，默认继承表格级 |
| `hideInTable` | `boolean` | 是否在表格中隐藏 |
| `defaultHidden` | `boolean` | 默认隐藏，可通过列设置显示 |
| `helpMessage` | `string \| string[]` | 列头右侧帮助文案 |
| `valueType` | `string` | 值类型（text / date / dateTime / option / select / index） |
| `valueEnum` | `Record` | 枚举值映射 |
| `customRender` | `({ text, record, index }) => VNode \| string` | 自定义渲染 |
| `formatter` | `(row, column, cellValue) => string` | Element UI formatter |
| `ifShow` | `boolean \| ({ column }) => boolean` | 是否显示当前列 |
| `children` | `ProColumn[]` | 子列配置（多级表头） |

---

## 组件默认配置（useComponentSetting）

通过 `useComponentSetting` 统一设置组件默认 props：

```ts
import { useComponentSetting } from 'element-component-pro'

const { getSetting, setSetting } = useComponentSetting()

setSetting('ProTable', { size: 'small', bordered: true })
setSetting('ProForm', { labelWidth: '140px', size: 'small' })

const tableDefaults = getSetting('ProTable')   // 获取 ProTable 默认配置
const all = getSetting()                        // 获取全部配置
```

详见 [ComponentSetting.md](./ComponentSetting.md)。

---

## 示例

```bash
npm install
npm run dev
```

访问 `/?tab=basic` 等路由参数切换示例：

| 参数 | 说明 |
|------|------|
| `?tab=basic` | 基础表格 |
| `?tab=tableForm` | ProTable + ProForm 对比表 |
| `?tab=tableFormCombo` | ProTable + ProForm 同页组合 |
| `?tab=remote` | 远程数据加载 |
| `?tab=custom` | 自定义渲染 |
| `?tab=action` | 带操作列 |
| `?tab=useProTable` | useProTable 编程控制 |
| `?tab=span` | 合并单元格 |
| `?tab=tree` | 树形表格 |
| `?tab=crossPage` | 跨分页选择 |
| `?tab=groupHeader` | 多级表头 |
| `?tab=componentSetting` | 组件默认配置 |
| `?tab=scrollToColumn` | 滚动到指定列 |
