# Table

表格组件，用于展示结构化数据。支持固定列、固定表头、排序、合并单元格、树状数据、操作列、屏幕粘连（吸顶吸底）等能力。

## 适用场景

- 数据列表展示（人员列表、订单列表等）
- 需要固定列头或固定左/右列的大数据表格
- 需要选择列、开关列、操作列的数据表格
- 树状层级数据展示
- 支持内排序或外排序（通过回调交由外部处理）
- 长页面中需要表头吸顶、横向滚动条吸底的大表格（`sticky`）

## Props

### Table Props

| 名称 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| className | `string` | `""` | 否 | 自定义 class |
| size | `"sm" \| "md"` | `-` | 否 | 尺寸 |
| lineSpacing | `"compact" \| "regular"` | `"compact"` | 否 | 行高空间 |
| columns | `TableCol<TableRow>[]` | `-` | 否 | 数据列定义 |
| rowId | `string \| ((row: TableRow, idx: number) => string)` | `"id"` | 否 | 数据行的主键字段，用以唯一标识数据行 |
| rowTrProps | `(row: TableRow, idx: number) => HTMLAttributes & Record<string, unknown>` | `-` | 否 | 行 tr 的 props |
| data | `TableRow[]` | `-` | 否 | 源数据 |
| loading | `boolean` | `-` | 否 | 是否处于 loading 状态 |
| maxBodyHeight | `number` | `-` | 否 | 指定 table body 高度以固定列头滚动 |
| headRowLines | `number` | `1` | 否 | 表头每行文字行数。1 为单行 ellipsis，n>1 固定 N 行 ellipsis |
| bodyRowLines | `number` | `1` | 否 | 表体每行文字行数。1 为单行 ellipsis，n>1 固定 N 行，0 为自由高度 |
| emptyCellContent | `string` | `-` | 否 | 空内容单元格的内容 |
| bordered | `boolean` | `true` | 否 | 是否显示列表框 |
| thousandSeparator | `string` | `","` | 否 | 所有右对齐纯数字自动添加的千分位符，设为 `''` 可禁用 |
| sticky | `boolean \| { top?: number; bottom?: number; scrollTarget?: RefObject<HTMLElement> }` | `-` | 否 | 屏幕粘连模式：随页面（或指定滚动容器）滚动时表头吸顶、横向滚动条吸底。`true` 偏移均为 0 相对页面；`{top, bottom}` 分别指定吸顶/吸底偏移；`{scrollTarget}` 相对传入的滚动容器计算 |
| noDataType | `"search" \| "success" \| "empty" \| "error"` | `"empty"` | 否 | 无数据类型，控制无数据插图 |
| noDataMessage | `string` | `-` | 否 | 无数据提示信息 |
| illusSize | `"md" \| "lg"` | `"md"` | 否 | 插图尺寸 |
| expandedIds | `string[]` | `-` | 否 | 树状表格展开行的 id 列表控制值 |
| defaultExpandedIds | `string[]` | `-` | 否 | 树状表格展开行的 id 列表默认值 |
| onChangeExpandedIds | `(expandedIds: string[]) => void` | `-` | 否 | 树状表格展开行 id 列表 change 回调 |
| hover | `boolean` | `true` | 否 | 是否启用 hover 行高亮 |
| sortMultiple | `boolean` | `false` | 否 | 排序是否为多列 |
| onSort | `(sorts: SortItem[]) => void` | `-` | 否 | 可排序列点击排序时的回调（Table 外排序） |

### TableCol Props

| 名称 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| key | `string \| string[]` | `-` | 是 | 列的主键，可对应数据行的字段名 |
| thContent | `ReactNode` | `-` | 否 | 列头的内容 |
| thTitle | `string` | `-` | 否 | 列头的 title tip |
| tdContent | `(row: TRow, rowIdx: number) => ReactNode` | `-` | 否 | 列的行内容，不指定则默认为 `row[key]` |
| tdTitle | `(row: TRow, rowIdx: number) => string` | `-` | 否 | 列的行内容 title tip |
| tdProps | `(row: TRow, rowIdx: number) => TdProps` | `-` | 否 | 此列 td 的 props（含 rowSpan/colSpan） |
| fixed | `"left" \| "right"` | `-` | 否 | 固定列于左/右，只在第一层 columns 中指定 |
| serial | `(row: TRow, rowIdx: number) => ReactNode` | `-` | 否 | 作为序号列的单元格内容 |
| checkbox | `(row: TRow, rowIdx: number) => CheckboxProps \| ReactElement` | `-` | 否 | 作为选择列的 checkbox props |
| switch | `(row: TRow, rowIdx: number) => SwitchProps \| ReactElement` | `-` | 否 | 作为开关列的 switch props |
| operations | `(row: TRow, rowIdx: number) => (OperationProps \| ReactElement)[]` | `-` | 否 | 作为操作列的 button 列表 |
| operationsEllipsisMoreThan | `number` | `3` | 否 | 操作个数超过 N 时收起到"更多" |
| operationsEllipsisMoreTriggerType | `"hover" \| "click"` | `"hover"` | 否 | "更多"下拉的触发方式 |
| align | `"left" \| "right" \| "center"` | `-` | 否 | 横向对齐方式 |
| width | `number` | `-` | 否 | 列宽，只在叶子列中指定 |
| children | `TableCol<TableRow>[]` | `-` | 否 | 合并列子列（表头分组） |
| thousandSeparator | `string` | `-` | 否 | 该列千分位字符，不定义则使用 props.thousandSeparator |
| colSpan | `number` | `-` | 否 | 表头合并列，只支持非 fixed head 情景下的 root columns |
| compare | `(a: TableRow, b: TableRow) => number` | `-` | 否 | 排序比较函数，用于"Table 内排序" |
| sortable | `boolean` | `-` | 否 | 是否为排序列，用于"Table 外排序" |

### TableRow Props

| 名称 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| children | `TableRow[]` | `-` | 否 | 树状数据子行 |

### TdProps Props

| 名称 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| className | `string` | `-` | 否 | 自定义 class |
| colSpan | `number` | `-` | 否 | 单元格横向合并数 |
| rowSpan | `number` | `-` | 否 | 单元格纵向合并数 |

### OperationProps Props

| 名称 | 类型 | 默认值 | 必填 | 说明 |
|------|------|--------|------|------|
| key | `string` | `-` | 否 | 操作项唯一标识 |
| label | `string` | `-` | 否 | 操作按钮文字 |
| dropList | `(OperationProps \| ReactElement)[]` | `-` | 否 | 下拉列表 |
| popoverConfirmProps | `PopoverConfirmProps` | `-` | 否 | 点击确认弹窗配置 |
| popoverProps | `PopoverProps` | `-` | 否 | 弹窗配置 |
| className | `string` | `-` | 否 | 自定义 class |
| size | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `-` | 否 | 尺寸 |
| disabled | `boolean` | `-` | 否 | 是否禁用 |
| type | `"normal"` | `-` | 否 | 类型 |
| color | `"normal" \| "brand" \| "success" \| "danger" \| "warning" \| "primary"` | `-` | 否 | 颜色，`primary` 已废弃请改用 `brand` |
| icon | `SvgFC` | `-` | 否 | 前缀 icon |
| loading | `boolean` | `-` | 否 | 是否处于 loading 状态 |
| loadingIcon | `SvgFC` | `-` | 否 | 自定义 loading Icon |
| loadingType | `"normal" \| "icon-only"` | `-` | 否 | loading 类型 |
| loadingDelay | `number` | `-` | 否 | loading 图标延迟响应时间（ms） |
| href | `string` | `-` | 否 | 链接跳转地址 |
| target | `string` | `-` | 否 | 链接跳转 target |
| download | `string` | `-` | 否 | `<a />` download 属性 |
| onClick | `(e: MouseEvent) => void \| Promise` | `-` | 否 | 点击回调，返回 Promise 自动进入 loading 状态 |
| refHTMLElement | `Ref<HTMLButtonElement \| HTMLAnchorElement>` | `-` | 否 | ref html element |
| selected | `boolean` | `-` | 否 | 是否选中 |
| indeterminate | `boolean` | `-` | 否 | 是否部分选中 |
| suffix | `ReactNode` | `-` | 否 | 后缀内容 |
| suffixArrow | `boolean \| SvgFC` | `-` | 否 | 后缀箭头 |
| suffixLoading | `boolean \| SvgFC` | `-` | 否 | 后缀 loading |

## 典型用法

### 基础用法（带选择列、开关列、操作列）

```tsx
import { Table, TableCol } from '@befe/brick'
import { Checkbox } from '@befe/brick-comp-checkbox'

interface RowData {
    personId: string
    fullName: string
    emailAddress: string
}

const columns: Array<TableCol<RowData>> = [
    {
        key: 'checkbox',
        thContent: <Checkbox disabled />,
        checkbox: (row, rowIdx) => ({
            onChange: (e) => console.log(e.target.checked, row, rowIdx),
        }),
    },
    { thContent: '姓名', key: 'fullName' },
    {
        thContent: '邮箱',
        key: 'emailAddress',
        tdContent: (row) => `${row.emailAddress} ^_^`,
    },
    {
        thContent: '状态',
        key: 'status',
        switch: (row, rowIdx) => ({
            onChange: (checked) => console.log(`change switch ${rowIdx}`, checked),
        }),
    },
    {
        key: 'operations',
        thContent: '操作',
        width: 156,
        operations: (row, rowIdx) => [
            { label: '编辑', onClick: () => console.log('edit', rowIdx) },
            {
                label: '删除',
                onClick: () => console.log('delete', rowIdx),
                popoverConfirmProps: {
                    message: '是否确认删除？',
                    onConfirm: () => console.log('confirm'),
                },
            },
        ],
    },
]

function Demo() {
    return <Table rowId="personId" columns={columns} data={data} />
}
```

### 固定列

```tsx
const columns = [
    { key: 'fullName', thContent: '姓名', fixed: 'left', width: 150 },
    { key: 'emailAddress', thContent: '邮箱' },
    { key: 'operations', thContent: '操作', fixed: 'right', width: 108, operations: ... },
]

function Demo() {
    return <Table rowId="personId" columns={columns} data={data} />
}
```

### 固定表头

> `props.maxBodyHeight` 指定高度以固定表头。对于 n 列表格，须指定 n-1 列的 `col.width`，留 1 列自由以吸收剩余宽度。

```tsx
function Demo() {
    return <Table rowId="personId" columns={columns} data={data} maxBodyHeight={280} />
}
```

### 排序（Table 内排序）

```tsx
const columns = [
    { key: 'name', thContent: '姓名', compare: (a, b) => a.name > b.name ? 1 : -1 },
    { key: 'score', thContent: '得分', compare: (a, b) => a.score - b.score },
]

function Demo() {
    return (
        <div>
            <Table columns={columns} data={data} />
            <Table columns={columns} data={data} sortMultiple />
        </div>
    )
}
```

### 树状数据

> 行数据嵌套 `children` 子行可启用树状表。

```tsx
import { useState } from 'react'
import { Table } from '@befe/brick'

function Demo() {
    const [expandedIds, setExpandedIds] = useState([])

    return (
        <Table
            columns={columns}
            data={treeData}
            expandedIds={expandedIds}
            onChangeExpandedIds={setExpandedIds}
        />
    )
}
```

### 屏幕粘连（sticky 吸顶吸底）

> `props.sticky` 开启后随页面（或指定滚动容器）滚动：表头吸附顶部、横向滚动条吸附底部。`true` 相对页面且偏移为 0；`{top, bottom}` 指定偏移（如页面有固定顶栏时给 `top`）；`{scrollTarget}` 用于表格处于固定高度内层滚动容器时相对该容器计算。左右固定列在吸顶浮层中同样生效。仅非 IE 浏览器启用。

```tsx
function Demo() {
    return <Table rowId="personId" columns={columns} data={data} sticky />
}
```

### 边缘滚动阴影提示

> 无固定列的一侧横向可继续滚动时，边缘用与固定列同款 inset 阴影暗示"该方向还有内容"，解决 overlay 滚动条静止态不可见、用户不知可滚的问题。左右到底则对应侧不出阴影；某侧有 `col.fixed` 时走既有固定列阴影，不重复补。`maxBodyHeight` 时横向阴影照常，纵向在底部出阴影暗示下方还有行。无需任何配置，满足条件自动生效。

```tsx
function Demo() {
    // 列总宽超出容器即自动出现边缘阴影
    return <Table rowId="personId" columns={wideColumns} data={data} />
}
```

## 注意事项

- `col.fixed` 只能设置在第一层 columns，`col.width` 只在叶子列中指定
- 使用 `maxBodyHeight` 固定表头时，须指定 n-1 列的 `col.width`，避免 head 与 body 列宽错位
- `bodyRowLines` 设为 `0` 为自由高度，不支持固定列情景下使用
- 操作列应使用 `col.operations` 而非 `tdContent: () => <Button />`，因为 `operations` 有内置的行高修正
- 链接内容应使用 `<Link />` 而非 `<Button type="plain" />`，`<Link />` 与文字行高一致
- `col.compare` 与 `col.sortable` 不可混用：只要任意列指定了 `col.sortable`，所有 `col.compare` 均失效
- 跨行合并单元格场景下建议关闭 `hover` 以避免高亮不一致
- `size` 去掉了 `xs`/`lg`，增加了 `lineSpacing` 行高维度；`OperationProps.loadingDelayInMS` 已废弃，改用 `loadingDelay`
- `sticky` 仅非 IE 浏览器启用（IE 下自动降级为不生效）；表格嵌在固定高度滚动容器内时须传 `sticky.scrollTarget`，否则吸顶吸底会相对页面而错位
- 边缘滚动阴影自动生效、无需配置：某侧有 `col.fixed` 走既有固定列阴影，无固定列侧才补边缘阴影
