--- 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'`。