---
title: Table 表格
description: 高性能虚拟滚动表格,支持排序、固定列、多选、行内编辑、列宽 / 列序拖拽与行拖拽排序
---
# Table 表格
`Table` 用于展示结构化数据,底层是 [`stk-table-vue`](https://ja-plus.github.io/stk-table-vue/),虚拟滚动 / 列宽拖拽 / 列序拖拽 / 区域选择 / 行拖拽排序都由它提供;组件负责**性能门控**、**内部列管理**与**行内编辑态**。
> 本组件与 `fms-vue` 的 `FmsTable` **同源同构**(实现照抄,仅替换命名与令牌体系),
> 两边行为一致、可互相迁移。迁移时需要的改动见文末「从旧版迁移」。
## 何时使用
- 数据量大(成百上千行)且需要滚动流畅时;
- 需要行内编辑、多选批量操作、列宽 / 列序可调时;
- 数据量很小、只要一张静态表时,原生 `
` 或 [Grid](/ui/general/grid) 更轻。
## 基础用法
给 `columns` 与 `dataSource` 两份数据即可。序号列(`seq`)默认开启,边框(`bordered`)默认有。
## 固定列
列上写 `fixed: 'left' | 'right'` 即可固定(底座用 `position: sticky` 实现,非双表格叠加)。三件需要知道的事:
- **序号列与选择列已经固定在左侧**,不需要业务再配;业务列的 `fixed: 'left'` 会自动接在它们后面,`fixed: 'right'` 的列要排在 `columns` 最后。
- **固定列必须写 `width`**:左固定列的吸附位移由「它左侧那些固定列的列宽累加」算出,缺宽度就没有位移。
- 固定列只有在**总列宽超过容器宽**时才看得出效果 —— 整表都看得见时,固定与不固定没有任何区别。
```vue
```
## 虚拟滚动与性能
组件内置两条门控,**自动**决定是否启用虚拟滚动,不需要业务手动切换:
| 方向 | 阈值 | 低于阈值时 | 原因 |
| --- | --- | --- | --- |
| 纵向 | 行数 > 100 | 全量渲染 | 库的虚拟滚动在「行数略多于单屏行数」时会把最后几行算出视口外,必须滚动才可见;行数少时全量渲染没有性能代价,却换来确定性正确的渲染 |
| 横向 | 列数 > 20 | 关掉横向虚拟 | 库在横向虚拟开启时会把每列宽度钉死为 `minWidth ?? width ?? 100`,未设 `width` 的列会失去「按比例铺满容器」的原生自适应 |
可以用 `virtual` / `virtualX` 关掉,但**不能**把它们强制打开到阈值以下。
除了门控,还有两处性能相关设计:
- **尺寸变化 150ms 防抖**:折叠动画 / 窗口缩放期间 ResizeObserver 每帧都会通知,逐帧重算会强制 reflow 并重渲染可见行列;组件合并到尺寸稳定后只刷新一次,并区分「仅宽度变化」只重算横向;
- **选择列用原生 ``**:视觉由 CSS 模拟成 [Checkbox](/ui/form/checkbox) 的外观,但每行不创建组件实例。
## 多选与批量操作
传 `rowSelection` 即在最前注入选择列;`getCheckboxProps` 可禁用个别行(禁用行不计入「全选」的分母)。
## 排序
列上写 `sorter: true` 即可点表头排序(组件内排序,不请求接口)。`sortType` 决定按数字还是字符串比较,默认字符串;需要自定义规则时 `sorter` 也可以传函数。
## 行内编辑
列上加 `editor` 即可编辑:**单击**单元格进入(不需要双击)。
### 编辑器类型
| `editor.type` | 说明 |
| --- | --- |
| `'input'`(默认) | 用 [Input](/ui/form/input) 渲染,**单行**。细分形态由 `inputType` 决定:`'text'`(默认)/ `'password'` / `'number'` / `'textarea'` |
| `'select'` | 用 [Select](/ui/form/select) 渲染;`options` 可以是数组,也可以是接收当前行的函数 |
| `'checkbox'` | 点击即切换,无需先激活整行;约定 `1` = 选中、`0` = 未选中(与后端 tinyint 对齐) |
不传 `type` 时按 `'input'` 处理,所以 `editor:` 这种写法也是合法的(走默认的文本输入)。
编辑器的输入值先写进内部草稿,**失焦才落库并抛出 `change`**。这样每敲一个字符不会触发父级重渲染(否则光标会丢、输入法会断)。`select` 与 `checkbox` 是「改即落库」,不走草稿。用 `editable={false}` 可整体禁用编辑。
`select` 列在**非编辑态**会把值解析成对应选项的 `label` 展示(找不到就原样显示值)。
### 键盘
| 按键 | 行为 |
| --- | --- |
| `Enter` | 全选当前格内容(直接输入即覆盖),不移动选区 |
| `Esc` | 退出编辑(失焦) |
| 方向键 | 由表格接管、移动单元格选区 |
| 失焦(点别处 / 方向键走开) | **提交**并抛出 `change` |
> 方向键移动会先让编辑器待命:按住连按时**全程不挂编辑器**(避免每帧创建 / 销毁组件导致掉帧),松手后再把编辑器挂到最终落点;快速离散连按(间隔 <150ms)也走 100ms 防抖合并。
### ⚠ 数据源必须是响应式的
**行对象必须能被 Vue 追踪**,否则编辑回写不会触发重渲染。典型的踩坑写法是用 `computed` 派生数据:
```ts
// ❌ 错:computed 返回的是普通对象数组,行字段不可追踪
const rows = computed(() => Array.from({ length: n.value }, (_, i) => makeRow(i)))
// ✅ 对:ref 会递归代理内部对象
const rows = ref([])
watch(n, (v) => { rows.value = Array.from({ length: v }, (_, i) => makeRow(i)) }, { immediate: true })
```
用错时的症状**极具迷惑性**:`change` 事件照常抛出(数据其实已经改了),但界面上**仍显示旧值**,点一下别处才「突然」更新;`select` 编辑器尤其像事件时序 bug(选完下拉关了、值却没变)。
### 按行禁用
`editor.disabled` 可以是布尔,也可以是接收当前行的函数 —— 「已放舱之后状态不许再改」这类条件随行数据变化,用函数形态:
```ts
{
dataIndex: 'status',
title: '状态',
editor: { type: 'select', options: STATUS_OPTIONS, disabled: (row) => row.status === 2 },
}
```
被禁用的格走纯展示渲染,不会挂编辑器。
## 操作列
列上加 `actions` 渲染成一排小按钮;`type: 'more'` 的项会收进「更多」下拉。带 `confirm` 的操作先弹确认框,确认后才执行 `onClick`。
操作列建议显式写 `resizable: false`:它的宽度由内容决定,拖了不会落库,列一重建就丢。
## 列宽、列序与行拖拽
列宽 / 列序拖拽要求用 `v-model:columns` 绑定列定义——否则组件拖完无处回写(松手回弹),会直接关掉这两个功能。行拖拽排序用 `row-draggable`,顺序变化通过 `row-order-change` 回传。
## API
### Props
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `columns` | `G3TableColumn[]` | `[]` | 列定义,配合 `v-model:columns` 接收拖拽后的回写 |
| `dataSource` | `T[]` | `[]` | 数据源(**必须响应式**,见「行内编辑」) |
| `rowKey` | `string \| (row) => string \| number` | `'id'` | 行唯一键 |
| `virtual` | `boolean` | `true` | 纵向虚拟滚动总开关(实际是否开启受行数门控) |
| `virtualX` | `boolean` | `true` | 横向虚拟滚动总开关(实际是否开启受列数门控) |
| `rowHeight` | `number` | `30` | 行高,同时驱动虚拟滚动计算 |
| `headerRowHeight` | `number` | `34` | 表头行高 |
| `bordered` | `boolean` | `true` | 边框。`true` = 圆角外框 + 行分隔线;`false` = 无外框,只留行分隔线(嵌进卡片 / 面板时用) |
| `colResizable` | `boolean \| object` | `true` | 列宽拖拽;未绑定 `v-model:columns` 时自动关闭。可用 `{ disabled: (col) => boolean }` 按列禁用 |
| `columnDraggable` | `boolean \| object` | `true` | 列序拖拽;同上要求绑定 `v-model:columns`。`{ mode: 'none' \| 'insert' \| 'swap', disabled }` |
| `rowDraggable` | `boolean` | `false` | 行拖拽排序 |
| `rowSelection` | `TableRowSelection \| null` | `null` | 多选配置,见下 |
| `seq` | `boolean \| { title?, width? }` | `true` | 序号列,**固定左侧**,默认宽 50 |
| `activeRowKey` | `string \| number \| null` | `null` | 当前高亮行 |
| `editable` | `boolean` | `true` | 行内编辑总开关 |
| `noDataFull` | `boolean` | `true` | 空态铺满容器高度 |
| `showOverflow` | `boolean` | `true` | 内容溢出省略 |
| `autoFitWidth` | `boolean` | `false` | 非横向虚拟时铺满容器宽度(开启后关闭列宽拖拽) |
| `theme` | `'light' \| 'dark'` | — | 不传则跟随全局主题(`html[data-g3-theme]`) |
| `footerData` | `T[]` | `[]` | 表尾数据 |
| `footerConfig` | `object` | `` | 表尾配置 |
### 列定义 `G3TableColumn`
在 `stk-table-vue` 的列类型基础上扩展(交叉类型,非 `Omit`);下面只列自有字段,其余(`width` / `minWidth` / `fixed` / `align` / `headerAlign` / `sorter` / `sortType` / `children` / `mergeCells` / `customCell` / `customHeaderCell` 等)原样透传。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `dataIndex` | `keyof T & string` | 取值字段 |
| `title` | `string` | 表头文字 |
| `resizable` | `boolean` | 该列是否可拖宽;操作列 / 展开列建议设 `false` |
| `editor` | `TableEditor` | 行内编辑器,见下 |
| `actions` | `TableAction[]` | 操作列按钮 |
| `customCell` | `(props) => VNode` | 自定义单元格渲染(逃生舱),传了就不再自动包装 |
### `editor`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `type` | `'input' \| 'select' \| 'checkbox'` | 编辑器类型,不传按 `'input'` 处理 |
| `inputType` | `'text' \| 'password' \| 'number' \| 'textarea'` | 仅 `type: 'input'` 生效,默认 `'text'` |
| `options` | `TableSelectOption[] \| ((row) => TableSelectOption[])` | select 选项,可依赖当前行其它字段 |
| `disabled` | `boolean \| ((row) => boolean)` | 是否禁用该格编辑 |
| `allowClear` | `boolean` | select 是否可清空,默认 `true` |
| `emptyValue` | `unknown` | select 清空后落库的值,默认 `null` |
| `onChange` | `(value, row, col) => void` | 本列值变化回调(早于组件的 `change`) |
`TableSelectOption`:`{ label: string; value: string | number | boolean | null; disabled?: boolean }`
### `actions`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `key` | `string \| number` | 唯一键;`type: 'more'` 的子项用它做下拉 key |
| `label` | `string` | 文案 |
| `type` | `'default' \| 'danger' \| 'delete' \| 'more'` | 语义类型 |
| `icon` | `Component` | 图标组件,渲染为 size=14 |
| `title` | `string` | title 提示 |
| `danger` | `boolean` | 危险色 |
| `visible` | `boolean \| ((row) => boolean)` | 是否显示 |
| `disabled` | `boolean \| ((row) => boolean)` | 是否禁用 |
| `confirm` | `string \| object` | 二次确认;字符串为标题,对象支持 `title` / `description` / `content` / `okText` / `cancelText` / `okType` / `width` / `maskClosable` |
| `onClick` | `(row) => void` | 点击回调(带 `confirm` 时在确认后执行) |
| `items` | `TableAction[]` | `type: 'more'` 时的子项 |
### `rowSelection`
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `selectedRowKeys` | `Array` | 已选行的 key 数组(受控) |
| `onChange` | `(keys, rows) => void` | 选中变化回调 |
| `getCheckboxProps` | `(row) => { disabled?: boolean }` | 单行复选框属性 |
### Events
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| `update:columns` | `(columns)` | 列宽 / 列序拖拽后回写(**内部列已剥离**) |
| `change` | `(row, col, value)` | 行内编辑的值变化 |
| `row-order-change` | `(rows, dragStartKey, targetRowKey)` | 行拖拽排序完成。后两个参数是**行 key**,不是索引 |
| `area-selection-change` | `(ranges)` | 单元格选区变化(行内编辑依赖它) |
> `row-click` 等底座事件**未声明为 emit**,而是随 `$attrs` 透传到内层 `StkTable`,
> 照常可用:`@row-click="(_, row) => (activeKey = String(row.id))"`。
> 注意这类监听器拿不到组件侧的类型推断,回调参数需显式标注
> (如 `(_: MouseEvent, row: TableRow) => ...`)。
### Exposed
| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `scrollToRow(rowKey)` | `Promise` | 滚动到指定行 |
| `editRow(rowKey, focusCol?)` | `Promise` | 滚动到指定行并进入编辑 |
| `initVirtualScroll()` / `refreshLayout()` | — | 手动重算虚拟滚动布局 |
| `getSelectedArea()` / `clearSelectedArea()` | — | 读取 / 清空单元格选区 |
| `setRowExpand(...)` | — | 展开 / 收起行(需配 `expand` 列) |
### Slots
| 名称 | 说明 |
| --- | --- |
| `empty` | 空态内容,默认显示「暂无数据」 |
| `expand` | 展开行内容(需列配置 `type: 'expand'`) |
## 实现说明
- **底层是 `stk-table-vue`**:虚拟滚动、列宽 / 列序拖拽、区域选择、行拖拽、二级表头、固定列、合并单元格都由它实现。组件不重复造这些轮子,只做门控与适配。
- **库样式已并入 `dist/style.css`**:库的 CSS 是独立文件(其 JS 里不含任何 `.css` import),组件 `@use` 了它,消费方不需要额外引一次第三方样式。
- 注意 SCSS 里**不能**写 `@use 'stk-table-vue/lib/style.css'`:带 `.css` 后缀时 Sass 不会内联,只原样输出一条 CSS `@import` 语句,产物里会留下一条浏览器解析不了的裸包路径。去掉后缀才会真正内联。
- **主题通过覆盖库的 CSS 变量实现**:库把主题值都做成了 `.stk-table` 作用域内的变量,组件把它们映射到 `--g3-*` 令牌(覆盖层在 `src/table/style/_overrides.scss`)。改变量而不是逐个选择器改样式,库升级时不容易失效。
- **区域选择按需自注册**:库的单元格区域选择是全局注册的(`registerFeature(useAreaSelection)`),不注册则 `area-selection-change` 永不触发、行内编辑失效。而且**注册必须早于任何 StkTable 实例的创建**(库在 setup 顶层取值),所以不能放在 `onMounted`。组件在**模块被求值时就注册**;需要显式控制时可调用导出的 `registerTableFeatures()`。
- **边框:外框自绘,横线走真 `border`**。组件把 `bordered` 翻成底座的 `'v'` 档(只借它的竖线),再自己补外框与横线。
- **行编辑态存在 `WeakMap` 里**:按行对象存,不给业务数据打标记;行被回收时状态自动释放。
- **编辑器按编辑态门控注入**:没有任何行处于编辑态时,可编辑列走底座原生 td 渲染路径(减少每格一个函数式组件实例);`checkbox` 与 `select` 例外——前者始终可交互,后者需要解析 label 展示。某一行进入编辑态后,列上才会挂上按行分发的编辑器。
- **RTL**:布局用逻辑属性,带符号位移走 `--g3-dir-sign`。
## 从旧版迁移
本组件曾有一版**自研能力更多**的实现(含校验、精度、日期编辑器、键盘跳格等)。为与
`fms-vue` 的 `FmsTable` 对齐,现已按 fms 重写;下列能力**已移除**,迁移时需改到业务侧:
| 已移除 | 迁移建议 |
| --- | --- |
| `editor.validate` / `editor.validateWarning` / `editor.errorStyle` | 校验改由业务侧负责:保存前遍历 `dataSource` 自行判定并提示 |
| `editor.precision` / `col.format` | 数值格式化与小数位规则移到业务侧(或列上 `customCell` 自定义渲染) |
| `editor.type: 'date'` / `editor.format` / `editor.valueFormat` / `editor.placeholder` | 日期列改为纯展示(`customCell`),或换用可编辑的 `input` |
| `checkPrecision` / `checkDate` 导出 | 已移除;规则由业务侧自行实现 |
| Tab / Shift+Tab 跳格、方向键切格与 Enter 下移 | 方向键仍由表格移动选区;`Enter` 改为「全选当前格」 |
| `editorSeamless` prop | 编辑器形态固定(fms 口径),不再可切换 |
| 只读格「灰底」视觉 | 已移除;不可编辑的列即无 `editor` |
| `stripe` / `fixedColShadow` prop | 已移除;如需斑马纹自行写 CSS |
| `scrollbar` / `scrollRowByRow` / `smoothScroll` prop | 已移除;固定走底座的默认滚动路径 |
| `update:selectedRowKeys` 事件 | 用 `rowSelection.onChange` 接收勾选变化 |
| `row-click` 声明式 emit | 仍可用,但改由 `$attrs` 透传(见「Events」小节的提示) |
| `getTableData()` 暴露方法 | 已移除;需要当前顺序时用 `row-order-change` 回传的 `rows` |
**`editor.type` 的值域变了**:旧版是 `'text' | 'number' | 'date' | 'password' | 'select' | 'checkbox'`,
现在是 `'input' | 'select' | 'checkbox'`(输入框的细分形态走 `inputType`)。
迁移时 `type: 'text'` → `type: 'input'`,`type: 'number'` → `type: 'input', inputType: 'number'`。