@lensui/lens-table 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,668 @@
1
+ # @lensui/lens-table
2
+
3
+ `@lensui/lens-table` 是一个面向大数据量的 React 表格组件。主体单元格使用 Canvas 绘制,编辑器、筛选器、菜单和可选中文本使用 DOM 承载,适合需要 10 万行级别滚动、固定列、拖拽、编辑和键盘操作的业务表格。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @lensui/lens-table
9
+ ```
10
+
11
+ React 和 React DOM 18 或更高版本为 peer dependencies。
12
+
13
+ ## 基础使用
14
+
15
+ ```tsx
16
+ import { useState } from 'react';
17
+ import {
18
+ Table,
19
+ type CellChange,
20
+ type GridColumn,
21
+ } from '@lensui/lens-table';
22
+ import '@lensui/lens-table/style.css';
23
+
24
+ interface Row {
25
+ id: number;
26
+ name: string;
27
+ department: string;
28
+ joinedAt: string;
29
+ amount: number;
30
+ }
31
+
32
+ const columns: GridColumn<Row>[] = [
33
+ { key: 'name', title: '姓名', dataIndex: 'name', width: 180, editable: true },
34
+ {
35
+ key: 'department',
36
+ title: '部门',
37
+ dataIndex: 'department',
38
+ width: 160,
39
+ editable: true,
40
+ editor: {
41
+ type: 'select',
42
+ options: [
43
+ { label: '研发', value: '研发' },
44
+ { label: '设计', value: '设计' },
45
+ ],
46
+ },
47
+ },
48
+ {
49
+ key: 'joinedAt',
50
+ title: '入职日期',
51
+ dataIndex: 'joinedAt',
52
+ width: 160,
53
+ editable: true,
54
+ editor: { type: 'date' },
55
+ },
56
+ { key: 'amount', title: '金额', dataIndex: 'amount', width: 140, align: 'right' },
57
+ ];
58
+
59
+ export function Example() {
60
+ const [rows, setRows] = useState<Row[]>([
61
+ { id: 1, name: '用户 1', department: '研发', joinedAt: '2026-01-15', amount: 120 },
62
+ ]);
63
+
64
+ const handleCellChange = ({ row, columnKey, value }: CellChange<Row>) => {
65
+ setRows((current) =>
66
+ current.map((item) =>
67
+ item.id === row.id ? { ...item, [columnKey]: value } : item,
68
+ ),
69
+ );
70
+ };
71
+
72
+ return (
73
+ <Table
74
+ columns={columns}
75
+ rows={rows}
76
+ height={560}
77
+ onCellChange={handleCellChange}
78
+ />
79
+ );
80
+ }
81
+ ```
82
+
83
+ ## Table 参数
84
+
85
+ | 参数 | 类型 | 默认值 | 说明 |
86
+ | ------------------------------ | ------------------------------------------------------------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
87
+ | `columns` | `GridColumn<Row>[]` | 必填 | 表格列配置。Canvas 绘制、DOM 编辑器和交互都依赖这份配置。 |
88
+ | `rows` | `Row[]` | 必填 | 表格数据。组件不会直接修改数据,编辑后通过事件通知外部更新。 |
89
+ | `rowKey` | `keyof Row \| ((row, index) => string \| number)` | `id` | 行唯一标识。默认读取每行的`id` 字段;如果数据不是 `id` 主键,请显式传入字段名或函数。 |
90
+ | `width` | `number \| string` | `100%` | 表格宽度。数字按像素处理,字符串可传`100%`、`80vw` 等。 |
91
+ | `height` | `number \| string` | `100%` | 表格高度。默认继承父容器高度;如果父容器没有可计算高度,则回落到`480px`。 |
92
+ | `rowHeight` | `number` | `40` | 每行高度,固定行高用于高性能虚拟滚动。 |
93
+ | `headerHeight` | `number` | `44` | 表头高度,单位 px。 |
94
+ | `fixedHeader` | `boolean` | `true` | 表头是否固定在顶部。 |
95
+ | `locale` | `'zh-CN' \| 'en-US' \| LocaleConfig` | `'zh-CN'` | 国际化配置。传字符串时使用内置中英文文案;传对象时可通过`language` 指定语言,并通过 `labels` 覆盖文案。 |
96
+ | `verticalBorderless` | `boolean` | `false` | 是否弱化竖向边框。开启后隐藏单元格之间的竖线,但保留横向分隔线。 |
97
+ | `striped` | `boolean \| string` | `false` | 是否显示斑马纹行背景。传`true` 使用主题变量,传颜色字符串可自定义斑马纹颜色,例如 `'#f6faff'`。 |
98
+ | `highlight` | `{ editedCells?: boolean \| string; insertedRows?: boolean \| string }` | `{}` | 高亮反馈配置。`editedCells` 控制编辑过的单元格底色,`insertedRows` 控制乐观插入行底色;传 `true` 使用默认色,传颜色字符串可自定义。 |
99
+ | `cellSpans` | `Array<{ rowKey?: GridKey; rowIndex?: number; columnKey: string; rowSpan?: number; colSpan?: number }>` | `[]` | 合并主体单元格配置。推荐使用 `rowKey` 定位;横向合并会限制在同一个固定列分区内,工具列不会参与合并。 |
100
+ | `loading` | `boolean` | `false` | 显示加载态。 |
101
+ | `loadingContent` | `ReactNode` | - | 自定义加载内容。传入后,首次加载和刷新加载不再区分,都会统一展示这个内容。 |
102
+ | `virtualized` | `boolean \| { enabled?: boolean; overscan?: number }` | `true` | 虚拟渲染配置。传`false` 时渲染全部行列;传对象时可通过 `enabled` 开关,并用 `overscan` 配置可见范围外的缓冲行列数量,默认 `4`。 |
103
+ | `tooltip` | `boolean \| { header?: boolean; cell?: boolean }` | `true` | 文本 tooltip 总配置。传`true` 时表头和单元格 hover 都显示完整文本;传 `false` 可关闭;传对象可分别控制表头和单元格。 |
104
+ | `summary` | `boolean \| { position?: 'top' \| 'bottom'; verticalBordered?: boolean; emptyValue?: ReactNode }` | `false` | 表格级合计行配置。默认不显示;传 `true` 或对象即开启合计行,对象可控制显示在表头下方或底部、是否显示内部竖线,以及无合计值单元格的填充内容。 |
105
+ | `rowNumber` | `boolean` | `true` | 是否显示序号列。默认开启并自动生成左侧序号列,不需要在`columns` 中配置;传 `false` 可关闭。 |
106
+ | `selectedCell` | `GridSelection \| null` | 非受控 | 受控单元格选中状态。 |
107
+ | `defaultSelectedCell` | `GridSelection \| null` | `null` | 非受控模式下的默认选中单元格。 |
108
+ | `onSelectedCellChange` | `(selection) => void` | - | 单元格选中变化回调。 |
109
+ | `rangeSelection` | `boolean` | `false` | 是否允许鼠标拖动选择多个单元格。开启后支持范围复制、范围右键菜单和右下角拖拽扩展范围。 |
110
+ | `rowSelection` | `boolean \| AxisSelectionConfig` | `false` | 开启行选择。可传`{ mode: 'single' \| 'multiple' }`;开启后组件自动生成左侧选择列,不需要在 `columns` 中配置。 |
111
+ | `selectedRowKeys` | `GridKey[]` | 非受控 | 受控行选择 key 列表。 |
112
+ | `defaultSelectedRowKeys` | `GridKey[]` | `[]` | 非受控模式下默认选中的行 key。 |
113
+ | `onSelectedRowKeysChange` | `(keys, rows, indices) => void` | - | 行选择变化回调。 |
114
+ | `columnSelection` | `boolean \| AxisSelectionConfig` | `false` | 开启列选择。可传`{ mode: 'single' \| 'multiple' }`。 |
115
+ | `selectedColumnKeys` | `string[]` | 非受控 | 受控列选择 key 列表。 |
116
+ | `defaultSelectedColumnKeys` | `string[]` | `[]` | 非受控模式下默认选中的列 key。 |
117
+ | `onSelectedColumnKeysChange` | `(keys) => void` | - | 列选择变化回调。 |
118
+ | `columnDraggable` | `boolean` | `true` | 是否允许拖拽调整列顺序。默认开启,普通列默认显示表头拖拽入口,传`false` 可关闭。 |
119
+ | `columnResizable` | `boolean` | `false` | 是否允许拖拽调整列宽。 |
120
+ | `onColumnOrderChange` | `(sourceIndex, targetIndex) => void` | - | 列拖拽排序回调。需要在外部更新`columns`。 |
121
+ | `onColumnResize` | `(columnKey, width) => void` | - | 列宽变化回调。需要在外部更新对应列宽。 |
122
+ | `rowDraggable` | `boolean` | `false` | 是否允许拖拽调整行顺序。开启后组件自动生成左侧拖拽手柄列,不需要在`columns` 中配置。 |
123
+ | `onRowOrderChange` | `(sourceIndex, targetIndex) => void` | - | 行拖拽排序回调。需要在外部更新`rows`。 |
124
+ | `onInsertRows` | `(event) => void` | - | 右键菜单插入行回调。 |
125
+ | `onDeleteRows` | `(event) => void` | - | 右键菜单删除行回调。 |
126
+ | `sortState` | `GridSortState \| null` | 非受控 | 受控排序状态。组件只管理 UI 状态,数据排序由外部完成。 |
127
+ | `onSortStateChange` | `(state) => void` | - | 点击表头排序时触发。 |
128
+ | `filterValues` | `Record<string, string>` | 非受控 | 受控筛选值。组件只管理输入值,数据过滤由外部完成。 |
129
+ | `onFilterValuesChange` | `(values) => void` | - | 表头筛选值变化回调。 |
130
+ | `onCellChange` | `(change) => void` | - | 单元格编辑提交回调。 |
131
+ | `onCellContextMenu` | `(event) => void` | - | 单元格右键回调,可用于扩展自定义菜单逻辑。 |
132
+ | `contextMenu` | `boolean \| { header?, cell?, range? }` | `true` | `true` 开启默认右键菜单,`false` 关闭右键菜单,传对象可自定义菜单项、顺序和分割线。 |
133
+ | `emptyContent` | `ReactNode` | 内置空状态 | 自定义空数据内容。 |
134
+ | `className` | `string` | - | 根节点 className。 |
135
+ | `style` | `CSSProperties` | - | 根节点内联样式,可用于传入主题 CSS 变量。 |
136
+ | `ariaLabel` | `string` | `Table` | 表格区域无障碍名称。 |
137
+
138
+ ## 高亮和范围选择
139
+
140
+ 编辑高亮、插入行高亮和鼠标拖动多单元格选择默认都不开启。需要这些视觉反馈或批量选择能力时,通过表格级配置显式开启:
141
+
142
+ ```tsx
143
+ <Table
144
+ columns={columns}
145
+ rows={rows}
146
+ highlight={{
147
+ editedCells: true,
148
+ insertedRows: '#c8ead4',
149
+ }}
150
+ rangeSelection
151
+ />
152
+ ```
153
+
154
+ `highlight.editedCells` 和 `highlight.insertedRows` 传 `true` 时使用主题默认色;传颜色字符串时只覆盖对应高亮色。`rangeSelection` 开启后,可以用鼠标拖动选择多个单元格,并在范围内复制或打开范围右键菜单。
155
+
156
+ ## 合并单元格
157
+
158
+ 通过 `cellSpans` 配置主体区域的合并单元格。每个合并项以左上角单元格为锚点,`rowSpan` 和 `colSpan` 分别控制覆盖的行数和列数:
159
+
160
+ ```tsx
161
+ <Table
162
+ columns={columns}
163
+ rows={rows}
164
+ cellSpans={[
165
+ { rowKey: rows[2].id, columnKey: 'role', rowSpan: 2 },
166
+ { rowKey: rows[7].id, columnKey: 'department', colSpan: 2 },
167
+ ]}
168
+ />
169
+ ```
170
+
171
+ `rowKey` 优先于 `rowIndex`,在排序、过滤或插入删除行后更稳定。被合并覆盖的单元格不会单独绘制或命中,点击覆盖区域会选中合并区域的锚点单元格。横向合并不会跨越固定左列、滚动列、固定右列这三个区域;如果声明跨区,组件会自动收缩到当前区域内。
172
+
173
+ ## Column 参数
174
+
175
+ | 参数 | 类型 | 默认值 | 说明 |
176
+ | -------------- | -------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
177
+ | `key` | `string` | 必填 | 列唯一标识。排序、筛选、选择、编辑回调都使用它。 |
178
+ | `title` | `string` | 必填 | 表头展示文本。 |
179
+ | `dataIndex` | `keyof Row` | - | 从行数据中读取和写入的字段。工具列可不传。 |
180
+ | `width` | `number` | `140` | 列宽,单位 px。最小宽度由内部布局保护。 |
181
+ | `fixed` | `'left' \| 'right'` | - | 固定列位置。左/右固定列会覆盖滚动列并显示阴影。 |
182
+ | `align` | `'left' \| 'center' \| 'right'` | `'left'` | 单元格文本和拖拽预览文本对齐方式。 |
183
+ | `editable` | `boolean` | `false` | 是否允许双击或按 Enter 进入编辑。 |
184
+ | `editor` | `EditorConfig` | `{ type: 'text' }` | 内置编辑器配置。详见下方编辑器表。未配置时默认使用文本编辑器。 |
185
+ | `sortable` | `boolean` | `true` | 表头显示排序按钮,并触发`onSortStateChange`。普通数据列默认开启,传 `false` 可关闭。 |
186
+ | `filterable` | `boolean` | `true` | 表头显示筛选按钮,并触发`onFilterValuesChange`。普通数据列默认开启,传 `false` 可关闭。 |
187
+ | `formatter` | `(value, row, rowIndex) => string` | - | 自定义展示文本。异常会被隔离并显示渲染错误文案。 |
188
+ | `renderCell` | `(value, row, rowIndex) => ReactNode` | - | 自定义单元格 React 内容。适合标签、徽标、图标组合等内置编辑器无法表达的展示。 |
189
+ | `summary` | `boolean \| ((rows, column) => ReactNode)` | `false` | 是否在合计行显示该列合计。传`true` 自动累加数字值;传函数可自定义合计内容。 |
190
+ | `cellStyle` | `(value, row, rowIndex) => { color?: string; backgroundColor?: string }` | - | 自定义单元格绘制样式。支持文本颜色和背景色,适合金额、状态等条件高亮。 |
191
+
192
+ ## 列合计
193
+
194
+ 合计行由表格级 `summary` 和列级 `columns[].summary` 共同控制:表格级 `summary` 控制是否显示、位置、内部竖线和空白填充;列级 `summary` 控制这一列显示什么合计内容。默认不显示合计行;传 `summary={true}` 或对象即开启合计行。合计行不参与虚拟滚动、排序、筛选、选择和编辑。
195
+
196
+ ```tsx
197
+ <Table
198
+ columns={columns}
199
+ rows={rows}
200
+ summary={{ position: 'bottom' }}
201
+ />
202
+ ```
203
+
204
+ 表格级 `summary` 支持:
205
+
206
+ | 属性 | 类型 | 默认值 | 说明 |
207
+ | -------------------- | -------------------- | ------------ | ----------------------------------------------------------------------------------------- |
208
+ | `position` | `'top' \| 'bottom'` | `'bottom'` | 合计行位置。`top` 显示在表头下方,`bottom` 显示在表格底部。 |
209
+ | `verticalBordered` | `boolean` | `true` | 是否显示合计行内部竖线。全局 `verticalBorderless` 为 `true` 时优先隐藏竖线。 |
210
+ | `emptyValue` | `ReactNode` | `''` | 合计行没有值的单元格填充内容。默认显示为空。 |
211
+
212
+ ```tsx
213
+ const columns: GridColumn<Row>[] = [
214
+ {
215
+ key: 'amount',
216
+ title: '金额',
217
+ dataIndex: 'amount',
218
+ align: 'right',
219
+ formatter: (value) => `¥${Number(value).toLocaleString('zh-CN')}`,
220
+ summary: true,
221
+ },
222
+ ];
223
+ ```
224
+
225
+ `summary: true` 会自动累加该列的数字值,非数字值会被忽略;如果列配置了 `formatter`,合计结果也会复用该 `formatter` 展示。
226
+
227
+ 需要自定义合计内容时,可以传函数:
228
+
229
+ ```tsx
230
+ const columns: GridColumn<Row>[] = [
231
+ {
232
+ key: 'name',
233
+ title: '姓名',
234
+ dataIndex: 'name',
235
+ summary: (rows) => `共 ${rows.length} 条`,
236
+ },
237
+ {
238
+ key: 'amount',
239
+ title: '金额',
240
+ dataIndex: 'amount',
241
+ summary: (rows) =>
242
+ rows.reduce((total, row) => total + Number(row.amount ?? 0), 0),
243
+ },
244
+ ];
245
+ ```
246
+
247
+ ## 内置编辑器
248
+
249
+ | `editor.type` | 数据格式示例 | 说明 |
250
+ | ------------------- | ----------------------------------------- | ------------------------------------------------------------------- |
251
+ | `text` | `''` | 默认文本编辑器。`editable: true` 且不传 `editor` 时也会使用它。 |
252
+ | `select` | `''` | 下拉选择。需要传`options: Array<{ label; value }>`。 |
253
+ | `date` | `'2026-01-15'` | 日期选择器。 |
254
+ | `time` | `'09:30'` 或 `'09:30:15'` | 时间选择器。是否显示秒由`format` 或当前值推断。 |
255
+ | `date-time` | `'2026-01-15 09:30'` | 日期时间选择器。日期和时间默认使用空格连接。 |
256
+ | `year` | `'2026'` | 年选择器。 |
257
+ | `month` | `'2026-01'` | 年月选择器。 |
258
+ | `date-range` | `'2026-01-01 ~ 2026-01-15'` | 日期范围选择器。 |
259
+ | `time-range` | `'09:00 ~ 18:00'` | 时间范围选择器。 |
260
+ | `date-time-range` | `'2026-01-01 09:00 ~ 2026-01-01 18:00'` | 日期时间范围选择器,包含日期和时间面板。 |
261
+
262
+ 可选 `format` 用于控制提交到数据里的字符串格式,例如:
263
+
264
+ ```tsx
265
+ {
266
+ key: 'startAt',
267
+ title: '开始时间',
268
+ dataIndex: 'startAt',
269
+ editable: true,
270
+ editor: { type: 'date-time', format: 'yyyy-MM-dd HH:mm:ss' },
271
+ }
272
+ ```
273
+
274
+ ## 右键菜单
275
+
276
+ `contextMenu` 支持三种模式:
277
+
278
+ - `true`:开启默认右键菜单。不传时也是这个行为。
279
+ - `false`:关闭组件右键菜单。
280
+ - 对象:自定义表头、单元格和范围选择菜单。对象里的 `header`、`cell`、`range` 可以传 `true` 使用该区域默认菜单,传 `false` 关闭该区域菜单,传数组自定义该区域菜单,也可以传 `{ exclude }` 基于默认菜单关闭部分内置项。
281
+
282
+ 对象配置里的数组顺序就是菜单显示顺序,移除某个内置 key 就会关闭该项,`'|'` 会渲染分割线。也可以插入自定义菜单项。直接写 key 使用内置行为;传对象且 `key` 命中内置项时,可以覆盖 `label`、`icon`、`shortcut`、`disabled`、`hidden`,传 `onClick` 时会替换该内置行为。
283
+
284
+ ```tsx
285
+ <Table
286
+ columns={columns}
287
+ rows={rows}
288
+ contextMenu={{
289
+ header: true,
290
+ cell: [
291
+ 'edit',
292
+ 'copy',
293
+ 'undo',
294
+ 'clear',
295
+ '|',
296
+ 'select-column',
297
+ {
298
+ key: 'inspect',
299
+ label: '查看详情',
300
+ onClick: ({ row }) => console.log(row),
301
+ },
302
+ ],
303
+ range: false,
304
+ }}
305
+ />
306
+ ```
307
+
308
+ 如果只是想关闭默认菜单中的一两个内置项,可以用 `exclude`:
309
+
310
+ ```tsx
311
+ <Table
312
+ columns={columns}
313
+ rows={rows}
314
+ contextMenu={{
315
+ cell: { exclude: ['undo', 'delete-row'] },
316
+ }}
317
+ />
318
+ ```
319
+
320
+ 对象菜单项支持这些属性:
321
+
322
+ | 属性 | 说明 |
323
+ | ------------ | ------------------------------------------------------------------------------------------------- |
324
+ | `key` | 菜单项唯一标识。命中内置 key 时覆盖内置菜单项;未命中时表示自定义菜单项。 |
325
+ | `label` | 菜单主文本,支持`ReactNode`。内置项不传时使用默认文案;自定义项建议必传。 |
326
+ | `icon` | 菜单左侧图标,支持`ReactNode`。内置项不传时使用默认图标;传入后会覆盖默认图标。 |
327
+ | `shortcut` | 菜单右侧快捷键提示,支持`ReactNode`,只负责展示,不会自动绑定键盘事件。 |
328
+ | `disabled` | 是否禁用菜单项。可以传布尔值,也可以传`(context) => boolean` 按当前表头、单元格或范围动态判断。 |
329
+ | `danger` | 是否使用危险操作样式。 |
330
+ | `hidden` | 是否隐藏菜单项。可以传布尔值,也可以传`(context) => boolean` 按当前表头、单元格或范围动态判断。 |
331
+ | `onClick` | 点击回调。命中内置 key 时会替换内置行为;自定义菜单项通常需要提供。 |
332
+ | `children` | 子菜单配置,只支持数组。数组项同样支持对象菜单项属性,也可以继续嵌套`children`。 |
333
+ | `render` | 自定义右侧面板内容,支持`ReactNode` 或 `(context) => ReactNode`。 |
334
+
335
+ `children` 用于配置多级菜单:
336
+
337
+ ```tsx
338
+ <Table
339
+ columns={columns}
340
+ rows={rows}
341
+ contextMenu={{
342
+ cell: [
343
+ 'copy',
344
+ {
345
+ key: 'more',
346
+ label: '更多操作',
347
+ children: [
348
+ { key: 'copy-id', label: '复制 ID', onClick: ({ row }) => console.log(row.id) },
349
+ '|',
350
+ {
351
+ key: 'export',
352
+ label: '导出',
353
+ children: [
354
+ { key: 'export-json', label: '导出 JSON', onClick: ({ row }) => console.log(row) },
355
+ ],
356
+ },
357
+ ],
358
+ },
359
+ ],
360
+ }}
361
+ />
362
+ ```
363
+
364
+ `render` 用于渲染自定义面板:
365
+
366
+ ```tsx
367
+ <Table
368
+ columns={columns}
369
+ rows={rows}
370
+ contextMenu={{
371
+ cell: [
372
+ {
373
+ key: 'custom-panel',
374
+ label: '自定义面板',
375
+ render: ({ row }) => <div style={{ padding: 12 }}>{row.name}</div>,
376
+ },
377
+ ],
378
+ }}
379
+ />
380
+ ```
381
+
382
+ 内置 key:
383
+
384
+ | 区域 | key |
385
+ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
386
+ | 表头 | `copy`, `select-column` |
387
+ | 单元格 | `edit`, `copy`, `annotation`, `select-row`, `select-column`, `insert-above`, `insert-below`, `move-up`, `move-down`, `undo`, `clear`, `delete-row` |
388
+ | 范围选择 | `copy`, `annotation`, `clear` |
389
+ | 分割线 | `\|`,可写在 `header`、`cell`、`range` 数组中,用于渲染菜单分割线。 |
390
+
391
+ ## 表头默认能力
392
+
393
+ 普通数据列默认显示三个表头入口:拖拽排序、正序/倒序排序、筛选。大多数列不需要显式写 `sortable: true` 或 `filterable: true`。
394
+
395
+ ```tsx
396
+ const columns: GridColumn<Row>[] = [
397
+ { key: 'name', title: '姓名', dataIndex: 'name' },
398
+ { key: 'amount', title: '金额', dataIndex: 'amount', align: 'right' },
399
+ ];
400
+
401
+ <Table columns={columns} rows={rows} />;
402
+ ```
403
+
404
+ 如果某一列不需要排序或筛选,显式传 `false`:
405
+
406
+ ```tsx
407
+ {
408
+ key: 'remark',
409
+ title: '备注',
410
+ dataIndex: 'remark',
411
+ sortable: false,
412
+ filterable: false,
413
+ }
414
+ ```
415
+
416
+ ## 排序和筛选
417
+
418
+ 组件只负责展示排序/筛选交互,不会直接改变 `rows`。业务侧需要根据回调结果自行排序、过滤。
419
+
420
+ ```tsx
421
+ const [sortState, setSortState] = useState<GridSortState | null>(null);
422
+ const [filterValues, setFilterValues] = useState<Record<string, string>>({});
423
+
424
+ const visibleRows = useMemo(() => {
425
+ const filtered = rows.filter((row) =>
426
+ Object.entries(filterValues).every(([key, value]) =>
427
+ String(row[key as keyof Row] ?? '').includes(value),
428
+ ),
429
+ );
430
+
431
+ if (!sortState) return filtered;
432
+
433
+ return filtered.slice().sort((a, b) => {
434
+ const left = a[sortState.columnKey as keyof Row];
435
+ const right = b[sortState.columnKey as keyof Row];
436
+ const result = String(left).localeCompare(String(right));
437
+ return sortState.direction === 'asc' ? result : -result;
438
+ });
439
+ }, [rows, sortState, filterValues]);
440
+
441
+ <Table
442
+ columns={columns}
443
+ rows={visibleRows}
444
+ sortState={sortState}
445
+ filterValues={filterValues}
446
+ onSortStateChange={setSortState}
447
+ onFilterValuesChange={setFilterValues}
448
+ />;
449
+ ```
450
+
451
+ ## 拖拽和列宽
452
+
453
+ 拖拽排序和调整列宽都采用外部受控数据模式:组件告诉你发生了什么,真正的 `rows` 或 `columns` 更新由你完成。
454
+
455
+ `columnDraggable` 默认开启,普通列会自动显示表头拖拽入口。行选择和行拖拽是表格级配置:开启 `rowSelection` 后自动生成左侧选择列,开启 `rowDraggable` 后自动生成左侧拖拽手柄列,不需要在 `columns` 里配置 `rowSelection` 或 `rowDragHandle`。
456
+
457
+ ```tsx
458
+ <Table
459
+ columns={columns}
460
+ rows={rows}
461
+ rowSelection={{ mode: 'multiple' }}
462
+ columnResizable
463
+ rowDraggable
464
+ onColumnOrderChange={(sourceIndex, targetIndex) => {
465
+ setColumns((current) => {
466
+ const next = current.slice();
467
+ const [column] = next.splice(sourceIndex, 1);
468
+ next.splice(targetIndex, 0, column);
469
+ return next;
470
+ });
471
+ }}
472
+ onColumnResize={(columnKey, width) => {
473
+ setColumns((current) =>
474
+ current.map((column) =>
475
+ column.key === columnKey ? { ...column, width } : column,
476
+ ),
477
+ );
478
+ }}
479
+ onRowOrderChange={(sourceIndex, targetIndex) => {
480
+ setRows((current) => {
481
+ const next = current.slice();
482
+ const [row] = next.splice(sourceIndex, 1);
483
+ next.splice(targetIndex, 0, row);
484
+ return next;
485
+ });
486
+ }}
487
+ />;
488
+ ```
489
+
490
+ ## 虚拟渲染
491
+
492
+ 默认开启虚拟渲染,只绘制可见区域和少量缓冲区域。缓冲数量默认是 `4`,可以通过对象形式调整。
493
+
494
+ ```tsx
495
+ <Table
496
+ columns={columns}
497
+ rows={rows}
498
+ virtualized={{ enabled: true, overscan: 6 }}
499
+ />
500
+ ```
501
+
502
+ 如果数据量很小,或需要一次性渲染全部 DOM 文本层,可以关闭虚拟渲染:
503
+
504
+ ```tsx
505
+ <Table
506
+ columns={columns}
507
+ rows={rows}
508
+ virtualized={false}
509
+ />
510
+ ```
511
+
512
+ ## 加载状态
513
+
514
+ 默认加载态会区分两种场景:首次加载显示骨架层,已有数据刷新时显示 spinner 遮罩。如果传入 `loadingContent`,组件不会再区分这两种场景,所有加载状态都会统一展示自定义内容。
515
+
516
+ ```tsx
517
+ <Table
518
+ columns={columns}
519
+ rows={rows}
520
+ loading={loading}
521
+ loadingContent={<div className="table-loading">加载中...</div>}
522
+ />
523
+ ```
524
+
525
+ ## 主题适配
526
+
527
+ 样式通过 CSS 变量开放。可以在全局、页面容器或单个表格上覆盖变量。
528
+
529
+ ```tsx
530
+ <Table
531
+ columns={columns}
532
+ rows={rows}
533
+ style={{
534
+ '--rvg-color-primary': '#10b981',
535
+ '--rvg-color-bg': '#0f172a',
536
+ '--rvg-color-text': '#e5e7eb',
537
+ '--rvg-color-grid': '#334155',
538
+ } as React.CSSProperties}
539
+ />
540
+ ```
541
+
542
+ 常用变量:
543
+
544
+ | 变量 | 说明 |
545
+ | ----------------------------------- | ---------------------------------------------------------------------------------- |
546
+ | `--rvg-color-bg` | 表格背景色。 |
547
+ | `--rvg-color-header-bg` | 表头背景色。 |
548
+ | `--rvg-color-text` | 主文本颜色。 |
549
+ | `--rvg-color-muted` | 次级文本颜色。 |
550
+ | `--rvg-color-icon` | 图标颜色。 |
551
+ | `--rvg-color-grid` | Canvas 网格线颜色。 |
552
+ | `--rvg-color-border` | 弹层、输入框等 DOM 边框颜色。 |
553
+ | `--rvg-color-primary` | 选中、按钮、焦点等强调色。 |
554
+ | `--rvg-color-selection-fill` | 单元格选中背景。 |
555
+ | `--rvg-color-axis-selection-fill` | 行/列选中背景。 |
556
+ | `--rvg-color-edited-fill` | 已编辑单元格背景。 |
557
+ | `--rvg-color-stripe` | `striped={true}` 时的斑马纹背景。也可以直接通过 `striped="#f6faff"` 单独指定。 |
558
+
559
+ ## 国际化
560
+
561
+ `locale` 是统一的国际化入口。直接传 `'zh-CN'` 或 `'en-US'` 时会使用组件内置文案;传对象时通过 `language` 指定基础语言,并在 `labels` 里覆盖你关心的字段。
562
+
563
+ 目前组件内置文案只提供中文和英文两套:`zh-CN`、`en-US`。日期选择器的底层库可以接受更多地区格式,但组件菜单、按钮、提示文案需要你通过对象模式自行覆盖。
564
+
565
+ ```tsx
566
+ <Table
567
+ columns={columns}
568
+ rows={rows}
569
+ locale="en-US"
570
+ />
571
+ ```
572
+
573
+ ```tsx
574
+ <Table
575
+ columns={columns}
576
+ rows={rows}
577
+ locale={{
578
+ language: 'en-US',
579
+ labels: {
580
+ choose: 'Choose',
581
+ confirm: 'OK',
582
+ reset: 'Reset',
583
+ search: 'Search',
584
+ empty: 'No data',
585
+ deleteConfirmDescription: (count) => `Delete ${count} row(s)?`,
586
+ },
587
+ }}
588
+ />
589
+ ```
590
+
591
+ 如果项目里已经有自己的 i18n 封装,可以把翻译函数的结果映射到 `locale` 对象里传入:
592
+
593
+ ```tsx
594
+ import { Table, type TableLocaleConfig } from '@lensui/lens-table';
595
+ import { useTranslation } from 'react-i18next';
596
+
597
+ function UserTable() {
598
+ const { t, i18n } = useTranslation();
599
+
600
+ const gridLocale: TableLocaleConfig = {
601
+ language: i18n.language.startsWith('en') ? 'en-US' : 'zh-CN',
602
+ labels: {
603
+ choose: t('grid.choose'),
604
+ confirm: t('grid.confirm'),
605
+ reset: t('grid.reset'),
606
+ search: t('grid.search'),
607
+ empty: t('grid.empty'),
608
+ filterWithValue: (value) => t('grid.filterWithValue', { value }),
609
+ filterColumn: (title) => t('grid.filterColumn', { title }),
610
+ deleteConfirmDescription: (count) =>
611
+ t('grid.deleteConfirmDescription', { count }),
612
+ },
613
+ };
614
+
615
+ return (
616
+ <Table
617
+ columns={columns}
618
+ rows={rows}
619
+ locale={gridLocale}
620
+ />
621
+ );
622
+ }
623
+ ```
624
+
625
+ 也可以只覆盖部分字段,未传的字段会从 `language` 对应的内置中英文文案里补齐:
626
+
627
+ ```tsx
628
+ <Table
629
+ locale={{
630
+ language: 'zh-CN',
631
+ labels: {
632
+ empty: i18n.t('common.empty'),
633
+ confirm: i18n.t('common.confirm'),
634
+ },
635
+ }}
636
+ />
637
+ ```
638
+
639
+ ## 常用交互
640
+
641
+ | 操作 | 行为 |
642
+ | ---------------- | ---------------------------------------- |
643
+ | 单击单元格 | 选中单元格。 |
644
+ | 拖拽单元格 | 创建多单元格范围选择。 |
645
+ | 双击可编辑单元格 | 进入编辑。 |
646
+ | `Enter` | 进入编辑或提交当前编辑。 |
647
+ | `Escape` | 取消编辑或关闭弹层。 |
648
+ | 表头排序按钮 | 切换升序、降序、无排序。 |
649
+ | 表头筛选按钮 | 打开当前列筛选输入。 |
650
+ | 右键单元格 | 打开复制、清空、编辑、插入、删除等菜单。 |
651
+
652
+ ## 开发
653
+
654
+ ```bash
655
+ pnpm install
656
+ pnpm dev
657
+ pnpm check
658
+ ```
659
+
660
+ ## 发布
661
+
662
+ 1. 在 `package.json` 中确认 npm 包名、版本和仓库信息。
663
+ 2. 执行 `pnpm check`,确保类型检查、测试和构建通过。
664
+ 3. 更新版本号并创建发布。
665
+
666
+ ## License
667
+
668
+ MIT