This commit is contained in:
oneao committed 2026-10-13 22:32:50 +08:00
1 parent f3936cec61
commit 15e0ae5b67
115 files changed
+11399 -2741

No files matched your search

+91 -14
View File
@@ -19,6 +19,35 @@ description: 高性能虚拟滚动表格,支持排序、固定列、多选、
<demo vue="table/basic.vue" />
## 固定列
列上写 `fixed: 'left' | 'right'` 即可固定(底座用 `position: sticky` 实现,非双表格叠加)。三件需要知道的事:
- **序号列与选择列已经固定在左侧**,不需要业务再配;业务列的 `fixed: 'left'` 会自动接在它们后面,`fixed: 'right'` 的列要排在 `columns` 最后。
- **固定列必须写 `width`**:左固定列的吸附位移由「它左侧那些固定列的列宽累加」算出,缺宽度就没有位移(右固定列若不需要吸附效果可以不写,但统一写上最省心)。
- 固定列被滚动内容遮住时才画阴影(`fixedColShadow`,默认关)。它不影响功能,但冻结列与滚过去的列底色同为白、又紧贴在一起,没有这层渐变就看不出边界在哪,业务侧建议打开。
固定列只有在**总列宽超过容器宽**时才看得出效果 —— 整表都看得见时,固定与不固定没有任何区别。
固定列的底色是**不透明的表格基色**(否则横滚时下方内容会透上来),行 hover / 当前行高亮改用内阴影叠色;这层叠色与**单元格选中描边并存**(选中框不会被行色替换掉),正在编辑的那一格则与普通列一致 —— 纯基色 + 焦点环,不叠行色。
同理还有**斑马纹**:开了 `stripe` 时,固定列(含序号列)也会跟着条纹 —— 冻结列的底色必须不透明,组件在它上面补了一层同色内阴影把条纹接上(否则就是「整表隔行灰、只有序号列没有」)。这层处理对**宿主页自己的行底色样式**(例如文档站的正文斑马纹)同样有效。
### ⚠ 固定列 + 横向虚拟的已知限制
同时开**固定列**与**横向虚拟**(列数 > 20 且显式要 `virtual-x`)时,**快速横向拖动**(拖滚动条、触控板甩)会出现一帧位移:被虚拟掉的列在表体左侧留了一个宽度可变的占位,而它是**异步**更新的(库内部用 rAF)。滚动已经到位、占位还是旧宽度的那一帧里,固定列的静态位置被占位推着走 —— 而 `position: sticky` 只能把格子往右收、不能把它往左拉回来,于是**左固定列**先偏一下再吸回去(实测偏 258 → 1628,等于占位全长);右固定列偏差为 0,所以现象只出现在左侧。
这是「占位 + sticky」这套布局的固有代价,组件这层无法在绘制前把它抹平(试过在 scroll 里提前重算窗口:无效,且会让表体多出中间态)。两条出路:
- 要**绝对稳**:别让二者同时存在 —— `:virtual-x="false"`(列数不多时没有任何代价);
- 要**根治**:需要把左占位挪到固定列之后(上游 `stk-table-vue` 的行布局改动)。
```vue
<!-- 左冻结首列、右冻结末列 -->
<G3Table :columns="columns" :data-source="data" fixed-col-shadow />
```
## 虚拟滚动与性能
组件内置两条门控,**自动**决定是否启用虚拟滚动,不需要业务手动切换:
@@ -58,15 +87,30 @@ description: 高性能虚拟滚动表格,支持排序、固定列、多选、
| --- | --- |
| `Enter` | **全选当前内容**(便于直接覆写),不提交 |
| `Esc` | **取消编辑**:把该行恢复到进入编辑前的值,然后退出编辑 |
| 失焦(点别处 / Tab) | **提交**并抛出 `change` |
| `Tab` / `Shift+Tab` | **提交并跳到下一个 / 上一个可编辑格**(见下方「跳格」) |
| 失焦(点别处) | **提交**并抛出 `change` |
`Enter` 不做提交是刻意的:单元格里的值通常要整体替换,全选后直接输入即可,不必先手动清空。提交一律靠失焦。
### 跳格
Tab / Shift+Tab 只在**可编辑格**之间走动:没有 `editor` 的列、以及被 `disabled` 按行禁用的格都会被跳过;行尾自动接到下一行的第一个可编辑格,到表格两头则**循环**(Excel / AG Grid 的手感)。全表没有任何可编辑格时不接管,Tab 保持浏览器默认行为。
为什么不直接交给浏览器的 Tab 序:
- 编辑态的编辑器**只挂在当前编辑行**上(其它行是纯文本,不可聚焦),浏览器按文档顺序在表格里找不到「下一格」,会把焦点送到页面其它元素上 —— 表现出来就是「按了 Tab 但没选中下一个」;
- `select` 更明显:它展开时会把 DOM 焦点移进**下拉浮层**,而浮层是 `Teleport` 到 `body` 的,浮层里的按键事件到不了表格。所以表格在 `document` 的捕获阶段接管 Tab,并用 ARIA 关联(浮层 id ↔ 触发器 `aria-controls`)把浮层追回它所属的单元格。
还有两种「焦点此刻不在编辑器上,但 Tab 仍属于表格」的情况,同样会被接管:
- **编辑行 / 编辑列被虚拟滚动滚出渲染窗口**(编辑器随之卸载、焦点掉到 `body`):这时按 Tab 会把表格**滚回编辑行**并继续前进一格,而不是把焦点送出表格(放行的话浏览器会按文档顺序落到页面上其它元素上)。判据是「编辑格此刻不在 DOM 里」——编辑格还在渲染时(例如点了页面空白)不接管,页面 / 表单的 Tab 序不受影响。
- **长按 Tab / 连按 Tab**(`keydown` 带 `repeat`,或间隔小于底座 100ms 的连按防抖):底座的导航优化在长按期间会**把编辑器全卸载**(连「当前编辑行」都置空,等松手再挂回来),焦点因此落在 `body`。这时 Tab 仍然被认作表格的,一格一格继续走,松手后编辑器落到最后一格。从**只读列**起步(如「订舱号」,没有编辑器)也走这条路径。
| 编辑器类型 | 说明 |
| --- | --- |
| `text` / `number` / `password` / `textarea` | 用 [Input](/ui/form/input) 渲染 |
| `select` | 用 [Select](/ui/form/select) 渲染;`options` 可以是数组,也可以是接收当前行的函数 |
| `checkbox` | 点击即切换,无需先激活整行;约定 `1` = 选中、`0` = 未选中(与后端 tinyint 对齐) |
| `checkbox` | 点击即切换,无需先激活整行;约定 `1` = 选中、`0` = 未选中(与后端 tinyint 对齐)。切换后**该格同时成为当前格**(行高亮 + 选中框 + 进入该行编辑),与点击其它单元格行为一致 |
编辑器的输入值先写进内部草稿,**失焦才落库并抛出 `change`**。这样每敲一个字符不会触发父级重渲染(否则光标会丢、输入法会断)。用 `editable={false}` 可整体禁用编辑。
@@ -78,7 +122,7 @@ description: 高性能虚拟滚动表格,支持排序、固定列、多选、
- 单元格几何在进入 / 离开编辑时**完全不变**——编辑器的盒高就等于 `rowHeight`,同一行的纯文本列不会跟着跳;
- 水平内边距由单元格统一提供(编辑器和纯文本格用同一套网格),**进出编辑态零位移**;
- 视觉提示只在**焦点、hover 与校验**时出现,且都不占用布局空间:
- 焦点 → 单元格内嵌的主色焦点环(`td:focus-within` 的内阴影)+ 输入框自身的焦点光晕;
- 焦点 → 单元格内嵌的主色焦点环(内阴影)+ 输入框自身的焦点光晕;**正在编辑的那一格始终有环**(判据是编辑器上的标记类,而不是 `:focus-within`——`select` 展开时焦点会被移进浮层,用 `:focus-within` 会时有时无);
- hover → 格内极淡填充;
- 校验(`status` = `error` / `warning`)→ 文字色 + 1px 内描边。
@@ -111,17 +155,44 @@ watch(n, (v) => { rows.value = Array.from({ length: v }, (_, i) => makeRow(i)) }
开发模式下,进入编辑时会检测并打印一条警告指明改法(每个组件实例只警告一次)。
### 编辑期的只读列
### 不可改的格子(灰底,常驻)
某一列没有配 `editor` 时,它在编辑期**不可修改**。为了让人一眼看出「这格点不动」,
该行里**没有编辑器的格子会自动铺一层灰底**、文字也降一档亮度——
不需要业务额外配任何东西,也不需要加锁图标。
在**支持行内编辑的表**里:凡是「现在改不了」的格子,都铺一层灰底 + 文字降一档亮度,
不需要业务额外配任何东西、也不加锁图标。三种情形统一走这套视觉:
灰底只出现在**正在编辑的那一行**(其余行保持原样),且用 `--td-bgc` 叠中性色得到,
所以亮 / 暗主题自动跟随。
| 情形 | 怎么配 | 何时变灰 |
| --- | --- | --- |
| 整列只读 | 列上**没有** `editor` | 始终 |
| 整列禁用 | `editor.disabled: true` | 始终 |
| 按行禁用 | `editor.disabled: (row) => boolean` | 该行判定为 true 时(动态) |
**前提:这张表里得先有人能编辑。** 灰底的语义是「这一格现在改不了」,它靠「旁边有可改的格子」传达意义;所以下面两种情况**不铺灰**:
- 表格里**一个可编辑列都没有**(纯展示表:列上都没写 `editor`)—— 每格都改不了时,灰底不指向任何东西,铺满反而让整张表看上去像「禁用状态」;
- `editable="false"`(编辑功能整体关掉)—— 同理,此时每一格都不可编辑。
按行禁用是最常用的那种:「已放舱之后状态不许再改」这类条件随行数据变化:
```ts
{
dataIndex: 'status',
title: '状态',
editor: { type: 'select', options: STATUS_OPTIONS, disabled: (row) => row.status === 2 },
}
```
几件需要知道的事:
- 判定是**行数据的派生值**,每次渲染重算 —— 数据变了灰底自己消失 / 出现,业务不用管;
- 整列只读 / 整列禁用的列**不会**被注入 `customCell`(走 td 上的类名),零运行时开销;
只有「按行禁用」的列会常驻注入,代价落在这些列上;
- 「既禁用、又校验不过」的格子显示为**灰**:既然改不了,「这里错了」提示了也没用;
- 灰底用 `--td-bgc` 叠中性色得到,亮 / 暗主题自动跟随。
<demo vue="table/editable.vue" />
下面这张 demo 里,第 2 行(状态=停用)的「备注」列就是**按行禁用**的灰格 —— 不点它也能看出来。
## 操作列
列上加 `actions` 渲染成一排小按钮;`type: 'more'` 的项会收进「更多」下拉。带 `confirm` 的操作先弹确认框,确认后才执行 `onClick`。
@@ -150,11 +221,13 @@ watch(n, (v) => { rows.value = Array.from({ length: v }, (_, i) => makeRow(i)) }
| `rowHeight` | `number` | `30` | 行高,同时驱动虚拟滚动计算 |
| `headerRowHeight` | `number` | `34` | 表头行高 |
| `bordered` | `boolean` | `true` | 是否显示边框(透传给底座的 `bordered: true`)。边框由底座绘制,不再是自绘外框 + 圆角 |
| `stripe` | `boolean` | `false` | 斑马纹(隔行底色)。**固定列会一起上条纹**(见「固定列」):库只把条纹画在 `tr` 上,冻结列的底色必须不透明,组件在它上面补一层同色内阴影接上 |
| `fixedColShadow` | `boolean` | `false` | 固定列被滚动内容遮挡时画 10px 渐变阴影(见「固定列」) |
| `colResizable` | `boolean \| object` | `true` | 列宽拖拽;未绑定 `v-model:columns` 时自动关闭。可用 `{ disabled: (col) => boolean }` 按列禁用 |
| `columnDraggable` | `boolean \| object` | `true` | 列序拖拽;同上要求绑定 `v-model:columns` |
| `rowDraggable` | `boolean` | `false` | 行拖拽排序 |
| `rowSelection` | `TableRowSelection \| null` | `null` | 多选配置,见下 |
| `seq` | `boolean \| { title?, width? }` | `true` | 序号列 |
| `seq` | `boolean \| { title?, width? }` | `true` | 序号列,**固定左侧**(见「固定列」) |
| `activeRowKey` | `string \| number \| null` | `null` | 当前高亮行 |
| `editable` | `boolean` | `true` | 行内编辑总开关 |
| `editorSeamless` | `boolean` | `true` | 编辑器无缝嵌入单元格:格内无外间距、无常驻边框,只有焦点 / hover / 校验态才给视觉提示。传 `false` 回到留间距 + 自身描边的形态 |
@@ -215,7 +288,7 @@ watch(n, (v) => { rows.value = Array.from({ length: v }, (_, i) => makeRow(i)) }
布尔 `true` 被当作「通过」、`false` 也被当作「通过」。要让布尔参与判错,
必须返回字符串。这一点容易踩,故单独说明。
- **校验是派生值,不是一次性状态**:每次渲染都按当前行数据重算,
所以用户把空值改成合法值后,红底与角标在下次渲染就自动消失,
所以用户把空值改成合法值后,红底在下次渲染就自动消失,
**不需要**业务手动清状态,组件内部也不维护任何 error 注册表。
- **校验函数抛错会被吞掉**并降级为「通过」(仅 DEV 打印警告)。
因为它在渲染期被调用,抛出去会让整格渲染失败,比「没有红底」严重得多。
@@ -224,11 +297,15 @@ watch(n, (v) => { rows.value = Array.from({ length: v }, (_, i) => makeRow(i)) }
| 值 | 效果 |
| --- | --- |
| `'cell'`(默认) | 单元格铺**浅红底** + 右上角 **`!` 角标**;聚焦时焦点环转为错误色 |
| `'inline'` | 只在编辑器自身画文字色与 1px 内描边,**不**铺底、**不**加角标 |
| `'cell'`(默认) | 单元格铺**浅红底 / 浅琥珀底**;**正在编辑的那一格例外**(见下) |
| `'inline'` | 只在编辑器自身画文字色与 1px 内描边,**不**铺底 |
浅红底取自设计原型,适合 ERP 明细这类以单元格为视觉单位的场景;
若表格本身已经很密、或那一列很窄(角标会挤到文字),用 `'inline'` 更克制。
若表格本身已经很密,用 `'inline'` 更克制。
**编辑态让位**(默认形态下的行为):正在编辑的那一格**不铺校验底色**,底色回到基色,校验信号只留**语义色焦点环 + 红字**(铺着底色时输入区被染色、对比度下降,录入时「看不清」)。离开该格后底色自动回来(校验本身是按行数据派生的,值改对了自然消失)。
> 早先这里还有一个右上角的 `!` 角标,**已整体去掉**(实际录入反馈):它画的 `!` 会压到文字上 —— 右对齐的数字列最明显(数字正好顶在角标下面),而为了给它让位留出的 14px 右边距又实打实缩窄了录入区。
### `actions`