# 表格

## 使用方法

`Table`（数据表格）用来以行和列的方式整整齐齐地展示结构化数据。只要传入行数据 `data` 和列配置 `columns` 就能快速渲染出表格。每一列都要有唯一标识 `key`、表头文字 `header`，也可以用 `proportional()` 或 `pixel()` 来分配宽度。单元格默认直接显示数据字段的值，需要自定义内容时可以通过 `renderCell` 渲染徽标、链接或按钮。`idKey` 用来区分每一行的唯一身份，默认取数据里的 `id`。

**什么时候用：**
- 需要对比多条结构相似的数据（例如订单列表、用户清单、财务账目），或者需要对数据进行排序、筛选、分页和多选时。

**什么时候不要用：**
- 数据结构不规则，或者更适合用卡片呈现视觉封面时，请改用网格或卡片列表。
- 在屏幕极窄的手机端，普通大表格容易被挤压，可以考虑使用精简列表或折叠详情面板。

常用的高级功能可以通过配套的函数插件（`plugins`）按需开启：

- `useTableSortable`：让用户点击表头在升序、降序和恢复默认之间切换。开启 `isMultiSortEnabled` 后，按住 Shift 键再点表头可以同时按多个列排序。搭配 `useTableSortableState` 保存排序规则并返回排好序的数据。
- `useTableSelection`：在每行最左侧添加复选框，点击可以选中或高亮当前行。搭配 `useTableSelectionState` 保存选中的行 ID。选中之后把返回的 `selectionState` 交给 `TableSelectionToolbar`，就可以对选中项做批量操作；一个都没选中时这条不出现。周围已经用行底色表示别的含义时，把 `hasRowHighlight` 设为 `false`，选中行不再铺底色，读屏仍能读到已选中。
- `useTablePagination`：在表格底部添加分页器。在传给表格前，用 `paginateData` 把总数据切出当前页的内容。
- `useTableColumnSettings`：让用户自己勾选显示或隐藏哪些列，带有 `isAlwaysVisible` 的列会固定保留。
- `useTableColumnResize`：允许用户拖动表头边缘自由调整列宽，最小宽度为 60px。
- `useTableFiltering`：为指定列开启文字模糊搜索或选项精确匹配。搭配 `applyTableFilters` 过滤出符合条件的行。
- `useTableGroupedRows`：把已经排好序的数据按某个分类整理成分组折叠行。
- `useTableTreeState` 与 `useTableTreeData`：支持像文件夹树一样展开或折叠带有层级关系的子行。
- `useTableRowExpansion`：允许点击行首箭头在某一行下方展开一整块详情面板。打开 `hasRowClickExpansion` 后点整行也能展开；点到按钮、链接、可编辑文字或已经选中的文字时不会切换。详情和第一列对齐，不把面板算进斑马纹。
- `useTableStickyColumns`：当表格很宽需要横向滚动时，把左侧或右侧的指定列钉住固定不动。
- `useTableRowStatus`：在行首显示成功、警告或错误等状态标记圆点，鼠标悬停时还可以看到文字解释。
- `useTableRowIndex`：自动为真正的数据行生成连续的序号。

没写宽度的列至少留 60px。写在表格上的 `style.minWidth` 如果比各列下限的总和还小，表格会抬到这个下限，避免列被压到读不清；写得更大时保留你给的值。表格放得下时，滚轮继续交给外面的页面；横向溢出之后才拦住回弹。

表格的显示风格也很灵活：`density` 可以调节行高间距（紧凑 `compact`、适中 `balanced` 或宽松 `spacious`）；`dividers` 控制网格线的样式（仅行线 `rows`、仅列线 `columns`、全网格 `grid` 或无边框 `none`）；`isStriped` 开启隔行换色的斑马纹，`hasHover` 开启鼠标悬停高亮。当 `data` 为空时，表格会自动显示贴心的缺省提示，也可以通过 `empty` 插槽换成自己的图文。

如果不需要配置式的驱动，也可以直接使用 `TableHeader`、`TableBody`、`TableFooter`、`TableRow`、`TableHeaderCell` 和 `TableCell` 这些原子组件像写普通 HTML 表格一样自由拼接。

```vue
<script setup lang="ts">
import { proportional, Table } from '@astryx-vue/core/Table'

const rows = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com' },
]
const columns = [
  { key: 'name', header: 'Name', width: proportional(1) },
  { key: 'email', header: 'Email', width: proportional(2) },
]
</script>

<template>
  <Table :data="rows" :columns="columns" has-hover />
</template>
```

如果开启了列过滤，状态要用函数（getter）的形式传进去，表格才能敏锐察觉到过滤词的变化：

```ts
const { filters, onFilterChange } = useTableFilterState()
const filterPlugin = useTableFiltering(() => ({
  filters: filters.value,
  onFilterChange,
  fields,
}))
```

`rowCount` 和 `rowIndexStart` 用来告诉读屏软件当前这一页数据在全表里的位置。两者只要填了一个，读屏就能准确播报当前行号。分组的表头和展开的详情面板不会占用行号。

## 建议

| 建议 | 做法 |
| --- | --- |
| 推荐 | 每一行给一个稳定的 `idKey`。选择和行身份都靠它。 |
| 推荐 | 短的数字列用 `pixel()`，需要分剩余宽度的文字列用 `proportional()`。 |
| 推荐 | 排序、选择和过滤的状态放在表格外面，把算好的行放进 `data`。 |
| 避免 | 在 `renderCell` 里排序或分页。单元格只画交给它的那一行。 |
| 避免 | 一张表把每个插件都打开。又密又要过滤、拖宽度、还要多选，通常太挤。 |

### 结构

- **表格**: 把表格分区、行和单元格组在一起的语义 table 元素。
- **滚动区**: 横向滚动的外层。只有列溢出时才进入键盘顺序，并且只在那时限制过度滚动。
- **表头区**: 有数据驱动的列时生成的列标题区。用插槽组合表格时，由 TableHeader 提供。
- **列标题单元格**: 标识一列的单元格，可以装着排序或批量选择控件。
- **排序控件**: 包住可排序列的标签，并改变该列排序方向的按钮。
- **排序指示符号**: 排序控件里由 Icon 画出的方向符号。
- **排序优先级**: 多列排序打开时，已排序列上显示的数字。
- **选择控件**: 选择插件在表头和可选正文行里渲染的 CheckboxInput。
- **正文区**: 装着数据行或当前空状态的分区。数据驱动模式会自动渲染它。
- **行**: 重复的 TableRow，把标准表头、正文或表尾行里的单元格组在一起。
- **单元格**: TableCell。在标准正文行或表脚行里，装着一个值或使用方提供的内容。
- **默认空状态**: 数据数组为空时显示的紧凑 EmptyState，除非被替换或关掉。
- **展开控件**: 前导单元格里的按钮，用来展开或收起一行可展开的行。
- **展开符号**: 展开控件里由 Icon 画出的方向符号。
- **展开详情面板**: 展开行下方的详情行和跨列单元格，包着使用方提供的内容。
- **表脚区**: 用插槽组合表格时，由 TableFooter 提供的可选汇总或合计分区。

## 示例

### Table — 列设置

工具栏里选择要显示的列。可以开关，也可以调整顺序由当前选中项决定。

```vue
<script setup lang="ts">
import type { ColumnSettingsOption, TableColumn } from '@astryx-vue/core/Table'
import { MultiSelector } from '@astryx-vue/core/MultiSelector'
import { Stack } from '@astryx-vue/core/Stack'
import { proportional, Table, useTableColumnSettings, useTableColumnSettingsState } from '@astryx-vue/core/Table'
import { Text } from '@astryx-vue/core/Text'
import { Toolbar } from '@astryx-vue/core/Toolbar'
import { ref } from 'vue'

interface User extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
  department: string
  status: string
}

const users: User[] = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer', department: 'Platform', status: 'Active' },
  { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer', department: 'Product', status: 'Active' },
  { id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'Manager', department: 'Platform', status: 'Away' },
  { id: '4', name: 'Diana Prince', email: 'diana@example.com', role: 'Engineer', department: 'Infra', status: 'Active' },
  { id: '5', name: 'Eve Davis', email: 'eve@example.com', role: 'Admin', department: 'Operations', status: 'Inactive' },
]

const allColumns: TableColumn<User>[] = [
  { key: 'name', header: 'Name', width: proportional(1) },
  { key: 'email', header: 'Email', width: proportional(2) },
  { key: 'role', header: 'Role', width: proportional(1) },
  { key: 'department', header: 'Department', width: proportional(1) },
  { key: 'status', header: 'Status', width: proportional(1) },
]

const columnOptions: ColumnSettingsOption<string>[] = [
  { key: 'name', label: 'Name', isAlwaysVisible: true },
  { key: 'email', label: 'Email' },
  { key: 'role', label: 'Role' },
  { key: 'department', label: 'Department' },
  { key: 'status', label: 'Status' },
]

const activeKeys = ref(columnOptions.map(column => column.key))
const state = useTableColumnSettingsState<string>({
  columns: columnOptions,
  activeColumnKeys: activeKeys,
  onChangeActiveColumnKeys(keys) {
    activeKeys.value = [...keys]
  },
})
const plugin = useTableColumnSettings<User>(state.columnSettingsConfig)
const selectorOptions = columnOptions.map(column => ({
  value: column.key,
  label: column.label,
  disabled: column.isAlwaysVisible === true,
}))

function onColumnsChange(keys: (string | number)[]) {
  state.setActiveColumnKeys(keys.map(String))
}
</script>

<template>
  <Stack direction="vertical" :gap="3" width="100%">
    <Toolbar label="Table actions">
      <template #start>
        <Text type="label">
          Team
        </Text>
      </template>
      <template #end>
        <MultiSelector
          label="Columns"
          label-hidden
          :options="selectorOptions"
          :model-value="[...state.activeColumnKeys]"
          @update:model-value="onColumnsChange"
        />
      </template>
    </Toolbar>
    <Table
      :data="users"
      :columns="allColumns"
      id-key="id"
      has-hover
      :plugins="{ columnSettings: plugin }"
    />
  </Stack>
</template>
```

### Table — 弹出过滤

点表头上的图标，在弹出层里过滤。

```vue
<script setup lang="ts">
import type { TableColumn, TableFilterField } from '@astryx-vue/core/Table'
import { applyTableFilters, proportional, Table, useTableFiltering, useTableFilterState } from '@astryx-vue/core/Table'
import { computed } from 'vue'

interface Employee extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
  department: string
}

const employees: Employee[] = [
  { id: '1', name: 'Alice', email: 'alice@example.com', role: 'Engineer', department: 'Platform' },
  { id: '2', name: 'Bob', email: 'bob@example.com', role: 'Designer', department: 'Product' },
  { id: '3', name: 'Charlie', email: 'charlie@example.com', role: 'Manager', department: 'Platform' },
  { id: '4', name: 'Diana', email: 'diana@example.com', role: 'Engineer', department: 'Infrastructure' },
  { id: '5', name: 'Eve', email: 'eve@example.com', role: 'Admin', department: 'Operations' },
]

const fields: TableFilterField[] = [
  { key: 'name', type: 'string', label: 'Name' },
  { key: 'email', type: 'string', label: 'Email' },
  {
    key: 'role',
    type: 'enum',
    label: 'Role',
    options: [
      { value: 'Engineer', label: 'Engineer' },
      { value: 'Designer', label: 'Designer' },
      { value: 'Manager', label: 'Manager' },
      { value: 'Admin', label: 'Admin' },
    ],
  },
]

const columns: TableColumn<Employee>[] = [
  { key: 'name', header: 'Name', width: proportional(1), filter: 'name' },
  { key: 'email', header: 'Email', width: proportional(2), filter: 'email' },
  { key: 'role', header: 'Role', width: proportional(1), filter: 'role' },
  { key: 'department', header: 'Department', width: proportional(1) },
]

const { filters, onFilterChange } = useTableFilterState()
const filterPlugin = useTableFiltering<Employee>(() => ({
  filters: filters.value,
  onFilterChange,
  fields,
}))
const rows = computed(() => applyTableFilters(employees, filters.value, fields))
</script>

<template>
  <Table
    :data="rows"
    :columns="columns"
    id-key="id"
    :plugins="{ filter: filterPlugin }"
  />
</template>
```

### Table — 网格分隔

紧凑密度，行和列都有分隔线，适合密集的数字。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { pixel, proportional, Table } from '@astryx-vue/core/Table'

interface Metric extends Record<string, unknown> {
  id: string
  metric: string
  q1: string
  q2: string
  q3: string
  q4: string
}

const metrics: Metric[] = [
  { id: '1', metric: 'Revenue', q1: '$1.2M', q2: '$1.4M', q3: '$1.3M', q4: '$1.8M' },
  { id: '2', metric: 'Users', q1: '12,400', q2: '14,200', q3: '15,800', q4: '18,100' },
  { id: '3', metric: 'Retention', q1: '82%', q2: '85%', q3: '84%', q4: '88%' },
  { id: '4', metric: 'NPS', q1: '42', q2: '45', q3: '48', q4: '51' },
  { id: '5', metric: 'CSAT', q1: '4.2', q2: '4.3', q3: '4.5', q4: '4.6' },
]

const columns: TableColumn<Metric>[] = [
  { key: 'metric', header: 'Metric', width: proportional(2) },
  { key: 'q1', header: 'Q1', width: pixel(90), align: 'end' },
  { key: 'q2', header: 'Q2', width: pixel(90), align: 'end' },
  { key: 'q3', header: 'Q3', width: pixel(90), align: 'end' },
  { key: 'q4', header: 'Q4', width: pixel(90), align: 'end' },
]
</script>

<template>
  <Table
    :data="metrics"
    :columns="columns"
    id-key="id"
    dividers="grid"
    density="compact"
  />
</template>
```

### Table — 放在卡片里

表格放在卡片里，标题和表格对齐，用来看容器出血。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { Card } from '@astryx-vue/core/Card'
import { Heading } from '@astryx-vue/core/Heading'
import { Stack } from '@astryx-vue/core/Stack'
import { proportional, Table } from '@astryx-vue/core/Table'

interface User extends Record<string, unknown> {
  id: string
  name: string
  role: string
  email: string
}

const users: User[] = [
  { id: '1', name: 'Alice Johnson', role: 'Engineer', email: 'alice@example.com' },
  { id: '2', name: 'Bob Smith', role: 'Designer', email: 'bob@example.com' },
  { id: '3', name: 'Charlie Brown', role: 'PM', email: 'charlie@example.com' },
  { id: '4', name: 'Diana Prince', role: 'Engineer', email: 'diana@example.com' },
]

const columns: TableColumn<User>[] = [
  { key: 'name', header: 'Name', width: proportional(1) },
  { key: 'role', header: 'Role', width: proportional(1) },
  { key: 'email', header: 'Email', width: proportional(2) },
]
</script>

<template>
  <Card :width="520">
    <Stack direction="vertical" :gap="3">
      <Heading :level="3">
        Team Members
      </Heading>
      <Table :data="users" :columns="columns" id-key="id" has-hover />
    </Stack>
  </Card>
</template>
```

### Table — 行内过滤

过滤控件直接放在每一列表头下面，输入后立刻生效。

```vue
<script setup lang="ts">
import type { TableColumn, TableFilterField } from '@astryx-vue/core/Table'
import { applyTableFilters, proportional, Table, useTableFiltering, useTableFilterState } from '@astryx-vue/core/Table'
import { computed } from 'vue'

interface Employee extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
  department: string
}

const employees: Employee[] = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer', department: 'Platform' },
  { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer', department: 'Product' },
  { id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'Manager', department: 'Platform' },
  { id: '4', name: 'Diana Prince', email: 'diana@example.com', role: 'Engineer', department: 'Infra' },
  { id: '5', name: 'Eve Davis', email: 'eve@example.com', role: 'Admin', department: 'Operations' },
]

const fields: TableFilterField[] = [
  { key: 'name', type: 'string', label: 'Name' },
  { key: 'email', type: 'string', label: 'Email' },
  {
    key: 'role',
    type: 'enum',
    label: 'Role',
    options: [
      { value: 'Engineer', label: 'Engineer' },
      { value: 'Designer', label: 'Designer' },
      { value: 'Manager', label: 'Manager' },
      { value: 'Admin', label: 'Admin' },
    ],
  },
]

const columns: TableColumn<Employee>[] = [
  { key: 'name', header: 'Name', width: proportional(1), filter: 'name' },
  { key: 'email', header: 'Email', width: proportional(2), filter: 'email' },
  { key: 'role', header: 'Role', width: proportional(1), filter: 'role' },
  { key: 'department', header: 'Department', width: proportional(1) },
]

const { filters, onFilterChange } = useTableFilterState()
const filterPlugin = useTableFiltering<Employee>(() => ({
  filters: filters.value,
  onFilterChange,
  fields,
  variant: 'inline',
}))
const rows = computed(() => applyTableFilters(employees, filters.value, fields))
</script>

<template>
  <Table
    :data="rows"
    :columns="columns"
    id-key="id"
    :plugins="{ filter: filterPlugin }"
  />
</template>
```

### Table — 分页

数据比较多时按页查看。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { paginateData, proportional, Table, useTablePagination } from '@astryx-vue/core/Table'
import { computed, ref } from 'vue'

interface User extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
}

const names = [
  'Alice Johnson',
  'Bob Smith',
  'Charlie Brown',
  'Diana Prince',
  'Eve Davis',
  'Frank Miller',
  'Grace Lee',
  'Hank Wilson',
  'Ivy Chen',
  'Jack Turner',
  'Karen White',
  'Leo Garcia',
  'Mia Thompson',
  'Noah Martinez',
  'Olivia Clark',
  'Paul Harris',
  'Quinn Walker',
  'Rachel Adams',
  'Sam Robinson',
  'Tina Scott',
]
const roles = ['Engineer', 'Designer', 'Manager', 'Admin', 'Analyst']
const users: User[] = names.map((name, index) => ({
  id: String(index + 1),
  name,
  email: `${name.split(' ')[0]!.toLowerCase()}@example.com`,
  role: roles[index % roles.length]!,
}))

const columns: TableColumn<User>[] = [
  { key: 'name', header: 'Name', width: proportional(1) },
  { key: 'email', header: 'Email', width: proportional(2) },
  { key: 'role', header: 'Role', width: proportional(1) },
]

const page = ref(1)
const pageSize = 5
const plugin = useTablePagination<User>(() => ({
  page: page.value,
  onPageChange(next) {
    page.value = next
  },
  totalItems: users.length,
  pageSize,
}))
const rows = computed(() => paginateData(users, page.value, pageSize))
</script>

<template>
  <Table
    :data="rows"
    :columns="columns"
    id-key="id"
    :plugins="{ pagination: plugin }"
  />
</template>
```

### Table — 可调整列宽

拖表头右缘可以改列宽，松开后记住宽度。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { pixel, proportional, Table, useTableColumnResize } from '@astryx-vue/core/Table'
import { ref } from 'vue'

interface User extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
}

const users: User[] = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer' },
  { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer' },
  { id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'PM' },
  { id: '4', name: 'Diana Prince', email: 'diana@example.com', role: 'Engineer' },
  { id: '5', name: 'Eve Davis', email: 'eve@example.com', role: 'Analyst' },
]

const columns: TableColumn<User>[] = [
  { key: 'name', header: 'Name' },
  { key: 'email', header: 'Email', width: proportional(2) },
  { key: 'role', header: 'Role', width: pixel(120) },
]

const columnWidths = ref<Record<string, number>>({})
const resizePlugin = useTableColumnResize<User>(() => ({
  columnWidths: columnWidths.value,
  onColumnResizeEnd: (updates) => {
    columnWidths.value = { ...columnWidths.value, ...updates }
  },
}))
</script>

<template>
  <Table
    :data="users"
    :columns="columns"
    id-key="id"
    has-hover
    :plugins="{ columnResize: resizePlugin }"
  />
</template>
```

### Table — 丰富的单元格

邮箱是链接，角色是徽标。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { Badge } from '@astryx-vue/core/Badge'
import { Link } from '@astryx-vue/core/Link'
import { pixel, proportional, Table } from '@astryx-vue/core/Table'
import { h } from 'vue'

interface User extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
  age: number
}

const users: User[] = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer', age: 30 },
  { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer', age: 25 },
  { id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'PM', age: 35 },
  { id: '4', name: 'Diana Prince', email: 'diana@example.com', role: 'Engineer', age: 28 },
  { id: '5', name: 'Eve Davis', email: 'eve@example.com', role: 'Designer', age: 32 },
]

const roleVariant: Record<string, 'blue' | 'purple' | 'green'> = {
  Engineer: 'blue',
  Designer: 'purple',
  PM: 'green',
}

const columns: TableColumn<User>[] = [
  { key: 'name', header: 'Name' },
  {
    key: 'email',
    header: 'Email',
    width: proportional(2),
    renderCell: item => h(Link, { href: `mailto:${item.email}` }, () => String(item.email)),
  },
  {
    key: 'role',
    header: 'Role',
    renderCell: item => h(Badge, { variant: roleVariant[item.role] ?? 'neutral' }, () => item.role),
  },
  { key: 'age', header: 'Age', width: pixel(80) },
]
</script>

<template>
  <Table :data="users" :columns="columns" id-key="id" has-hover />
</template>
```

### Table — 行选择

每一行有复选框，表头可以全选。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { proportional, Table, useTableSelection, useTableSelectionState } from '@astryx-vue/core/Table'
import { ref } from 'vue'

interface User extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
}

const users: User[] = [
  { id: '1', name: 'Alice', email: 'alice@example.com', role: 'Engineer' },
  { id: '2', name: 'Bob', email: 'bob@example.com', role: 'Designer' },
  { id: '3', name: 'Charlie', email: 'charlie@example.com', role: 'Manager' },
  { id: '4', name: 'Diana', email: 'diana@example.com', role: 'Engineer' },
  { id: '5', name: 'Eve', email: 'eve@example.com', role: 'Admin' },
]

const columns: TableColumn<User>[] = [
  { key: 'name', header: 'Name', width: proportional(1) },
  { key: 'email', header: 'Email', width: proportional(2) },
  { key: 'role', header: 'Role', width: proportional(1) },
]

const selectedKeys = ref(new Set<string>())
const { selectionConfig } = useTableSelectionState<User>({
  data: users,
  idKey: 'id',
  selectedKeys,
  setSelectedKeys(keys) {
    selectedKeys.value = keys
  },
})
const selectionPlugin = useTableSelection<User>(selectionConfig)
</script>

<template>
  <Table
    :data="users"
    :columns="columns"
    id-key="id"
    :plugins="{ selection: selectionPlugin }"
  />
</template>
```

### Table — 可排序列

点表头在升序和降序之间切换。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { pixel, proportional, Table, useTableSortable, useTableSortableState } from '@astryx-vue/core/Table'

interface Employee extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
  age: number
}

const employees: Employee[] = [
  { id: '1', name: 'Alice', email: 'alice@example.com', role: 'Engineer', age: 32 },
  { id: '2', name: 'Bob', email: 'bob@example.com', role: 'Designer', age: 28 },
  { id: '3', name: 'Charlie', email: 'charlie@example.com', role: 'Manager', age: 45 },
  { id: '4', name: 'Diana', email: 'diana@example.com', role: 'Engineer', age: 37 },
  { id: '5', name: 'Eve', email: 'eve@example.com', role: 'Admin', age: 29 },
]

const columns: TableColumn<Employee>[] = [
  { key: 'name', header: 'Name', width: proportional(1), sortable: true },
  { key: 'email', header: 'Email', width: proportional(2), sortable: true },
  { key: 'role', header: 'Role', width: proportional(1), sortable: true },
  { key: 'age', header: 'Age', width: pixel(80), sortable: true },
]

const { sortedData, sortConfig } = useTableSortableState<Employee>({
  data: employees,
  defaultSort: [{ sortKey: 'name', direction: 'ascending' }],
})
const sortablePlugin = useTableSortable<Employee>(sortConfig)
</script>

<template>
  <Table
    :data="sortedData"
    :columns="columns"
    id-key="id"
    :plugins="{ sortable: sortablePlugin }"
  />
</template>
```

### Table — 斑马纹

隔行变色，悬停时再高亮，方便扫读。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { pixel, proportional, Table } from '@astryx-vue/core/Table'

interface User extends Record<string, unknown> {
  id: string
  name: string
  email: string
  role: string
  age: number
}

const users: User[] = [
  { id: '1', name: 'Alice Johnson', email: 'alice@example.com', role: 'Engineer', age: 30 },
  { id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer', age: 25 },
  { id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'PM', age: 35 },
  { id: '4', name: 'Diana Prince', email: 'diana@example.com', role: 'Engineer', age: 28 },
  { id: '5', name: 'Eve Davis', email: 'eve@example.com', role: 'Designer', age: 32 },
]

const columns: TableColumn<User>[] = [
  { key: 'name', header: 'Name' },
  { key: 'email', header: 'Email', width: proportional(2) },
  { key: 'role', header: 'Role' },
  { key: 'age', header: 'Age', width: pixel(80) },
]
</script>

<template>
  <Table :data="users" :columns="columns" id-key="id" is-striped has-hover />
</template>
```

### Table — 分组和行号

按角色分成两段。折叠后表头还在，行号只数真正的数据行。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { pixel, proportional, Table, useTableGroupedRows, useTableRowIndex } from '@astryx-vue/core/Table'
import { ref } from 'vue'

interface Employee extends Record<string, unknown> {
  id: string
  name: string
  role: string
}

const employees: Employee[] = [
  { id: '1', name: 'Alice', role: 'Engineer' },
  { id: '2', name: 'Bob', role: 'Designer' },
  { id: '3', name: 'Carol', role: 'Engineer' },
  { id: '4', name: 'Diego', role: 'Designer' },
]

const columns: TableColumn<Employee>[] = [
  { key: 'name', header: 'Name', width: proportional(1) },
  { key: 'role', header: 'Role', width: pixel(140) },
]

const collapsed = ref(new Set<string>())

function toggleGroup(key: string) {
  const next = new Set(collapsed.value)
  if (next.has(key))
    next.delete(key)
  else
    next.add(key)
  collapsed.value = next
}

const { data: groupedRows, plugin: groupedPlugin, idKey } = useTableGroupedRows<Employee>(() => ({
  data: employees,
  groupBy: employee => employee.role,
  collapsedGroups: collapsed.value,
  onToggleGroup: toggleGroup,
  getRowKey: employee => employee.id,
  groupOrder: ['Designer', 'Engineer'],
}))

const numbers = useTableRowIndex<Employee>(() => ({
  data: groupedRows.value,
  getRowKey: employee => employee.id,
}))
</script>

<template>
  <Table
    :data="groupedRows"
    :columns="columns"
    :id-key="idKey"
    :plugins="{ grouped: groupedPlugin, numbers }"
  />
</template>
```

### Table — 树形数据

子行沿用同样的列。表头可以一次展开，点行也能展开有子节点的那一行。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { proportional, Table, useTableTreeState } from '@astryx-vue/core/Table'

interface Node extends Record<string, unknown> {
  id: string
  name: string
  owner: string
  children?: Node[]
}

const nodes: Node[] = [
  {
    id: 'platform',
    name: 'Platform',
    owner: 'Ada',
    children: [
      { id: 'api', name: 'API', owner: 'Grace' },
      {
        id: 'web',
        name: 'Web',
        owner: 'Lin',
        children: [
          { id: 'docs', name: 'Docs', owner: 'Maya' },
        ],
      },
    ],
  },
  { id: 'design', name: 'Design', owner: 'Noah' },
]

const columns: TableColumn<Node>[] = [
  { key: 'name', header: 'Name', width: proportional(2) },
  { key: 'owner', header: 'Owner', width: proportional(1) },
]

const { visibleData, plugin: treePlugin } = useTableTreeState<Node>({
  data: nodes,
  idKey: 'id',
  defaultExpandedIds: ['platform'],
  hasExpandAllControl: true,
  hasRowClickExpansion: true,
})
</script>

<template>
  <Table
    :data="visibleData"
    :columns="columns"
    id-key="id"
    has-hover
    :plugins="{ tree: treePlugin }"
  />
</template>
```

### Table — 展开行

点行或行首的箭头，在下面展开备注。右键也有同样的操作。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { pixel, proportional, Table, useTableRowExpansion } from '@astryx-vue/core/Table'
import { h, ref } from 'vue'

interface Order extends Record<string, unknown> {
  id: string
  customer: string
  total: string
  note: string
}

const orders: Order[] = [
  { id: '1042', customer: 'Northwind', total: '$240', note: 'Two seats, ships Friday.' },
  { id: '1043', customer: 'Contoso', total: '$80', note: 'Gift wrap requested.' },
  { id: '1044', customer: 'Fabrikam', total: '$560', note: 'Hold for pickup.' },
]

const columns: TableColumn<Order>[] = [
  { key: 'id', header: 'Order', width: pixel(100) },
  { key: 'customer', header: 'Customer', width: proportional(1) },
  { key: 'total', header: 'Total', width: pixel(100) },
]

const expanded = ref(new Set<string>(['1042']))

function toggle(key: string) {
  const next = new Set(expanded.value)
  if (next.has(key))
    next.delete(key)
  else
    next.add(key)
  expanded.value = next
}

const expansion = useTableRowExpansion<Order>(() => ({
  expandedKeys: expanded.value,
  onToggle: toggle,
  getRowKey: order => order.id,
  panelVariant: 'muted',
  hasRowClickExpansion: true,
  renderExpanded: order => h('p', order.note),
}))
</script>

<template>
  <Table
    :data="orders"
    :columns="columns"
    id-key="id"
    :plugins="{ expansion }"
  />
</template>
```

### Table — 固定列

姓名钉在左边，周五钉在右边。把表格滚到边上时，中间的日期会从固定列下面滑过去。

```vue
<script setup lang="ts">
import type { TableColumn } from '@astryx-vue/core/Table'
import { pixel, Table, useTableStickyColumns } from '@astryx-vue/core/Table'

interface Shift extends Record<string, unknown> {
  id: string
  name: string
  monday: string
  tuesday: string
  wednesday: string
  thursday: string
  friday: string
}

const shifts: Shift[] = [
  { id: '1', name: 'Alice', monday: 'Desk', tuesday: 'Desk', wednesday: 'Remote', thursday: 'Desk', friday: 'Off' },
  { id: '2', name: 'Bob', monday: 'Remote', tuesday: 'Desk', wednesday: 'Desk', thursday: 'Remote', friday: 'Desk' },
  { id: '3', name: 'Carol', monday: 'Off', tuesday: 'Remote', wednesday: 'Desk', thursday: 'Desk', friday: 'Remote' },
]

const columns: TableColumn<Shift>[] = [
  { key: 'name', header: 'Name', width: pixel(140) },
  { key: 'monday', header: 'Monday', width: pixel(140) },
  { key: 'tuesday', header: 'Tuesday', width: pixel(140) },
  { key: 'wednesday', header: 'Wednesday', width: pixel(160) },
  { key: 'thursday', header: 'Thursday', width: pixel(140) },
  { key: 'friday', header: 'Friday', width: pixel(140) },
]

const sticky = useTableStickyColumns<Shift>({
  startKeys: ['name'],
  endKeys: ['friday'],
})
</script>

<template>
  <Table
    :data="shifts"
    :columns="columns"
    id-key="id"
    :plugins="{ sticky }"
  />
</template>
```

### Table — 行状态

行首的标记说明这一行是成功、警告、错误，或者只是一个没有信号的圆点。

```vue
<script setup lang="ts">
import type { TableColumn, TableRowStatus } from '@astryx-vue/core/Table'
import { proportional, Table, useTableRowStatus } from '@astryx-vue/core/Table'

interface Check extends Record<string, unknown> {
  id: string
  name: string
  detail: string
  state: 'success' | 'warning' | 'error' | 'neutral'
}

const checks: Check[] = [
  { id: '1', name: 'Payments', detail: 'Settled', state: 'success' },
  { id: '2', name: 'Inventory', detail: 'Low stock', state: 'warning' },
  { id: '3', name: 'Mail', detail: 'Bounced', state: 'error' },
  { id: '4', name: 'Notes', detail: 'No signal', state: 'neutral' },
]

const columns: TableColumn<Check>[] = [
  { key: 'name', header: 'Service', width: proportional(1) },
  { key: 'detail', header: 'Detail', width: proportional(1) },
]

function statusFor(check: Check): TableRowStatus | null {
  if (check.state === 'neutral')
    return { color: 'gray', label: 'No signal' }
  if (check.state === 'success')
    return { status: 'success', label: 'Settled' }
  if (check.state === 'warning')
    return { status: 'warning', label: 'Low stock' }
  return { status: 'error', label: 'Bounced' }
}

const status = useTableRowStatus<Check>({ getStatus: statusFor })
</script>

<template>
  <Table
    :data="checks"
    :columns="columns"
    id-key="id"
    :plugins="{ status }"
  />
</template>
```

## 属性

### Table

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `data` | `T[]` | - | 行数据。不传时，用默认插槽自己拼表格。 |
| `columns` | `TableColumn[]` | - | `key`、`header`、`width`、`align`、`verticalAlign`、`sortable`、`filter` 和 `renderCell`。 |
| `idKey` | `string \| (row: T) => string` | `'id'` | 行身份。分组行要用函数，才能认出合成出来的表头。 |
| `density` | `'compact' \| 'balanced' \| 'spacious'` | `'balanced'` | 单元格内边距。紧凑 4×8，适中 8×12，宽松 12×16。 |
| `dividers` | `'rows' \| 'columns' \| 'grid' \| 'none'` | `'rows'` | 表头始终有底边。表体最后一行不再画行线。 |
| `isStriped` | `boolean` | `false` | 偶数行使用淡底。 |
| `hasHover` | `boolean` | `false` | 悬停填色，只在指针能悬停时出现。 |
| `verticalAlign` | `'top' \| 'middle' \| 'bottom'` | `'middle'` | 默认垂直对齐。列上可以单独覆盖。 |
| `textOverflow` | `'wrap' \| 'truncate'` | `'wrap'` | 文字换行，或截成一行。 |
| `plugins` | `Record<string, TablePlugin>` | - | 排序、选择、分页、列设置、拖宽度、过滤、分组、树、展开、固定列、行状态和行号。 |
| `label` | `string` | 本地化的「表格」 | 表格的无障碍名称。 |
| `rowIndexStart` | `number` | `1` | 当前窗口第一行在整表里的位置。和总行数只要设置一个，数据行就会带上行号。 |
| `rowCount` | `number` | - | 整表有多少行。只设置起点时，总行数按未知处理。 |

### useTableGroupedRows

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `data` | `T[]` | 必需 | 已经过滤、排序、分页之后的行。 |
| `groupBy` | `(item: T) => string` | 必需 | 这一行属于哪一组。 |
| `collapsedGroups` | `Set<string>` | 必需 | 当前折叠的组。切换时换成新集合。 |
| `onToggleGroup` | `(groupKey: string) => void` | 必需 | 点表头或箭头时调用。 |
| `renderGroupHeader` | `(key, count, collapsed) => VNodeChild` | 组名和数量 | 箭头右边的内容。 |
| `getRowKey` | `(item: T) => string` | 按位置 | 真实行的稳定身份。 |
| `groupOrder` | `string[]` | 首次出现的顺序 | 写在前面的组先出现。 |

### useTableTreeState

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `data` | `T[]` | 必需 | 带子节点的树。 |
| `idKey` | `string \| (item) => string` | 必需 | 节点身份。用来记住展开状态，也用来跳过环。 |
| `childrenKey` | `string` | `children` | 子节点放在哪个字段。 |
| `expandedIds` | `Set<string>` | - | 受控的展开集合。不传则组件自己记。 |
| `defaultExpandedIds` | `Iterable<string>` | 空 | 一开始展开哪些节点。 |
| `onExpandedIdsChange` | `(ids) => void` | - | 展开集合变化时调用。 |
| `hasExpandAllControl` | `boolean` | `false` | 在树那一列表头放全部展开按钮。 |
| `hasRowClickExpansion` | `boolean` | `false` | 点有子节点的行也能展开。点到按钮、链接或选中的文字时不动。 |
| `indent` | `'sm' \| 'md' \| 'lg'` | `md` | 每一层往里缩多少。 |
| `treeColumnKey` | `string` | 第一个普通列 | 缩进和箭头画在哪一列。 |
| `sortSiblings` | `(siblings: T[]) => T[]` | - | 同一层怎么排序。 |

### useTableRowExpansion

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `expandedKeys` | `Set<string>` | 必需 | 当前展开的行。由调用方保存。 |
| `onToggle` | `(key: string) => void` | 必需 | 箭头、行点击或右键时调用。 |
| `getRowKey` | `(item: T) => string` | 必需 | 行身份。 |
| `renderExpanded` | `(item: T) => VNodeChild` | 必需 | 详情内容。它从第一列开始，不从箭头下面开始。 |
| `getIsItemExpandable` | `(item: T) => boolean` | 全部可以 | 哪些行可以展开。 |
| `panelVariant` | `'muted' \| 'transparent'` | `transparent` | 详情的底。透明时接着表格自己的表面，淡底用来和数据行分开。 |
| `hasRowClickExpansion` | `boolean` | `false` | 点行也能展开。点到控件或选中的文字时不动。 |

### useTableStickyColumns

| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `startKeys` | `string[]` | 空 | 钉住从第一列到这里列出的最后一列。 |
| `endKeys` | `string[]` | 空 | 钉住从这里列出的第一列到表格末尾。 |

### useTableRowStatus

`getStatus` 返回 `null` 时这一格是空的。语义状态用成功、警告或错误图标。自定义状态给一个颜色；有图标就画图标，没有就画一个圆点。说明会进提示，也会读给辅助技术。

### useTableRowIndex

`data` 是表格正在画的行，分组之后的那一份。`startFrom` 默认从 1 开始。`getRowKey` 用来认出同一行；不传就按对象本身比较。`label` 是这一列表头，默认是一个井号。

`proportional(value)` 分剩余宽度，默认最小 120px。没写宽度的列是弹性列，下限 60px。`pixel(value)` 同时设定宽度和最小宽度。

### TableSelectionToolbar

`count` 是选中的行数。`clear` 事件让调用方清掉选择。默认插槽放批量操作。数量、清除文案和区域名称来自表格的文案。

## 主题

表头用标签字号、半粗字重和次要文字色。表体用正文字号。选中行用强调色的淡底。斑马纹用淡底。悬停用悬停遮罩。放进卡片时，滚动区域会用卡片公布的容器内边距，让首尾单元格和标题对齐。
