stk-table-vue 1.2.5 → 1.2.6

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.
Files changed (65) hide show
  1. package/README.md +194 -194
  2. package/lib/{Dropdown-C4c7Nmgf.js → Dropdown-BAANgg_5.js} +2 -2
  3. package/lib/{StkTable-CzktjLlV.js → StkTable-CGcxwpdb.js} +2 -1
  4. package/lib/stk-table-vue.js +3 -3
  5. package/lib/style.css +1 -1
  6. package/package.json +93 -93
  7. package/src/StkTable/StkTable.vue +2362 -2359
  8. package/src/StkTable/components/DragHandle.vue +9 -9
  9. package/src/StkTable/components/SortIcon.vue +6 -6
  10. package/src/StkTable/components/TreeNodeCell.vue +19 -19
  11. package/src/StkTable/components/TriangleIcon.vue +3 -3
  12. package/src/StkTable/const.ts +50 -50
  13. package/src/StkTable/custom-cells/ChangeCell/ChangeCell.less +34 -34
  14. package/src/StkTable/custom-cells/ChangeCell/createChangeCell.ts +90 -90
  15. package/src/StkTable/custom-cells/ChangeCell/index.ts +4 -4
  16. package/src/StkTable/custom-cells/CheckboxCell/CheckboxCell.less +14 -14
  17. package/src/StkTable/custom-cells/CheckboxCell/CheckboxCell.vue +56 -56
  18. package/src/StkTable/custom-cells/CheckboxCell/createCheckboxCell.ts +146 -146
  19. package/src/StkTable/custom-cells/CheckboxCell/index.ts +5 -5
  20. package/src/StkTable/custom-cells/FilterCell/Dropdown/index.ts +16 -16
  21. package/src/StkTable/custom-cells/FilterCell/Dropdown/index.vue +186 -186
  22. package/src/StkTable/custom-cells/FilterCell/Filter.less +118 -118
  23. package/src/StkTable/custom-cells/FilterCell/Filter.vue +66 -66
  24. package/src/StkTable/custom-cells/FilterCell/createFilterCell.ts +120 -120
  25. package/src/StkTable/custom-cells/FilterCell/index.ts +6 -6
  26. package/src/StkTable/custom-cells/FilterCell/types.ts +43 -43
  27. package/src/StkTable/custom-cells/NumberCell/createNumberCell.ts +43 -43
  28. package/src/StkTable/custom-cells/NumberCell/index.ts +2 -2
  29. package/src/StkTable/custom-cells/utils/formatNumber.ts +137 -137
  30. package/src/StkTable/features/const.ts +1 -1
  31. package/src/StkTable/features/index.ts +1 -1
  32. package/src/StkTable/features/useAreaSelection.ts +1240 -1240
  33. package/src/StkTable/index.ts +20 -20
  34. package/src/StkTable/mergeCellsCache.ts +69 -69
  35. package/src/StkTable/registerFeature.ts +40 -40
  36. package/src/StkTable/style.less +791 -791
  37. package/src/StkTable/types/highlightDimOptions.ts +28 -28
  38. package/src/StkTable/types/index.ts +503 -503
  39. package/src/StkTable/useAutoResize.ts +87 -87
  40. package/src/StkTable/useColResize.ts +202 -202
  41. package/src/StkTable/useFixedCol.ts +168 -168
  42. package/src/StkTable/useFixedStyle.ts +75 -75
  43. package/src/StkTable/useGetFixedColPosition.ts +63 -63
  44. package/src/StkTable/useHighlight.ts +270 -270
  45. package/src/StkTable/useIndexResolver.ts +29 -29
  46. package/src/StkTable/useKeyboardArrowScroll.ts +114 -114
  47. package/src/StkTable/useMaxRowSpan.ts +60 -60
  48. package/src/StkTable/useMergeCells.ts +541 -541
  49. package/src/StkTable/useRowExpand.ts +81 -81
  50. package/src/StkTable/useScrollRowByRow.ts +96 -96
  51. package/src/StkTable/useScrollbar.ts +177 -177
  52. package/src/StkTable/useSorter.ts +267 -267
  53. package/src/StkTable/useTableColumns.ts +142 -142
  54. package/src/StkTable/useThDrag.ts +94 -94
  55. package/src/StkTable/useTrDrag.ts +100 -100
  56. package/src/StkTable/useTree.ts +253 -253
  57. package/src/StkTable/useVirtualScroll.ts +1032 -1032
  58. package/src/StkTable/useWheeling.ts +23 -23
  59. package/src/StkTable/utils/constRefUtils.ts +29 -29
  60. package/src/StkTable/utils/index.ts +331 -331
  61. package/src/StkTable/utils/rowHeightFenwickTree.ts +74 -74
  62. package/src/StkTable/utils/useTriggerRef.ts +33 -33
  63. package/src/VirtualTree.vue +622 -622
  64. package/src/VirtualTreeSelect.vue +367 -367
  65. package/src/vite-env.d.ts +10 -10
@@ -1,2359 +1,2362 @@
1
- <!-- eslint-disable vue/attribute-hyphenation -->
2
- <template>
3
- <div
4
- ref="tableContainerRef"
5
- class="stk-table"
6
- :class="{
7
- virtual,
8
- 'virtual-x': virtualX,
9
- 'vt-on': virtual_on,
10
- light: theme === 'light',
11
- dark: theme === 'dark',
12
- headless,
13
- 'is-col-resizing': isColResizing,
14
- 'col-resizable': props.colResizable,
15
- bordered: props.bordered,
16
- [`bordered-${props.bordered}`]: typeof props.bordered === 'string',
17
- stripe: props.stripe,
18
- 'cell-hover': props.cellHover,
19
- 'cell-active': props.cellActive,
20
- 'row-hover': props.rowHover,
21
- 'row-active': rowActiveProp.enabled,
22
- 'text-overflow': props.showOverflow,
23
- 'header-text-overflow': props.showHeaderOverflow,
24
- 'fixed-relative-mode': isRelativeMode,
25
- 'auto-row-height': props.autoRowHeight,
26
- 'scroll-row-by-row': isSRBRActive,
27
- 'scrollbar-on': scrollbarOptions.enabled,
28
- 'area-selection': areaSelectionConfig.enabled,
29
- 'is-area-selecting': isAreaSelecting,
30
- 'exp-scroll-y': isExperimentalScrollY,
31
- }"
32
- :tabindex="areaSelectionConfig.enabled ? 0 : void 0"
33
- :style="{
34
- '--row-height': props.autoRowHeight ? void 0 : virtualScroll.rowHeight + 'px',
35
- '--header-row-height': props.headerRowHeight + 'px',
36
- '--footer-row-height': props.footerRowHeight + 'px',
37
- '--highlight-duration': props.highlightConfig.duration && props.highlightConfig.duration + 's',
38
- '--highlight-timing-function': highlightSteps ? `steps(${highlightSteps})` : void 0,
39
- '--sb-width': `${scrollbarOptions.width}px`,
40
- '--sb-height': `${scrollbarOptions.height}px`,
41
- }"
42
- @scroll="onTableScroll"
43
- @wheel="onTableWheel"
44
- >
45
- <div v-if="!isExperimentalScrollY && SRBRTotalHeight" class="row-by-row-table-height" :style="`height: ${SRBRTotalHeight}px`"></div>
46
-
47
- <div v-if="colResizable" ref="colResizeIndicatorRef" class="column-resize-indicator"></div>
48
-
49
- <div class="stk-table-scroll-container">
50
- <table
51
- class="stk-table-main"
52
- :style="{ width, minWidth, maxWidth }"
53
- :class="{
54
- 'fixed-mode': props.fixedMode,
55
- }"
56
- @dragover="onTrDragOver"
57
- @dragenter="onTrDragEnter"
58
- @dragend="onTrDragEnd"
59
- @click="onRowClick"
60
- @dblclick="onRowDblclick"
61
- @contextmenu="onRowMenu"
62
- @mouseover="onTrMouseOver"
63
- >
64
- <!-- table-layout:fixed 下浏览器仅依据首行/colgroup 决定列宽,多级表头时子列宽度位于非首行会被忽略。
65
- 故固定模式下通过 colgroup 显式声明每个叶子列宽度,保证子列 width 生效。 -->
66
- <colgroup v-if="props.fixedMode && !virtualX_on">
67
- <col v-for="col in tableHeaderLast" :key="colKeyGen(col)" :style="getColGroupColStyle(col)" />
68
- </colgroup>
69
- <thead v-if="!headless">
70
- <tr
71
- v-for="(row, rowIndex) in virtualX_on ? virtualX_tableHeaders : tableHeaders"
72
- :key="rowIndex"
73
- @contextmenu="onHeaderMenu($event)"
74
- >
75
- <th
76
- v-if="virtualX_on"
77
- class="vt-x-left"
78
- :style="`min-width:${theadVirtualX.offsetLeft}px;width:${theadVirtualX.offsetLeft}px`"
79
- ></th>
80
- <th
81
- v-for="(col, colIndex) in row"
82
- :key="colKeyGen(col)"
83
- v-bind="getTHProps(col)"
84
- @click="e => onHeaderCellClick(e, col)"
85
- @dragstart="onThDragStart"
86
- @drop="onThDrop"
87
- @dragover="onThDragOver"
88
- >
89
- <div
90
- v-if="colResizeOn(col) && colIndex > 0"
91
- class="table-header-resizer left"
92
- @mousedown="onThResizeMouseDown($event, col, true)"
93
- ></div>
94
- <div class="table-header-cell-wrapper" :style="col.__R_SP__ ? `--row-span:${col.__R_SP__}` : null">
95
- <component
96
- :is="col.customHeaderCell"
97
- v-if="col.customHeaderCell"
98
- :col="col"
99
- :colIndex="colIndex"
100
- :rowIndex="rowIndex"
101
- />
102
- <template v-else>
103
- <slot name="tableHeader" :col="col">
104
- <span class="table-header-title">{{ col.title }}</span>
105
- </slot>
106
- </template>
107
- <SortIcon v-if="col.sorter" class="table-header-sorter" />
108
- </div>
109
- <div v-if="colResizeOn(col)" class="table-header-resizer right" @mousedown="onThResizeMouseDown($event, col)"></div>
110
- </th>
111
- <th v-if="virtualX_on" class="vt-x-right" :style="`min-width:${virtualX_offsetRight}px;width:${virtualX_offsetRight}px`"></th>
112
- </tr>
113
- </thead>
114
-
115
- <component
116
- :is="footerTagName"
117
- v-if="footerData && footerData.length > 0"
118
- class="stk-footer"
119
- :style="isFooterTop ? `top:${tableHeaderHeight}px` : ''"
120
- >
121
- <tr v-for="(footRow, footRowIndex) in footerData" :key="footRowIndex">
122
- <td
123
- v-if="virtualX_on"
124
- class="vt-x-left"
125
- :style="`min-width:${theadVirtualX.offsetLeft}px;width:${theadVirtualX.offsetLeft}px`"
126
- ></td>
127
- <template v-for="(col, _colIdx) in virtualX_columnPart" :key="col.__VT_C_SP__ ? `spacer-${_colIdx}` : colKeyGen(col)">
128
- <td v-if="col.__VT_C_SP__" class="vt-x-spacer" :colspan="col.__VT_C_SP__"></td>
129
- <td v-else v-bind="getTFProps(col)">
130
- <component
131
- :is="col.customFooterCell"
132
- v-if="col.customFooterCell"
133
- class="table-cell-wrapper"
134
- tabindex="-1"
135
- :col="col"
136
- :row="footRow"
137
- :rowIndex="footRowIndex"
138
- :cellValue="footRow[col.dataIndex]"
139
- />
140
- <div class="table-cell-wrapper" tabindex="-1" :title="footRow[col.dataIndex] || ''">
141
- <span v-if="footRow[col.dataIndex] != null">{{ footRow[col.dataIndex] }}</span>
142
- </div>
143
- </td>
144
- </template>
145
- <td v-if="virtualX_on" class="vt-x-right" :style="`min-width:${virtualX_offsetRight}px;width:${virtualX_offsetRight}px`"></td>
146
- </tr>
147
- </component>
148
-
149
- <tbody
150
- class="stk-tbody-main"
151
- :style="isExperimentalScrollY ? `transform:translateY(${virtualScroll.translateY}px)` : ''"
152
- @click="onCellClick"
153
- @mousedown="onCellMouseDown"
154
- @mouseover="onCellMouseOver"
155
- @mouseout="onTbodyMouseOut"
156
- @drop="onBodyDrop"
157
- >
158
- <tr v-if="!isExperimentalScrollY && virtual_on && !isSRBRActive" :style="paddingTopStyle" class="padding-top-tr">
159
- <template v-if="fixedMode && headless">
160
- <td
161
- v-if="virtualX_on"
162
- class="vt-x-left"
163
- :style="`min-width:${theadVirtualX.offsetLeft}px;width:${theadVirtualX.offsetLeft}px`"
164
- ></td>
165
- <template v-for="(col, _colIdx) in virtualX_columnPart" :key="col.__VT_C_SP__ ? `spacer-${_colIdx}` : colKeyGen(col)">
166
- <td v-if="col.__VT_C_SP__" class="vt-x-spacer" :colspan="col.__VT_C_SP__"></td>
167
- <td v-else :style="cellStyleMap[TagType.TD].get(colKeyGen(col))"></td>
168
- </template>
169
- <td
170
- v-if="virtualX_on"
171
- class="vt-x-right"
172
- :style="`min-width:${virtualX_offsetRight}px;width:${virtualX_offsetRight}px`"
173
- ></td>
174
- </template>
175
- </tr>
176
- <!-- tbody 渲染列表(异构):
177
- - row: 数据行(视口行 + 视口上方保留的锚点行/孤立空行)
178
- - above-ph: 视口上方连续空行(无 td)合并的占位段
179
- - below-ph: 视口下方行(rowspan 修正产生的 viewportEndIndex ~ endIndex 区域)
180
- 按跨界 rowspan 结束行切分的占位段
181
- 占位 tr 以 height: calc(var(--row-height) * 行数) 保持总高度,大 rowspan 场景显著减少 DOM 节点数 -->
182
- <!-- key 必须在 template 上:v-for 模板含多个条件分支时子节点 key 不会提升到 Fragment,
183
- 会导致 Fragment 无 key 走 unkeyed diff,滚动时整行重建 -->
184
- <template v-for="item in bodyRenderItems" :key="item.key">
185
- <tr v-if="item.type === 'row'" ref="trRef" v-bind="getTRProps(item.row, item.rowIndex)">
186
- <td v-if="item.row && item.row.__EXP_R__" :colspan="expandRowColspan">
187
- <div class="table-cell-wrapper" tabindex="-1">
188
- <slot name="expand" :row="item.row.__EXP_R__" :col="item.row.__EXP_C__">
189
- {{ (item.row.__EXP_R__ && item.row.__EXP_C__ && item.row.__EXP_R__[item.row.__EXP_C__.dataIndex]) || '' }}
190
- </slot>
191
- </div>
192
- </td>
193
- <template v-else>
194
- <td v-if="virtualX_on" class="vt-x-left"></td>
195
- <template
196
- v-for="(col, _colIdx) in getBodyColumns(item.row, item.rowIndex)"
197
- :key="col.__VT_C_SP__ ? `spacer-${_colIdx}` : col.__VT_PH__ ? `ph-${_colIdx}` : colKeyGen(col)"
198
- >
199
- <td v-if="col.__VT_C_SP__" class="vt-x-spacer" :colspan="col.__VT_C_SP__"></td>
200
- <td v-else-if="col.__VT_PH__" class="vt-above-viewport-ph" :colspan="col.__VT_PH__"></td>
201
- <td
202
- v-else-if="!shouldHideCell(item.row, col)"
203
- v-bind="getTDProps(item.row, col, item.rowIndex, (col as PrivateStkTableColumn<DT>).__LF_S__ ?? 0)"
204
- >
205
- <component
206
- :is="col.customCell"
207
- v-if="col.customCell"
208
- class="table-cell-wrapper"
209
- tabindex="-1"
210
- :col="col"
211
- :row="item.row"
212
- :rowIndex="getAbsoluteRowIndex(item.rowIndex)"
213
- :colIndex="(col as PrivateStkTableColumn<DT>).__LF_S__ ?? 0"
214
- :cellValue="item.row && item.row[col.dataIndex]"
215
- :expanded="item.row && item.row.__EXP__"
216
- :tree-expanded="item.row && item.row.__T_EXP__"
217
- >
218
- <template #stkFoldIcon>
219
- <TriangleIcon @click="triangleClick($event, item.row, col)"></TriangleIcon>
220
- </template>
221
- <template #stkDragIcon>
222
- <DragHandle @dragstart="onTrDragStart($event, getAbsoluteRowIndex(item.rowIndex))" />
223
- </template>
224
- </component>
225
- <div v-else-if="!col.type" class="table-cell-wrapper" tabindex="-1" :title="item.row[col.dataIndex] || ''">
226
- {{
227
- (item.row && item.row[col.dataIndex]) != null
228
- ? item.row && item.row[col.dataIndex]
229
- : getEmptyCellText(col, item.row)
230
- }}
231
- </div>
232
- <div v-else-if="col.type === 'seq'" class="table-cell-wrapper" tabindex="-1">
233
- {{ (props.seqConfig.startIndex || 0) + getAbsoluteRowIndex(item.rowIndex) + 1 }}
234
- </div>
235
- <TreeNodeCell
236
- v-else-if="col.type === 'tree-node'"
237
- class="table-cell-wrapper"
238
- tabindex="-1"
239
- :col="col"
240
- :row="item.row"
241
- ></TreeNodeCell>
242
- <div v-else class="table-cell-wrapper" tabindex="-1" :title="item.row[col.dataIndex] || ''">
243
- <DragHandle
244
- v-if="col.type === 'dragRow'"
245
- @dragstart="onTrDragStart($event, getAbsoluteRowIndex(item.rowIndex))"
246
- />
247
- <TriangleIcon v-else-if="col.type === 'expand'" />
248
- <span v-if="item.row[col.dataIndex] != null">{{ item.row[col.dataIndex] }}</span>
249
- </div>
250
- </td>
251
- </template>
252
- <td v-if="virtualX_on" class="vt-x-right"></td>
253
- </template>
254
- </tr>
255
- <tr
256
- v-else-if="item.type === 'above-ph'"
257
- class="vt-above-viewport-ph-row"
258
- :data-above-count="item.count"
259
- :style="`height: calc(var(--row-height) * ${item.count})`"
260
- ></tr>
261
- <tr
262
- v-else
263
- class="vt-below-viewport-ph"
264
- :data-below-count="item.count"
265
- :style="`height: calc(var(--row-height) * ${item.count})`"
266
- ></tr>
267
- </template>
268
- <template v-if="!isExperimentalScrollY">
269
- <tr v-if="virtual_on && !isSRBRActive" :style="offsetBottomStyle"></tr>
270
- <tr v-if="SRBRBottomHeight" :style="SRBRBottomStyle"></tr>
271
- </template>
272
- </tbody>
273
- </table>
274
- <div
275
- v-if="scrollbarOptions.enabled && showScrollbar.y"
276
- class="stk-sb-thumb vertical"
277
- :style="`height:${scrollbar.h}px;transform:translateY(${scrollbar.t}px)`"
278
- @mousedown="onVerticalScrollbarMouseDown"
279
- @touchstart.passive="onVerticalScrollbarMouseDown"
280
- ></div>
281
- </div>
282
- <div v-if="(!dataSourceCopy || !dataSourceCopy.length) && showNoData" class="stk-table-no-data" :class="{ 'no-data-full': noDataFull }">
283
- <slot name="empty">暂无数据</slot>
284
- </div>
285
- <slot name="customBottom"></slot>
286
- <div
287
- v-if="scrollbarOptions.enabled && showScrollbar.x"
288
- class="stk-sb-thumb horizontal"
289
- :style="`width:${scrollbar.w}px;transform:translateX(${scrollbar.l}px)`"
290
- @mousedown="onHorizontalScrollbarMouseDown"
291
- @touchstart.passive="onHorizontalScrollbarMouseDown"
292
- ></div>
293
- </div>
294
- </template>
295
-
296
- <!-- Vue 2.7 compatible: add component name -->
297
- <script lang="ts">
298
- export default {
299
- name: 'StkTable',
300
- };
301
- </script>
302
-
303
- <script setup lang="ts">
304
- /**
305
- * StkTable 主组件(Vue 3 / Vue 2.7)
306
- *
307
- * 这是一个超大的核心组件文件,约 1800+ 行。理解它时请按以下「分区地图」定位:
308
- *
309
- * 1. 顶部(template 261 行前)
310
- * 渲染结构:滚动容器 + 原生 <table>。包括 colgroup(固定模式列宽)、thead(多级表头、
311
- * sticky 固定)、tbody(虚拟滚动 tr / 空 td 定位)、tfoot(汇总行)、自定义滚动条容器。
312
- * 通过 CSS 类(如 virtual / virtual-x / area-selection / theme 等)联动对应 hook 状态。
313
- *
314
- * 2. script setup(270 行起)
315
- * - props 定义:全部配置项,即公共 API(对应 types/index.ts 与文档 table-props.md)。
316
- * - 大量 useXxx hooks 的组装:本文件只做「装配」,具体逻辑在对应 hook 中。
317
- *
318
- * 3. 核心 hook 与职责(均在 src/StkTable/use*.ts):
319
- * - useVirtualScroll XY 轴虚拟滚动(可视区裁剪、占位 tr/th 定位、scroll 计算)
320
- * - useTableColumns 列配置归一化 / 多级表头展开为叶子列
321
- * - useFixedCol/useFixedStyle/getFixedColPosition 固定列 sticky/relative 定位
322
- * - useSorter/useTableColumns.sort 排序(本地/远端、多列)
323
- * - useColResize/useThDrag/useTrDrag 列宽调整 / 表头拖拽 / 行拖拽
324
- * - useMergeCells/useMaxRowSpan 单元格合并与最大行跨
325
- * - useRowExpand/useTree 展开行 / 树形
326
- * - useHighlight 单元格/行高亮(Web Animations API 或 css)
327
- * - useAreaSelection (features/) 区域选取(Excel 式键盘操作)
328
- * - useScrollbar/useScrollRowByRow/useWheeling 自定义滚动条 / 按行滚动 / 滚轮
329
- * - useAutoResize 容器尺寸自动适配
330
- *
331
- * 4. expose:对外暴露的实例方法(对应文档 expose.md),供 ref 调用。
332
- *
333
- * 注意:此文件与 vue2.7 兼容,props 不可抽离到独立文件(见下方注释),修改时注意不要破坏兼容性。
334
- *
335
- * @author japlus
336
- */
337
- import { computed, nextTick, onMounted, provide, ref, shallowRef, toRaw, toRef, watch } from 'vue';
338
- import DragHandle from './components/DragHandle.vue';
339
- import SortIcon from './components/SortIcon.vue';
340
- import TreeNodeCell from './components/TreeNodeCell.vue';
341
- import TriangleIcon from './components/TriangleIcon.vue';
342
- import {
343
- CELL_KEY_SEPARATE,
344
- DEFAULT_ROW_ACTIVE_CONFIG,
345
- DEFAULT_ROW_HEIGHT,
346
- DEFAULT_SMOOTH_SCROLL,
347
- DEFAULT_SORT_CONFIG,
348
- IS_LEGACY_MODE,
349
- } from './const';
350
- import type { FilterStatus } from './custom-cells/FilterCell/types';
351
- import { useAreaSelectionName } from './features';
352
- import { ON_DEMAND_FEATURE } from './registerFeature';
353
- import {
354
- AreaSelectionConfig,
355
- AreaSelectionRange,
356
- AutoRowHeightConfig,
357
- ColResizableConfig,
358
- DragRowConfig,
359
- ExpandConfig,
360
- ExperimentalConfig,
361
- FooterConfig,
362
- HeaderDragConfig,
363
- HighlightConfig,
364
- Order,
365
- PrivateRowDT,
366
- PrivateStkTableColumn,
367
- RowActiveOption,
368
- ScrollAxisTarget,
369
- ScrollToFn,
370
- ScrollToOptions,
371
- SeqConfig,
372
- SortConfig,
373
- StkTableColumn,
374
- TagType,
375
- TreeConfig,
376
- UniqKey,
377
- UniqKeyProp,
378
- } from './types/index';
379
- import { useAutoResize } from './useAutoResize';
380
- import { useColResize } from './useColResize';
381
- import { useFixedCol } from './useFixedCol';
382
- import { useFixedStyle } from './useFixedStyle';
383
- import { useGetFixedColPosition } from './useGetFixedColPosition';
384
- import { useHighlight } from './useHighlight';
385
- import { useKeyboardArrowScroll } from './useKeyboardArrowScroll';
386
- import { useMaxRowSpan } from './useMaxRowSpan';
387
- import { createMergeCellsCache } from './mergeCellsCache';
388
- import { useMergeCells } from './useMergeCells';
389
- import { useRowExpand } from './useRowExpand';
390
- import { useScrollbar, type ScrollbarOptions } from './useScrollbar';
391
- import { useScrollRowByRow } from './useScrollRowByRow';
392
- import { useSorter } from './useSorter';
393
- import { useTableColumns } from './useTableColumns';
394
- import { useThDrag } from './useThDrag';
395
- import { useTrDrag } from './useTrDrag';
396
- import { useTree } from './useTree';
397
- // import { useIndexResolver } from './useIndexResolver';
398
- import { useVirtualScroll } from './useVirtualScroll';
399
- import { useWheeling } from './useWheeling';
400
- import { createStkTableId, getCalculatedColWidth } from './utils/constRefUtils';
401
- import { getClosestColKey, getClosestTd, getClosestTr, getClosestTrIndex, rafThrottle, transformWidthToStr } from './utils/index';
402
-
403
- /** Generic stands for DataType */
404
- type DT = any & PrivateRowDT;
405
-
406
- /** generate table instance id */
407
- const stkTableId = createStkTableId();
408
-
409
- // Vue 2.7 compatible: use export default instead of defineOptions
410
- // defineOptions({ name: 'StkTable' });
411
- /**
412
- * props cannot be placed in a separate file. It will cause compilation errors with vue 2.7 compiler.
413
- */
414
- const props = withDefaults(
415
- defineProps<{
416
- /** 表格宽度*/
417
- width?: string;
418
- /** 最小表格宽度 @deprecated*/
419
- minWidth?: string;
420
- /** 表格最大宽度 @deprecated*/
421
- maxWidth?: string;
422
- /** 斑马线条纹 */
423
- stripe?: boolean;
424
- /** 是否使用 table-layout:fixed(低版本浏览器需要设置table) */
425
- fixedMode?: boolean;
426
- /** 是否隐藏表头 */
427
- headless?: boolean;
428
- /** 主题,亮、暗 */
429
- theme?: 'light' | 'dark';
430
- /**
431
- * 行高
432
- * - `props.autoRowHeight` 为 `true` 时,将表示为期望行高,用于计算。不再影响实际行高。
433
- * - 支持动态修改,修改后自动重算可视区行数与滚动高度,并与 `--row-height` 保持同步。
434
- */
435
- rowHeight?: number;
436
- /**
437
- * 是否可变行高
438
- * - 设置为 `true` 时, `props.rowHeight` 将表示为期望行高,用于计算。不再影响实际行高。
439
- */
440
- autoRowHeight?: boolean | AutoRowHeightConfig<DT>;
441
- /** 是否高亮鼠标悬浮的行 */
442
- rowHover?: boolean;
443
- /** 是否高亮选中的行 */
444
- rowActive?: boolean | RowActiveOption<DT>;
445
- /**
446
- * @deprecated
447
- */
448
- rowCurrentRevokable?: boolean;
449
- /**
450
- * 表头行高。default = rowHeight
451
- * - 支持动态修改,表头占用的表体行数(pageSize)随之重算。
452
- */
453
- headerRowHeight?: number | string;
454
- /**
455
- * 表尾行高。default = rowHeight
456
- * - 支持动态修改。表尾行悬浮于滚动区底部,不计入虚拟滚动的总高度。
457
- */
458
- footerRowHeight?: number | string;
459
- /** 虚拟滚动 */
460
- virtual?: boolean;
461
- /** x轴虚拟滚动(必须设置列宽)*/
462
- virtualX?: boolean;
463
- /** 表格列配置 */
464
- columns?: StkTableColumn<DT>[];
465
- /** 表格数据源 */
466
- dataSource?: DT[];
467
- /** 行唯一键 (行唯一值不能为undefined) */
468
- rowKey?: UniqKeyProp;
469
- /** 列唯一键 */
470
- colKey?: UniqKeyProp;
471
- /** 空值展示文字 */
472
- emptyCellText?: string | ((option: { row: DT; col: StkTableColumn<DT> }) => string);
473
- /** 暂无数据兜底高度是否撑满 */
474
- noDataFull?: boolean;
475
- /** 是否展示暂无数据 */
476
- showNoData?: boolean;
477
- /** 是否服务端排序,true则不排序数据 */
478
- sortRemote?: boolean;
479
- /** 表头是否溢出展示... */
480
- showHeaderOverflow?: boolean;
481
- /** 表体溢出是否展示... */
482
- showOverflow?: boolean;
483
- /** 是否增加行hover class $*$ rename*/
484
- showTrHoverClass?: boolean;
485
- /** 是否高亮鼠标悬浮的单元格 */
486
- cellHover?: boolean;
487
- /** 是否高亮选中的单元格 */
488
- cellActive?: boolean;
489
- /** 单元格再次点击否可以取消选中 (cellActive=true)*/
490
- selectedCellRevokable?: boolean;
491
- /** 是否启用单元格范围选中(拖拽选区) */
492
- areaSelection?: boolean | AreaSelectionConfig;
493
- /** 表头是否可拖动。支持回调函数。 */
494
- headerDrag?: boolean | HeaderDragConfig<DT>;
495
- /**
496
- * 给行附加className<br>
497
- * FIXME: 是否需要优化,因为不传此prop会使表格行一直执行空函数,是否有影响
498
- */
499
- rowClassName?: (row: DT, i: number) => string | undefined;
500
- /**
501
- * 列宽是否可拖动(需要设置v-model:columns)<br>
502
- * **不要设置**列minWidth,**必须**设置width<br>
503
- * 列宽拖动时,每一列都必须要有width,且minWidth/maxWidth不生效。table width会变为"fit-content"。
504
- * - 会自动更新props.columns中的with属性
505
- */
506
- colResizable?: boolean | ColResizableConfig<DT>;
507
- /** 可拖动至最小的列宽 */
508
- colMinWidth?: number;
509
- /**
510
- * 单元格分割线。
511
- * 默认横竖都有
512
- * "h" - 仅展示横线
513
- * "v" - 仅展示竖线
514
- * "body-v" - 仅表体展示竖线
515
- */
516
- bordered?: boolean | 'h' | 'v' | 'body-v' | 'body-h';
517
- /**
518
- * 自动重新计算虚拟滚动高度宽度。默认true
519
- * [非响应式]
520
- * 传入方法表示resize后的回调
521
- */
522
- autoResize?: boolean | (() => void);
523
- /** 是否展示固定列阴影。为节省性能,默认false。 */
524
- fixedColShadow?: boolean;
525
- /** 优化vue2 滚动 */
526
- optimizeVue2Scroll?: boolean;
527
- /** 排序配置 */
528
- sortConfig?: SortConfig<DT>;
529
- /** 隐藏头部title。可传入colKey数组 */
530
- hideHeaderTitle?: boolean | string[];
531
- /** 高亮配置 */
532
- highlightConfig?: HighlightConfig;
533
- /** 序号列配置 */
534
- seqConfig?: SeqConfig;
535
- /** 展开行配置 */
536
- expandConfig?: ExpandConfig;
537
- /** 行拖动配置 */
538
- dragRowConfig?: DragRowConfig;
539
- /** 树形配置 */
540
- treeConfig?: TreeConfig;
541
- /**
542
- * 固定头,固定列实现方式。(非响应式)
543
- *
544
- * relative:固定列只会放在props.columns的两侧。
545
- * - 如果列宽会变动则谨慎使用。
546
- * - 多级表头固定列慎用
547
- *
548
- * 低版本浏览器强制为'relative',
549
- */
550
- cellFixedMode?: 'sticky' | 'relative';
551
- /**
552
- * 是否平滑滚动。default: chrome < 85 ? true : false
553
- * - false: 使用 onwheel 滚动。为了防止滚动过快导致白屏。
554
- * - true: 不使用 onwheel 滚动。鼠标滚轮滚动时更加平滑。滚动过快时会白屏。
555
- */
556
- smoothScroll?: boolean;
557
- /**
558
- * 按整数行纵向滚动
559
- * - scrollbar:仅拖动滚动条生效
560
- */
561
- scrollRowByRow?: boolean | 'scrollbar';
562
- /**
563
- * 自定义滚动条配置
564
- * - false: 禁用自定义滚动条
565
- * - true: 启用默认配置的自定义滚动条
566
- * - ScrollbarOptions: 启用并配置自定义滚动条
567
- */
568
- scrollbar?: boolean | ScrollbarOptions;
569
- /**
570
- * 实验性功能配置
571
- */
572
- experimental?: ExperimentalConfig;
573
- /** 表格底部合计行数据 */
574
- footerData?: DT[];
575
- /** 表格底部配置 */
576
- footerConfig?: FooterConfig;
577
- }>(),
578
- {
579
- width: '',
580
- fixedMode: false,
581
- stripe: false,
582
- minWidth: '',
583
- maxWidth: '',
584
- headless: false,
585
- theme: 'light',
586
- rowHeight: DEFAULT_ROW_HEIGHT,
587
- autoRowHeight: () => false,
588
- footerData: () => [],
589
- rowHover: true,
590
- rowActive: () => DEFAULT_ROW_ACTIVE_CONFIG,
591
- rowCurrentRevokable: true,
592
- headerRowHeight: DEFAULT_ROW_HEIGHT,
593
- footerRowHeight: DEFAULT_ROW_HEIGHT,
594
- virtual: false,
595
- virtualX: false,
596
- columns: () => [],
597
- dataSource: () => [],
598
- rowKey: '',
599
- colKey: void 0,
600
- emptyCellText: '--',
601
- noDataFull: false,
602
- showNoData: true,
603
- sortRemote: false,
604
- showHeaderOverflow: false,
605
- showOverflow: false,
606
- showTrHoverClass: false,
607
- cellHover: false,
608
- cellActive: false,
609
- selectedCellRevokable: true,
610
- areaSelection: false,
611
- headerDrag: () => false,
612
- rowClassName: () => '',
613
- colResizable: () => false,
614
- colMinWidth: 10,
615
- bordered: true,
616
- autoResize: true,
617
- fixedColShadow: false,
618
- optimizeVue2Scroll: false,
619
- sortConfig: () => DEFAULT_SORT_CONFIG,
620
- hideHeaderTitle: false,
621
- highlightConfig: () => ({}),
622
- seqConfig: () => ({}),
623
- expandConfig: () => ({}),
624
- dragRowConfig: () => ({}),
625
- treeConfig: () => ({}),
626
- cellFixedMode: 'sticky',
627
- smoothScroll: DEFAULT_SMOOTH_SCROLL,
628
- scrollRowByRow: false,
629
- scrollbar: false,
630
- experimental: () => ({}),
631
- footerConfig: () => ({ position: 'bottom' }),
632
- },
633
- );
634
-
635
- provide('stkTheme', toRef(props, 'theme'));
636
-
637
- const emits = defineEmits<{
638
- /**
639
- * 排序变更触发。defaultSort.dataIndex 找不到时,col 将返回null。
640
- *
641
- * ```(col: StkTableColumn<DT> | null, order: Order, data: DT[], sortConfig: SortConfig<DT>)```
642
- */
643
- (e: 'sort-change', col: StkTableColumn<DT> | null, order: Order, data: DT[], sortConfig: SortConfig<DT>): void;
644
- /**
645
- * 一行点击事件
646
- *
647
- * ```(ev: MouseEvent, row: DT, data: { rowIndex: number })```
648
- */
649
- (e: 'row-click', ev: MouseEvent, row: DT, data: { rowIndex: number }): void;
650
- /**
651
- * 选中一行触发。ev返回null表示不是点击事件触发的
652
- *
653
- * ```(ev: MouseEvent | null, row: DT | undefined, data: { select: boolean} })```
654
- */
655
- (e: 'current-change', ev: MouseEvent | null, row: DT | undefined, data: { select: boolean }): void;
656
- /**
657
- * 选中单元格触发。ev返回null表示不是点击事件触发的
658
- *
659
- * ```(ev: MouseEvent | null, data: { select: boolean; row: DT | undefined; col: StkTableColumn<DT> | null })```
660
- */
661
- (e: 'cell-selected', ev: MouseEvent | null, data: { select: boolean; row: DT | undefined; col: StkTableColumn<DT> | undefined }): void;
662
- /**
663
- * 行双击事件
664
- *
665
- * ```(ev: MouseEvent, row: DT, data: { rowIndex: number })```
666
- */
667
- (e: 'row-dblclick', ev: MouseEvent, row: DT, data: { rowIndex: number }): void;
668
- /**
669
- * 表头右键事件
670
- *
671
- * ```(ev: MouseEvent)```
672
- */
673
- (e: 'header-row-menu', ev: MouseEvent): void;
674
- /**
675
- * 表体行右键点击事件
676
- *
677
- * ```(ev: MouseEvent, row: DT, data: { rowIndex: number })```
678
- */
679
- (e: 'row-menu', ev: MouseEvent, row: DT, data: { rowIndex: number }): void;
680
- /**
681
- * 单元格点击事件
682
- *
683
- * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>, data: { rowIndex: number })```
684
- */
685
- (e: 'cell-click', ev: MouseEvent, row: DT, col: StkTableColumn<DT>, data: { rowIndex: number }): void;
686
- /**
687
- * 单元格鼠标进入事件
688
- *
689
- * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>)```
690
- */
691
- (e: 'cell-mouseenter', ev: MouseEvent, row: DT, col: StkTableColumn<DT>): void;
692
- /**
693
- * 单元格鼠标移出事件
694
- *
695
- * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>)```
696
- */
697
- (e: 'cell-mouseleave', ev: MouseEvent, row: DT, col: StkTableColumn<DT>): void;
698
- /**
699
- * 单元格悬浮事件
700
- *
701
- * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>)```
702
- */
703
- (e: 'cell-mouseover', ev: MouseEvent, row: DT, col: StkTableColumn<DT>): void;
704
- /**
705
- * 单元格鼠标按下事件
706
- *
707
- * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>, data: { rowIndex: number })```
708
- */
709
- (e: 'cell-mousedown', ev: MouseEvent, row: DT, col: StkTableColumn<DT>, data: { rowIndex: number }): void;
710
- /**
711
- * 表头单元格点击事件
712
- *
713
- * ```(ev: MouseEvent, col: StkTableColumn<DT>)```
714
- */
715
- (e: 'header-cell-click', ev: MouseEvent, col: StkTableColumn<DT>): void;
716
- /**
717
- * 表格滚动事件
718
- *
719
- * ```(ev: Event, data: { startIndex: number; endIndex: number })```
720
- */
721
- (e: 'scroll', ev: Event, data: { startIndex: number; endIndex: number }): void;
722
- /**
723
- * 表格横向滚动事件
724
- *
725
- * ```(ev: Event)```
726
- */
727
- (e: 'scroll-x', ev: Event): void;
728
- /**
729
- * 表头列拖动事件
730
- *
731
- * ```(dragStartKey: string, targetColKey: string)```
732
- */
733
- (e: 'col-order-change', dragStartKey: string, targetColKey: string): void;
734
- /**
735
- * 表头列拖动开始
736
- *
737
- * ```(dragStartKey: string)```
738
- */
739
- (e: 'th-drag-start', dragStartKey: string): void;
740
- /**
741
- * 表头列拖动drop
742
- *
743
- * ```(targetColKey: string)```
744
- */
745
- (e: 'th-drop', targetColKey: string): void;
746
- /**
747
- * 行拖动事件
748
- *
749
- * ```(dragStartKey: string, targetRowKey: string)```
750
- */
751
- (e: 'row-order-change', dragStartKey: string, targetRowKey: string): void;
752
- /**
753
- * 列宽变动时触发
754
- *
755
- * ```(col: StkTableColumn<DT>)```
756
- */
757
- (e: 'col-resize', col: StkTableColumn<DT>): void;
758
- /**
759
- * 展开行触发
760
- *
761
- * ```( data: { expanded: boolean; row: DT; col: StkTableColumn<DT> })```
762
- */
763
- (e: 'toggle-row-expand', data: { expanded: boolean; row: DT; col: StkTableColumn<DT> | null }): void;
764
- /**
765
- * 点击展开树行触发
766
- *
767
- * ```( data: { expanded: boolean; row: DT; col: StkTableColumn<DT> })```
768
- */
769
- (e: 'toggle-tree-expand', data: { expanded: boolean; row: DT; col: StkTableColumn<DT> | null }): void;
770
- /**
771
- * 单元格选区变更事件
772
- *
773
- * ```(ranges: AreaSelectionRange[])```
774
- */
775
- (e: 'area-selection-change', ranges: AreaSelectionRange[]): void;
776
- /**
777
- * 筛选变更触发(Beta)
778
- *
779
- * ```(status: Record<UniqKey, FilterStatus>)```
780
- */
781
- (e: 'filter-change', status: Record<UniqKey, FilterStatus>): void;
782
- /**
783
- * v-model:columns col resize 时更新宽度
784
- */
785
- (e: 'update:columns', cols: StkTableColumn<DT>[]): void;
786
- }>();
787
-
788
- // 仅支持vue3.3+
789
- // const slots = defineSlots<{
790
- // /** 表头插槽 */
791
- // tableHeader(props: { col: StkTableColumn<DT> }): void;
792
- // /** 空状态插槽 */
793
- // empty(): void;
794
- // }>();
795
-
796
- const tableContainerRef = ref<HTMLDivElement>();
797
- const colResizeIndicatorRef = ref<HTMLDivElement>();
798
- const trRef = ref<HTMLTableRowElement[]>();
799
-
800
- /** 是否使用 relative 固定头和列 */
801
- const isRelativeMode = ref(IS_LEGACY_MODE ? true : props.cellFixedMode === 'relative');
802
-
803
- /** 表格底部是否吸附在顶部 */
804
- const isFooterTop = computed(() => props.footerConfig?.position === 'top');
805
-
806
- /** 表格底部标签名:顶部吸附用 tbody,底部吸附用 tfoot */
807
- const footerTagName = computed(() => (isFooterTop.value ? 'tbody' : 'tfoot'));
808
-
809
- /**
810
- * 当前选中的一行
811
- * - shallowRef: 使 currentRow.value === row 地址相同。防止rowKeyGen 的WeakMap key不一致。
812
- */
813
- const currentRow = shallowRef<DT>();
814
- /**
815
- * 保存当前选中行的key<br>
816
- * 原因:vue3 不用ref包dataSource时,row为原始对象,与currentItem(Ref)相比会不相等。
817
- */
818
- const currentRowKey = ref<UniqKey | undefined>();
819
- /** 当前选中的单元格key */
820
- const currentSelectedCellKey = ref<string | undefined>();
821
- /** 当前hover行 */
822
- let currentHoverRow: DT | null = null;
823
- /** 当前hover的行的key */
824
- const currentHoverRowKey = ref<UniqKey | null>(null);
825
- /** 当前hover的列的key */
826
- // const currentColHoverKey = ref(null);
827
-
828
- const [tableHeaders, tableHeadersForCalc, dealColumns] = useTableColumns<DT>(props.virtualX, isRelativeMode);
829
-
830
- const filterStatus = ref<Record<UniqKey, FilterStatus>>({});
831
-
832
- /** 最后一行的tableHeaders.内容是 props.columns 的引用集合 */
833
- const tableHeaderLast = computed(() => tableHeadersForCalc.value.slice(-1)[0] || []);
834
-
835
- const isTreeData = computed(() => {
836
- return props.columns.some(col => col.type === 'tree-node');
837
- });
838
-
839
- const rowActiveProp = computed<Required<RowActiveOption<DT>>>(() => {
840
- const { rowActive } = props;
841
- if (typeof rowActive === 'boolean') {
842
- return {
843
- ...DEFAULT_ROW_ACTIVE_CONFIG,
844
- enabled: rowActive ?? true,
845
- revokable: Boolean(props.rowCurrentRevokable),
846
- };
847
- } else {
848
- return { ...DEFAULT_ROW_ACTIVE_CONFIG, ...rowActive };
849
- }
850
- });
851
-
852
- const dataSourceCopy = shallowRef<DT[]>([]);
853
-
854
- const rowKeyGenComputed = computed(() => {
855
- const { rowKey } = props;
856
- if (typeof rowKey === 'function') {
857
- return (row: DT) => (rowKey as (row: DT) => string)(row);
858
- } else {
859
- return (row: DT) => row[rowKey];
860
- }
861
- });
862
-
863
- const colKeyGen = computed<(col: StkTableColumn<DT>) => string>(() => {
864
- const { colKey } = props;
865
- if (colKey === void 0) {
866
- return col => col.key || col.dataIndex;
867
- } else if (typeof colKey === 'function') {
868
- return col => (colKey as (col: StkTableColumn<DT>) => string)(col);
869
- } else {
870
- return col => (col as any)[colKey];
871
- }
872
- });
873
-
874
- const getEmptyCellText = computed(() => {
875
- const { emptyCellText } = props;
876
- if (typeof emptyCellText === 'string') {
877
- return () => emptyCellText;
878
- } else {
879
- return (col: StkTableColumn<DT>, row: DT) => emptyCellText({ row, col });
880
- }
881
- });
882
-
883
- /** scroll-row-by-row total-height */
884
- const SRBRTotalHeight = computed(() => {
885
- if (!isSRBRActive.value || !props.virtual) return 0;
886
- return (
887
- dataSourceCopy.value.length * virtualScroll.value.rowHeight + tableHeaderHeight.value //+
888
- );
889
- });
890
- const SRBRBottomHeight = computed(() => {
891
- if (!isSRBRActive.value || !props.virtual) return 0;
892
- const { containerHeight, rowHeight } = virtualScroll.value;
893
- return (containerHeight - tableHeaderHeight.value) % rowHeight;
894
- });
895
-
896
- const scrollbarOptions = computed(() => ({
897
- enabled: true,
898
- minHeight: 20,
899
- minWidth: 20,
900
- width: 8,
901
- height: 8,
902
- ...(typeof props.scrollbar === 'boolean' ? { enabled: props.scrollbar } : props.scrollbar),
903
- }));
904
-
905
- const isExperimentalScrollY = computed(() => {
906
- if (scrollbarOptions.value?.enabled && props.scrollRowByRow) {
907
- return true;
908
- }
909
- return props.experimental?.scrollY;
910
- });
911
-
912
- const rowKeyGenCache = new WeakMap();
913
-
914
- // 使用 useSorter hook 管理排序逻辑
915
- const [sortStates, sortCol, onColumnSort, setSorter, resetSorter, getSortColumns, dealDefaultSorter, getColumnSortState, sortData] = useSorter<DT>(
916
- props,
917
- emits,
918
- colKeyGen,
919
- tableHeaderLast,
920
- dataSourceCopy,
921
- initDataSource,
922
- onDataSourceChange,
923
- );
924
-
925
- const [isSRBRActive] = useScrollRowByRow(props, tableContainerRef);
926
-
927
- const [onThDragStart, onThDragOver, onThDrop, isHeaderDraggable] = useThDrag(props, emits, colKeyGen);
928
-
929
- const [onTrDragStart, onTrDragEnter, onTrDragOver, onTrDrop, onTrDragEnd] = useTrDrag(props, emits, dataSourceCopy);
930
-
931
- const [maxRowSpan, updateMaxRowSpan, getMaxRowSpanValue] = useMaxRowSpan(props, tableHeaderLast, rowKeyGen, dataSourceCopy);
932
-
933
- /**
934
- * mergeCells 结果共享缓存:useVirtualScroll(可视列范围修正)与 useMergeCells
935
- * (hidden map 构建 / 视口上方占位 / 渲染)共用,同一单元格只调用一次用户回调,
936
- * 且可跨滚动帧复用(数据/列变化时才清空)。
937
- */
938
- const mergeCellsCache = createMergeCellsCache();
939
-
940
- const [
941
- virtualScroll,
942
- virtualScrollX,
943
- virtual_on,
944
- virtual_dataSourcePart,
945
- virtual_offsetBottom,
946
- virtualX_on,
947
- virtualX_offsetRight,
948
- tableHeaderHeight,
949
- initVirtualScroll,
950
- initVirtualScrollY,
951
- initVirtualScrollX,
952
- updateVirtualScrollY,
953
- updateVirtualScrollX,
954
- setAutoHeight,
955
- clearAllAutoHeight,
956
- getRowsHeight,
957
- clearColWidthCache,
958
- virtualX_tableHeaders,
959
- expandRowColspan,
960
- theadVirtualX,
961
- virtualX_columnPart,
962
- virtualX_expandColSegments,
963
- // 内部诊断用(测试通过 setupState 访问),不加入 defineExpose
964
- getRowHeightCacheInfo,
965
- ] = useVirtualScroll(
966
- props,
967
- tableContainerRef,
968
- trRef,
969
- dataSourceCopy,
970
- tableHeaderLast,
971
- tableHeaders,
972
- rowKeyGen,
973
- maxRowSpan,
974
- getMaxRowSpanValue,
975
- scrollbarOptions,
976
- isExperimentalScrollY,
977
- isRelativeMode,
978
- mergeCellsCache,
979
- );
980
-
981
- /** requestAnimationFrame throttled version of updateVirtualScrollY for smoother wheel scrolling */
982
- const rafUpdateVirtualScrollYForWheel = rafThrottle(updateVirtualScrollY);
983
-
984
- const [scrollbar, showScrollbar, onVerticalScrollbarMouseDown, onHorizontalScrollbarMouseDown, updateCustomScrollbar] = useScrollbar(
985
- props,
986
- tableContainerRef,
987
- virtualScroll,
988
- virtualScrollX,
989
- updateVirtualScrollY,
990
- scrollbarOptions,
991
- isExperimentalScrollY,
992
- );
993
-
994
- /**
995
- * 是否允许把虚拟窗口内的连续空行合并为占位 tr。
996
- * autoRowHeight 时容器不定义 --row-height 且行高不均匀,不能合并。
997
- */
998
- const canMergeEmptyRows = computed(() => virtual_on.value && !props.autoRowHeight);
999
-
1000
- /** 视口上方行数(rowspan/stripe 修正区域)。不允许合并时为 0(上方行走常规行渲染) */
1001
- const aboveViewportRowCount = computed(() => {
1002
- if (!canMergeEmptyRows.value) return 0;
1003
- const part = virtual_dataSourcePart.value;
1004
- const { startIndex, viewportStartIndex } = virtualScroll.value;
1005
- return Math.min(part.length, Math.max(0, viewportStartIndex - startIndex));
1006
- });
1007
-
1008
- /**
1009
- * 视口下方可合并的占位行数。
1010
- * rowspan 修正会使 endIndex 超出 viewportEndIndex,二者之间的行无可见内容
1011
- * (跨视口的合并单元格由视口内锚点覆盖),此前逐行渲染为空 tr。
1012
- * 这里将其合并为占位 tr(按跨界 rowspan 结束行切段,见 belowPhSegments),
1013
- * 用 `height: calc(var(--row-height) * N)` 保持总高度,
1014
- * 大 rowspan 场景下可显著减少 DOM 节点数。
1015
- * 行高不均匀时(autoRowHeight / 该区域存在展开行)不能按统一行高合并,返回 0。
1016
- */
1017
- const belowViewportRowCount = computed(() => {
1018
- if (!canMergeEmptyRows.value) return 0;
1019
- const part = virtual_dataSourcePart.value;
1020
- const { startIndex, viewportEndIndex } = virtualScroll.value;
1021
- // 视口行数(含 rowspan/stripe 修正的越界部分),钳制到实际数据长度
1022
- const viewportCount = Math.min(part.length, Math.max(0, viewportEndIndex - startIndex + 1));
1023
- const count = part.length - viewportCount;
1024
- if (count <= 0) return 0;
1025
- // 展开行有独立行高(--row-height 被行内覆写),不能参与统一行高合并
1026
- for (let i = viewportCount; i < part.length; i++) {
1027
- if (part[i]?.__EXP_R__) return 0;
1028
- }
1029
- return count;
1030
- });
1031
-
1032
- const [
1033
- hiddenCellMap,
1034
- mergeCellsWrapper,
1035
- hoverMergedCells,
1036
- updateHoverMergedCells,
1037
- activeMergedCells,
1038
- updateActiveMergedCells,
1039
- aboveViewportColumnMap,
1040
- aboveEmptyBlocks,
1041
- belowPhSegments,
1042
- ] = useMergeCells(
1043
- rowActiveProp,
1044
- tableHeaderLast,
1045
- rowKeyGen,
1046
- colKeyGen,
1047
- virtual_dataSourcePart,
1048
- virtualScroll,
1049
- virtualX_columnPart,
1050
- dataSourceCopy,
1051
- mergeCellsCache,
1052
- canMergeEmptyRows,
1053
- belowViewportRowCount,
1054
- );
1055
-
1056
- const getFixedColPosition = useGetFixedColPosition(tableHeadersForCalc, colKeyGen);
1057
-
1058
- const getFixedStyle = useFixedStyle<DT>(props, isRelativeMode, getFixedColPosition, virtualScroll, virtualScrollX, virtualX_on, virtualX_offsetRight);
1059
-
1060
- const [highlightSteps, setHighlightDimRow, setHighlightDimCell] = useHighlight(props, stkTableId, tableContainerRef);
1061
-
1062
- function getRowIndex(row: DT): number {
1063
- const targetKey = rowKeyGen(row);
1064
- return dataSourceCopy.value.findIndex(item => rowKeyGen(item) === targetKey);
1065
- }
1066
-
1067
- function getColumnIndex(column: PrivateStkTableColumn<DT>): number {
1068
- const targetKey = colKeyGen.value(column);
1069
- return tableHeaderLast.value.findIndex(item => colKeyGen.value(item) === targetKey);
1070
- }
1071
-
1072
- const {
1073
- config: areaSelectionConfig,
1074
- isSelecting: isAreaSelecting,
1075
- onMD: onSelectionMouseDown,
1076
- // getClass: getAreaSelectionClasses,
1077
- // getRowClass: getAreaSelectionRowClass,
1078
- get: getSelectedArea,
1079
- set: setAreaSelection,
1080
- clear: clearSelectedArea,
1081
- copy: copySelectedArea,
1082
- } = ON_DEMAND_FEATURE[useAreaSelectionName](
1083
- props,
1084
- emits,
1085
- tableContainerRef,
1086
- dataSourceCopy,
1087
- tableHeaderLast,
1088
- colKeyGen,
1089
- cellKeyGen,
1090
- scrollTo,
1091
- virtualScroll,
1092
- virtualScrollX,
1093
- getRowIndex,
1094
- getColumnIndex,
1095
- );
1096
-
1097
- /** 键盘箭头滚动 */
1098
- useKeyboardArrowScroll(tableContainerRef, props, scrollTo, virtualScroll, virtualScrollX, tableHeaders, virtual_on, areaSelectionConfig);
1099
-
1100
- /** 固定列处理 */
1101
- const [fixedCols, fixedColClassMap, updateFixedShadow] = useFixedCol(
1102
- props,
1103
- colKeyGen,
1104
- getFixedColPosition,
1105
- tableHeaders,
1106
- tableHeadersForCalc,
1107
- tableContainerRef,
1108
- );
1109
-
1110
- if (props.autoResize) {
1111
- useAutoResize(
1112
- tableContainerRef,
1113
- () => {
1114
- initVirtualScroll();
1115
- // 容器宽度变化后,需重新计算固定列状态
1116
- updateFixedShadow();
1117
- },
1118
- props,
1119
- 200,
1120
- );
1121
- }
1122
-
1123
- const [colResizeOn, isColResizing, onThResizeMouseDown] = useColResize(
1124
- props,
1125
- emits,
1126
- tableContainerRef,
1127
- tableHeaderLast,
1128
- colResizeIndicatorRef,
1129
- colKeyGen,
1130
- fixedCols,
1131
- clearColWidthCache,
1132
- );
1133
-
1134
- const [toggleExpandRow, setRowExpand] = useRowExpand(emits, dataSourceCopy, rowKeyGen, onDataSourceChange);
1135
-
1136
- const [toggleTreeNode, setTreeExpand, flatTreeData] = useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange);
1137
-
1138
- /** style cache */
1139
- const paddingTopStyle = computed(() => `height:${virtualScroll.value.offsetTop}px`);
1140
- const offsetBottomStyle = computed(() => `height:${virtual_offsetBottom.value}px`);
1141
- const SRBRBottomStyle = computed(() => `height:${SRBRBottomHeight.value}px`);
1142
-
1143
- /** tbody 渲染项:数据行 | 视口上方合并占位段 | 视口下方合并占位段 */
1144
- type BodyRenderItem =
1145
- | { type: 'row'; row: DT; rowIndex: number; key: UniqKey }
1146
- | { type: 'above-ph'; count: number; key: string }
1147
- | { type: 'below-ph'; count: number; key: string };
1148
-
1149
- /**
1150
- * 视口上方区域(startIndex..viewportStartIndex-1)的有序渲染段。
1151
- * 含 must-render 单元格(rowspan 跨入视口)的锚点行保留独立 tr;
1152
- * 连续「无 td」的空行(aboveEmptyBlocks,>= 2 行)合并为单个占位段,
1153
- * 以 height: calc(var(--row-height) * N) 保持总高度。
1154
- *
1155
- * 注意:占位段在 DOM 中只算 1 行,跨越它的 rowspan 属性已在
1156
- * mergeCellsWrapper 中按合并行数修正,保证单元格不会多跨行。
1157
- * 不允许合并时返回空数组。
1158
- */
1159
- const aboveRenderParts = computed<BodyRenderItem[]>(() => {
1160
- const aboveCount = aboveViewportRowCount.value;
1161
- if (aboveCount <= 0) return [];
1162
- const part = virtual_dataSourcePart.value;
1163
- const blocks = aboveEmptyBlocks.value;
1164
- const { startIndex } = virtualScroll.value;
1165
-
1166
- const items: BodyRenderItem[] = [];
1167
- let i = 0;
1168
- let blockIdx = 0;
1169
- while (i < aboveCount) {
1170
- const block = blocks[blockIdx];
1171
- if (block && block.start === startIndex + i) {
1172
- items.push({ type: 'above-ph', count: block.count, key: `vt-above-ph-${i}` });
1173
- i += block.count;
1174
- blockIdx++;
1175
- } else {
1176
- const row = part[i];
1177
- items.push({ type: 'row', row, rowIndex: i, key: rowKeyGen(row) });
1178
- i++;
1179
- }
1180
- }
1181
- return items;
1182
- });
1183
-
1184
- /**
1185
- * tbody 渲染列表(异构):视口上方段 + 视口行 + 视口下方占位段。
1186
- * 不允许合并时退化为全量数据行列表。
1187
- */
1188
- const bodyRenderItems = computed<BodyRenderItem[]>(() => {
1189
- const part = virtual_dataSourcePart.value;
1190
- const items = aboveRenderParts.value.slice();
1191
- const rowEnd = part.length - belowViewportRowCount.value;
1192
- for (let i = aboveViewportRowCount.value; i < rowEnd; i++) {
1193
- const row = part[i];
1194
- items.push({ type: 'row', row, rowIndex: i, key: rowKeyGen(row) });
1195
- }
1196
- // 下方占位按跨界 rowspan 结束行分段输出,保证不同结束行的单元格止于不同 tr
1197
- const belowSegs = belowPhSegments.value;
1198
- for (let i = 0; i < belowSegs.length; i++) {
1199
- items.push({ type: 'below-ph', count: belowSegs[i].count, key: `vt-below-ph-${i}` });
1200
- }
1201
- return items;
1202
- });
1203
-
1204
- watch(
1205
- () => props.columns,
1206
- () => {
1207
- handleDealColumns();
1208
- updateMaxRowSpan();
1209
- // nextTick: initVirtualScrollX need get container width。
1210
- nextTick(() => {
1211
- initVirtualScrollX();
1212
- updateFixedShadow();
1213
- updateCustomScrollbar();
1214
- });
1215
- },
1216
- );
1217
- watch(
1218
- () => props.virtual,
1219
- () => {
1220
- nextTick(initVirtualScrollY);
1221
- },
1222
- );
1223
-
1224
- /**
1225
- * 行高类配置变化后重算虚拟滚动几何。
1226
- * 合并为一个 watcher:同一 tick 内多项同时变化只重算一次。
1227
- * 必须无参调用 initVirtualScrollY(其首参是虚拟滚动容器高度,不能把行高值传入),
1228
- * 且需 nextTick 等新的 --row-height / 表头行高等样式应用到 DOM 后再测量。
1229
- *
1230
- * 监听值拼接为单个原始值(字符串)而非返回数组:getter 返回数组时每次求值都是新数组,
1231
- * 身份比较必然变化,父组件传入内联对象字面量(如 :expand-config="{ height: 40 }",官方示例即如此写法)
1232
- * 会让本 watcher 在父组件每次重渲染时误触发;展开行/变高大表下误触发一次会连带行高树 O(n) 重建
1233
- * (实测 50K 行每次父重渲染多付 ~10ms)。拼接为原始值后仅在值真正变化时触发,且兼容 Vue 2.7。
1234
- */
1235
- watch(
1236
- () => `${props.rowHeight}|${props.headerRowHeight}|${props.footerRowHeight}|${props.expandConfig?.height}`,
1237
- () => nextTick(initVirtualScrollY),
1238
- );
1239
-
1240
- watch(
1241
- () => props.virtualX,
1242
- () => {
1243
- handleDealColumns();
1244
- // initVirtualScrollX 需要获取容器滚动宽度等。必须等渲染完成后再调用。因此使用nextTick。
1245
- nextTick(() => {
1246
- initVirtualScrollX();
1247
- updateFixedShadow();
1248
- });
1249
- },
1250
- );
1251
-
1252
- watch(
1253
- () => props.dataSource,
1254
- val => {
1255
- updateDataSource(val);
1256
- },
1257
- );
1258
-
1259
- watch(
1260
- () => props.fixedColShadow,
1261
- () => updateFixedShadow(),
1262
- );
1263
-
1264
- handleDealColumns();
1265
- initDataSource();
1266
- updateMaxRowSpan();
1267
-
1268
- onMounted(() => {
1269
- initVirtualScroll();
1270
- updateFixedShadow();
1271
- dealDefaultSorter();
1272
- });
1273
-
1274
- async function onDataSourceChange() {
1275
- // 数据顺序/行数变化后重算 rowspan 映射(排序后 mergeCells 的 rowIndex 会变化)
1276
- updateMaxRowSpan();
1277
- await nextTick();
1278
- initVirtualScrollY();
1279
- updateCustomScrollbar();
1280
- }
1281
-
1282
- function initDataSource(v = props.dataSource, option?: { forceSort?: boolean }) {
1283
- let dataSourceTemp = v.slice(); // shallow copy
1284
-
1285
- // 排序(tableSort 内部会根据 sortChildren 自动处理树形递归排序)
1286
- if (!props.sortRemote || option?.forceSort) {
1287
- dataSourceTemp = sortData(dataSourceTemp);
1288
- }
1289
-
1290
- if (isTreeData.value) {
1291
- dataSourceTemp = flatTreeData(dataSourceTemp);
1292
- }
1293
- dataSourceTemp = filterDataSource(dataSourceTemp);
1294
- dataSourceCopy.value = dataSourceTemp;
1295
- }
1296
-
1297
- function setFilter(
1298
- status: Record<UniqKey, FilterStatus> | null,
1299
- option?: {
1300
- remote?: boolean;
1301
- silent?: boolean;
1302
- },
1303
- ) {
1304
- status = status || {};
1305
- filterStatus.value = status;
1306
- if (!option?.remote) {
1307
- initDataSource();
1308
- // #80:筛选改变 dataSourceCopy 后重算虚拟滚动窗口,
1309
- // 否则残留 startIndex/endIndex/scrollHeight 会导致只渲染 1 行或空白视口
1310
- onDataSourceChange();
1311
- }
1312
- if (!option?.silent) {
1313
- emits('filter-change', status);
1314
- }
1315
- }
1316
-
1317
- function filterDataSource(dataSource: DT[]) {
1318
- const filterKeys = Object.keys(filterStatus.value);
1319
- if (!filterKeys?.length) return dataSource;
1320
- let result = dataSource;
1321
- for (const key of filterKeys) {
1322
- const { value, filter } = filterStatus.value[key];
1323
- if (!value?.length) continue;
1324
- result = result.filter(row => {
1325
- const cellValue = row[key];
1326
- if (filter) {
1327
- return filter({ row, cellValue, filterValues: value });
1328
- }
1329
- return value.some(v => cellValue == v);
1330
- });
1331
- }
1332
- return result;
1333
- }
1334
-
1335
- /**
1336
- * Wrapper for dealColumns to pass props.columns
1337
- */
1338
- function handleDealColumns() {
1339
- dealColumns(props.columns);
1340
- }
1341
-
1342
- function updateDataSource(val: DT[]) {
1343
- if (!Array.isArray(val)) {
1344
- console.warn('invalid dataSource');
1345
- return;
1346
- }
1347
-
1348
- let needInitVirtualScrollY = false;
1349
- if (/* slots.customBottom || */ dataSourceCopy.value.length !== val.length) {
1350
- needInitVirtualScrollY = true;
1351
- }
1352
- initDataSource(val);
1353
- updateMaxRowSpan();
1354
-
1355
- // #47
1356
- if (!val.length) {
1357
- clearSelectedArea();
1358
- }
1359
-
1360
- // if data length is not change, not init virtual scroll
1361
- if (needInitVirtualScrollY) {
1362
- // wait for table render,initVirtualScrollY has get `dom` operation.
1363
- nextTick(() => initVirtualScrollY());
1364
- }
1365
- nextTick(updateCustomScrollbar);
1366
- }
1367
-
1368
- /** tr key */
1369
- function rowKeyGen(row: DT | null | undefined) {
1370
- if (!row) return row;
1371
-
1372
- let key = rowKeyGenCache.get(row);
1373
- if (key !== undefined) return key;
1374
-
1375
- // Check for cached key in row object
1376
- const cachedRowKey = (row as PrivateRowDT).__R_K__;
1377
- if (cachedRowKey !== undefined) {
1378
- rowKeyGenCache.set(row, cachedRowKey);
1379
- return cachedRowKey;
1380
- }
1381
-
1382
- key = rowKeyGenComputed.value(row);
1383
-
1384
- if (key === void 0) {
1385
- // key为undefined时,不应该高亮行。因此重新生成key
1386
- key = Math.random().toString(36).slice(2);
1387
- }
1388
- rowKeyGenCache.set(row, key);
1389
- return key;
1390
- }
1391
-
1392
- /** td key */
1393
- function cellKeyGen(row: DT | null | undefined, col: StkTableColumn<DT>) {
1394
- return rowKeyGen(row) + CELL_KEY_SEPARATE + colKeyGen.value(col);
1395
- }
1396
-
1397
- const cellStyleMap = computed(() => {
1398
- const thMap = new Map();
1399
- const tdMap = new Map();
1400
- const tfMap = new Map();
1401
- const { virtualX } = props;
1402
- const headers = tableHeaders.value;
1403
- const colKeyGenValue = colKeyGen.value;
1404
-
1405
- for (let depth = 0, depthLen = headers.length; depth < depthLen; depth++) {
1406
- const cols = headers[depth];
1407
- for (let i = 0, colsLen = cols.length; i < colsLen; i++) {
1408
- const col = cols[i];
1409
- const width = virtualX ? getCalculatedColWidth(col) + 'px' : transformWidthToStr(col.width);
1410
- const minWidthStr = transformWidthToStr(col.minWidth);
1411
- const maxWidthStr = transformWidthToStr(col.maxWidth);
1412
- // en: Use string instead of object to reduce patchStyle overhead
1413
- let styleStr = '';
1414
- if (width) styleStr += `--cw:${width}`;
1415
- if (minWidthStr) styleStr += `;min-width:${minWidthStr}`;
1416
- if (maxWidthStr) styleStr += `;max-width:${maxWidthStr}`;
1417
- const colKey = colKeyGenValue(col);
1418
- thMap.set(colKey, styleStr + ';' + getFixedStyle(TagType.TH, col, depth));
1419
- tdMap.set(colKey, styleStr + ';' + getFixedStyle(TagType.TD, col, depth));
1420
- tfMap.set(colKey, 'position:sticky;' + styleStr + ';' + getFixedStyle(TagType.TF, col, depth));
1421
- }
1422
- }
1423
- return {
1424
- [TagType.TH]: thMap,
1425
- [TagType.TD]: tdMap,
1426
- [TagType.TF]: tfMap,
1427
- };
1428
- });
1429
-
1430
- /**
1431
- * fixed 模式下 colgroup 中单个 col 的样式。
1432
- * 仅取叶子列的 width(与 cellStyleMap 中 --cw 保持一致);未设置 width 的列不声明宽度,
1433
- * 由 table-layout:fixed 将剩余空间平分,符合“一列固定、其余列平分”的预期。
1434
- */
1435
- function getColGroupColStyle(col: PrivateStkTableColumn<DT>) {
1436
- const width = transformWidthToStr(col.width);
1437
- return width ? `width:${width}` : null;
1438
- }
1439
-
1440
- function getAbsoluteRowIndex(rowIndex: number) {
1441
- return rowIndex + virtualScroll.value.startIndex;
1442
- }
1443
-
1444
- function shouldHideCell(row: PrivateRowDT | null | undefined, col: StkTableColumn<PrivateRowDT>): boolean | undefined {
1445
- if (!hiddenCellMap.value || !row) return;
1446
- return hiddenCellMap.value[rowKeyGen(row)]?.has(colKeyGen.value(col));
1447
- }
1448
-
1449
- /**
1450
- * Get the column list for a body row.
1451
- * - Above-viewport rows: returns a modified list with placeholder entries (__VT_PH__) to reduce DOM nodes.
1452
- * Rows without any must-render cell return [] and consecutive such rows are merged into a single
1453
- * placeholder tr (see aboveRenderParts).
1454
- * - Below-viewport rows: normally merged into placeholder tr segments split at crossing rowspan
1455
- * ends (see belowPhSegments); when still rendered individually (fallback), returns []
1456
- * (tr already has height via CSS, no td needed).
1457
- *
1458
- * 仅在 canMergeEmptyRows(定高虚拟列表)时启用上述剔除:空 tr 依赖 CSS `height: var(--row-height)`
1459
- * 保持高度,autoRowHeight 模式行高靠 td 内容撑开(容器也不定义 --row-height),
1460
- * 剔除 td 会让视口上方行(stripe/rowspan 修正保留的行)塌陷为 0 高,且塌陷高度会被
1461
- * DOM 测量写入行高缓存,导致行高无法正常撑开。
1462
- */
1463
- function getBodyColumns(row: PrivateRowDT | null | undefined, rowIndex: number): PrivateStkTableColumn<PrivateRowDT>[] {
1464
- if (!row) return virtualX_columnPart.value;
1465
- if (canMergeEmptyRows.value) {
1466
- const { startIndex, viewportStartIndex, viewportEndIndex } = virtualScroll.value;
1467
- const aboveCount = viewportStartIndex - startIndex;
1468
- if (aboveCount > 0 && rowIndex < aboveCount) {
1469
- return (aboveViewportColumnMap.value.get(rowKeyGen(row)) || virtualX_columnPart.value) as PrivateStkTableColumn<PrivateRowDT>[];
1470
- }
1471
- // Below-viewport rows: no td needed
1472
- if (startIndex + rowIndex > viewportEndIndex) {
1473
- return [];
1474
- }
1475
- }
1476
- const segments = virtualX_expandColSegments.value;
1477
- if (!segments) return virtualX_columnPart.value;
1478
- return buildExpandColumns(row, rowIndex, segments) as PrivateStkTableColumn<PrivateRowDT>[];
1479
- }
1480
-
1481
- /**
1482
- * 超长 colspan 优化:收缩修正扩展区域(virtualX_expandColSegments)内的行渲染列列表。
1483
- * - 左扩展区:colspan/rowspan 锚点单元格保留真实渲染(否则行列占位断裂);
1484
- * 被覆盖单元格照旧不渲染(槽位由跨越它的单元格占用);其余连续普通单元格
1485
- * 合并为单个 colspan 占位 td(fixed 布局下恰好占用相同列槽位,保持对齐);
1486
- * - 右扩展区:完全不渲染(均在可视视口外,不影响可视单元格对齐)。
1487
- */
1488
- function buildExpandColumns(
1489
- row: PrivateRowDT,
1490
- rowIndex: number,
1491
- segments: {
1492
- prefix: PrivateStkTableColumn<PrivateRowDT>[];
1493
- leftExpand: PrivateStkTableColumn<PrivateRowDT>[];
1494
- viewport: PrivateStkTableColumn<PrivateRowDT>[];
1495
- suffix: PrivateStkTableColumn<PrivateRowDT>[];
1496
- leftExpandStart: number;
1497
- },
1498
- ) {
1499
- const { prefix, leftExpand, viewport, suffix, leftExpandStart } = segments;
1500
- const result: (PrivateStkTableColumn<PrivateRowDT> | { __VT_PH__: number })[] = prefix.slice();
1501
-
1502
- if (leftExpand.length) {
1503
- const hiddenSet = hiddenCellMap.value ? hiddenCellMap.value[rowKeyGen(row)] : void 0;
1504
- const colKeyGenValue = colKeyGen.value;
1505
- const absRowIndex = (virtual_on.value ? virtualScroll.value.startIndex : 0) + rowIndex;
1506
- let run = 0;
1507
- for (let i = 0; i < leftExpand.length; i++) {
1508
- const col = leftExpand[i];
1509
- // 被覆盖单元格:不渲染 td(槽位由上方/左侧的跨越单元格占用)
1510
- if (hiddenSet && hiddenSet.has(colKeyGenValue(col))) continue;
1511
- // 锚点单元格(colspan/rowspan > 1)必须真实渲染,不能占位替代
1512
- if (col.mergeCells) {
1513
- const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, col.__LF_S__ ?? leftExpandStart + i);
1514
- if (colspan > 1 || rowspan > 1) {
1515
- if (run > 0) {
1516
- result.push({ __VT_PH__: run } as PrivateStkTableColumn<PrivateRowDT>);
1517
- run = 0;
1518
- }
1519
- result.push(col);
1520
- continue;
1521
- }
1522
- }
1523
- run++;
1524
- }
1525
- if (run > 0) result.push({ __VT_PH__: run } as PrivateStkTableColumn<PrivateRowDT>);
1526
- }
1527
-
1528
- // 右扩展区不渲染,直接拼接可视区与右侧固定列
1529
- result.push(...viewport, ...suffix);
1530
- return result;
1531
- }
1532
- /** th title */
1533
- function getHeaderTitle(col: StkTableColumn<DT>): string {
1534
- const colKey = colKeyGen.value(col);
1535
- // hide title
1536
- if (props.hideHeaderTitle === true || (Array.isArray(props.hideHeaderTitle) && props.hideHeaderTitle.includes(colKey))) {
1537
- return '';
1538
- }
1539
- return col.title || '';
1540
- }
1541
-
1542
- function getTRProps(row: PrivateRowDT | null | undefined, index: number) {
1543
- const rowIndex = getAbsoluteRowIndex(index);
1544
- const rowKey = rowKeyGen(row);
1545
-
1546
- const classList = [props.rowClassName(row, rowIndex), row?.__EXP__ ? 'expanded' : '', row?.__EXP_R__ ? 'expanded-row' : ''];
1547
- // area selection row highlight
1548
- // if (areaSelectionConfig.value.enabled) {
1549
- // classList.push(...getAreaSelectionRowClass(rowIndex));
1550
- // }
1551
- if (currentRowKey.value === rowKey || row === currentRow.value) {
1552
- classList.push('active');
1553
- }
1554
- if (props.showTrHoverClass && (rowKey === currentHoverRowKey.value || row === currentHoverRow)) {
1555
- classList.push('hover');
1556
- }
1557
-
1558
- const result: Record<string, number | string | null> = {
1559
- id: stkTableId + '-' + rowKey,
1560
- 'data-row-key': rowKey,
1561
- 'data-row-i': rowIndex,
1562
- class: classList.filter(Boolean).join(' '),
1563
- style: null,
1564
- };
1565
-
1566
- const needRowHeight = row?.__EXP_R__ && props.virtual && props.expandConfig?.height;
1567
-
1568
- if (needRowHeight) {
1569
- result.style = `--row-height: ${props.expandConfig?.height}px`;
1570
- }
1571
- return result;
1572
- }
1573
-
1574
- function getTHProps(col: PrivateStkTableColumn<DT>) {
1575
- const colKey = colKeyGen.value(col);
1576
- const sortState = getColumnSortState(colKey);
1577
- const isSorted = !!sortState && sortState.order !== null;
1578
-
1579
- return {
1580
- 'data-col-key': colKey,
1581
- draggable: Boolean(isHeaderDraggable(col)),
1582
- rowspan: col.__R_SP__,
1583
- colspan: col.__C_SP__,
1584
- style: cellStyleMap.value[TagType.TH].get(colKey),
1585
- title: getHeaderTitle(col),
1586
- // class 用预拼接字符串(而非数组),降低每格 vnode diff 与 GC 开销
1587
- class: [
1588
- col.sorter ? 'sortable' : '',
1589
- isSorted ? 'sorter-' + sortState?.order : '',
1590
- col.headerClassName,
1591
- fixedColClassMap.value.get(colKey),
1592
- col.headerAlign === 'left' ? 'text-l' : col.headerAlign === 'right' ? 'text-r' : col.headerAlign === 'center' ? 'text-c' : '',
1593
- ]
1594
- .filter(Boolean)
1595
- .join(' '),
1596
- };
1597
- }
1598
-
1599
- function getTFProps(col: StkTableColumn<DT>) {
1600
- const colKey = colKeyGen.value(col);
1601
- return {
1602
- 'data-col-key': colKey,
1603
- style: cellStyleMap.value[TagType.TF].get(colKey),
1604
- class: [
1605
- col.className,
1606
- fixedColClassMap.value.get(colKey),
1607
- col.type === 'seq' ? 'seq-column' : '',
1608
- col.align === 'center' ? 'text-c' : col.align === 'right' ? 'text-r' : '',
1609
- ]
1610
- .filter(Boolean)
1611
- .join(' '),
1612
- };
1613
- }
1614
-
1615
- function getTDProps(row: PrivateRowDT | null | undefined, col: StkTableColumn<PrivateRowDT>, rowIndex: number, colIndex: number) {
1616
- const colKey = colKeyGen.value(col);
1617
- if (!row) {
1618
- return {
1619
- style: cellStyleMap.value[TagType.TD].get(colKey),
1620
- };
1621
- }
1622
-
1623
- const cellKey = cellKeyGen(row, col);
1624
- const classList = [col.className, fixedColClassMap.value.get(colKey)];
1625
-
1626
- if (col.align === 'center') {
1627
- classList.push('text-c');
1628
- } else if (col.align === 'right') {
1629
- classList.push('text-r');
1630
- }
1631
- if (col.mergeCells) {
1632
- if (hoverMergedCells.value.has(cellKey)) {
1633
- classList.push('cell-hover');
1634
- }
1635
- if (activeMergedCells.value.has(cellKey)) {
1636
- classList.push('cell-active');
1637
- }
1638
- }
1639
-
1640
- if (props.cellActive && currentSelectedCellKey.value === cellKey) {
1641
- classList.push('active');
1642
- }
1643
- // // area selection style
1644
- // if (areaSelectionConfig.value.enabled) {
1645
- // const absRowIndex = getAbsoluteRowIndex(rowIndex);
1646
- // classList.push(...getAreaSelectionClasses(cellKey, absRowIndex, colKey));
1647
- // }
1648
- if (col.type === 'seq') {
1649
- classList.push('seq-column');
1650
- } else if (col.type === 'expand' && (row.__EXP__ ? colKeyGen.value(row.__EXP__) === colKey : false)) {
1651
- classList.push('expanded');
1652
- } else if (row.__T_EXP__ && col.type === 'tree-node') {
1653
- classList.push('tree-expanded');
1654
- } else if (col.type === 'dragRow') {
1655
- classList.push('drag-row-cell');
1656
- }
1657
-
1658
- return {
1659
- 'data-col-key': colKey,
1660
- style: cellStyleMap.value[TagType.TD].get(colKey),
1661
- class: classList.filter(Boolean).join(' '),
1662
- ...mergeCellsWrapper(row, col, rowIndex, (col as PrivateStkTableColumn<DT>).__LF_S__ ?? 0),
1663
- };
1664
- }
1665
-
1666
- function onRowClick(e: MouseEvent) {
1667
- const rowIndex = getClosestTrIndex(e.target as HTMLElement);
1668
- const row = dataSourceCopy.value[rowIndex];
1669
- if (!row) return;
1670
- emits('row-click', e, row, { rowIndex });
1671
- if (rowActiveProp.value.disabled?.(row)) return;
1672
- const isCurrentRow = props.rowKey ? currentRowKey.value === rowKeyGen(row) : currentRow.value === row;
1673
- if (isCurrentRow) {
1674
- if (!rowActiveProp.value.revokable) {
1675
- return;
1676
- }
1677
- setCurrentRow(void 0, { silent: true });
1678
- } else {
1679
- setCurrentRow(row, { silent: true });
1680
- }
1681
- emits('current-change', e, row, { select: !isCurrentRow });
1682
- }
1683
-
1684
- function onRowDblclick(e: MouseEvent) {
1685
- const rowIndex = getClosestTrIndex(e.target as HTMLElement);
1686
- const row = dataSourceCopy.value[rowIndex];
1687
- if (!row) return;
1688
- emits('row-dblclick', e, row, { rowIndex });
1689
- }
1690
-
1691
- function onHeaderMenu(e: MouseEvent) {
1692
- emits('header-row-menu', e);
1693
- }
1694
-
1695
- function onRowMenu(e: MouseEvent) {
1696
- const rowIndex = getClosestTrIndex(e.target as HTMLElement);
1697
- const row = dataSourceCopy.value[rowIndex];
1698
- if (!row) return;
1699
- emits('row-menu', e, row, { rowIndex });
1700
- }
1701
-
1702
- function triangleClick(e: MouseEvent, row: DT, col: StkTableColumn<DT>) {
1703
- if (col.type === 'expand') {
1704
- toggleExpandRow(row, col);
1705
- } else if (col.type === 'tree-node') {
1706
- toggleTreeNode(row, col);
1707
- }
1708
- }
1709
-
1710
- function onCellClick(e: MouseEvent) {
1711
- const rowIndex = getClosestTrIndex(e.target as HTMLElement);
1712
- const row = dataSourceCopy.value[rowIndex];
1713
- if (!row) return;
1714
- const colKey = getClosestColKey(e.target as HTMLElement);
1715
- const col = tableHeaderLast.value.find(item => colKeyGen.value(item) === colKey);
1716
- if (!col) return;
1717
- // Delegated triangle/fold icon click
1718
- if ((e.target as HTMLElement)?.closest('.stk-fold-icon')) {
1719
- triangleClick(e, row, col);
1720
- return;
1721
- }
1722
- if (props.cellActive) {
1723
- const cellKey = cellKeyGen(row, col);
1724
- const result = { row, col, select: false, rowIndex };
1725
- if (props.selectedCellRevokable && currentSelectedCellKey.value === cellKey) {
1726
- currentSelectedCellKey.value = void 0;
1727
- } else {
1728
- currentSelectedCellKey.value = cellKey;
1729
- result.select = true;
1730
- }
1731
- emits('cell-selected', e, result);
1732
- }
1733
- emits('cell-click', e, row, col, { rowIndex });
1734
- }
1735
-
1736
- function getCellEventData(e: MouseEvent) {
1737
- const rowIndex = getClosestTrIndex(e.target as HTMLElement) || 0;
1738
- const row = dataSourceCopy.value[rowIndex];
1739
- const colKey = getClosestColKey(e.target as HTMLElement);
1740
- const col = tableHeaderLast.value.find(item => colKeyGen.value(item) === colKey) as any;
1741
- return { row, col, rowIndex };
1742
- }
1743
-
1744
- /** th click */
1745
- function onHeaderCellClick(e: MouseEvent, col: StkTableColumn<DT>) {
1746
- onColumnSort(col);
1747
- emits('header-cell-click', e, col);
1748
- }
1749
-
1750
- /**
1751
- * Delegated mouseover on tbody: emits cell-mouseover, and simulates cell-mouseenter
1752
- * by checking if relatedTarget is outside the current td.
1753
- */
1754
- function onCellMouseOver(e: MouseEvent) {
1755
- const td = getClosestTd(e.target as HTMLElement);
1756
- if (!td) return;
1757
- const { row, col } = getCellEventData(e);
1758
- emits('cell-mouseover', e, row, col);
1759
- // Simulate cell-mouseenter: relatedTarget is outside this td
1760
- const related = e.relatedTarget as Node | null;
1761
- if (!related || !td.contains(related)) {
1762
- emits('cell-mouseenter', e, row, col);
1763
- }
1764
- }
1765
-
1766
- /**
1767
- * Delegated mouseout on tbody: handles both cell-mouseleave and tr-mouseleave.
1768
- * - cell-mouseleave: relatedTarget is outside the current td.
1769
- * - tr-mouseleave: relatedTarget is outside the current tr (simulates the old onTrMouseLeave).
1770
- */
1771
- function onTbodyMouseOut(e: MouseEvent) {
1772
- const target = e.target as HTMLElement;
1773
- const related = e.relatedTarget as Node | null;
1774
-
1775
- // Cell mouseleave
1776
- const td = getClosestTd(target);
1777
- if (td && (!related || !td.contains(related))) {
1778
- const { row, col } = getCellEventData(e);
1779
- emits('cell-mouseleave', e, row, col);
1780
- }
1781
-
1782
- // Tr mouseleave
1783
- const tr = getClosestTr(target);
1784
- if (tr && (!related || !tr.contains(related))) {
1785
- currentHoverRow = null;
1786
- if (props.showTrHoverClass) {
1787
- currentHoverRowKey.value = null;
1788
- }
1789
- if (props.rowHover) {
1790
- updateHoverMergedCells(void 0);
1791
- }
1792
- }
1793
- }
1794
-
1795
- /** Delegated drop on tbody: extracts rowIndex from the closest tr and calls onTrDrop. */
1796
- function onBodyDrop(e: DragEvent) {
1797
- const trIndex = getClosestTrIndex(e.target as HTMLElement);
1798
- if (trIndex < 0) return;
1799
- onTrDrop(e, getAbsoluteRowIndex(trIndex));
1800
- }
1801
-
1802
- function onCellMouseDown(e: MouseEvent) {
1803
- const { row, col, rowIndex } = getCellEventData(e);
1804
- emits('cell-mousedown', e, row, col, { rowIndex });
1805
-
1806
- if (areaSelectionConfig.value.enabled) {
1807
- onSelectionMouseDown(e);
1808
- }
1809
- }
1810
-
1811
- // isWheeling: true when wheel event is triggered, auto reset to false after delay
1812
- const [isWheeling, setIsWheeling] = useWheeling();
1813
-
1814
- /**
1815
- * proxy scroll, prevent white screen
1816
- * @param e
1817
- */
1818
- function onTableWheel(e: WheelEvent) {
1819
- if (props.smoothScroll) return;
1820
- // if is resizing, not allow scroll
1821
- if (isColResizing.value) {
1822
- e.stopPropagation();
1823
- return;
1824
- }
1825
- const dom = tableContainerRef.value as HTMLElement;
1826
-
1827
- const { deltaY, deltaX, shiftKey } = e;
1828
-
1829
- if (virtual_on.value && deltaY && !shiftKey) {
1830
- const { containerHeight, scrollTop, scrollHeight } = virtualScroll.value;
1831
- // overflow: hidden mode: manually control scroll, preventDefault to block parent scroll
1832
- const canScrollDown = scrollTop < scrollHeight - containerHeight - 1;
1833
- const canScrollUp = scrollTop > 1;
1834
-
1835
- if ((deltaY > 0 && canScrollDown) || (deltaY < 0 && canScrollUp)) {
1836
- // Table can still scroll → mark wheeling & prevent parent
1837
- setIsWheeling(true);
1838
- e.preventDefault();
1839
- } else if (isWheeling()) {
1840
- // At boundary but user is still actively wheeling → briefly prevent parent for smooth transition
1841
- e.preventDefault();
1842
- }
1843
- // At boundary + isWheeling expired → no preventDefault → browser scrolls parent naturally
1844
-
1845
- if (isExperimentalScrollY.value) {
1846
- rafUpdateVirtualScrollYForWheel(scrollTop + deltaY);
1847
- updateCustomScrollbar();
1848
- } else {
1849
- dom.scrollTop += deltaY;
1850
- }
1851
- }
1852
- if (virtualX_on.value) {
1853
- const { containerWidth, scrollLeft, scrollWidth } = virtualScrollX.value;
1854
- let distance = deltaX;
1855
- if (shiftKey && deltaY) {
1856
- distance = deltaY;
1857
- }
1858
- const canScrollRight = scrollLeft < scrollWidth - containerWidth - 1;
1859
- const canScrollLeft = scrollLeft > 1;
1860
-
1861
- if ((distance > 0 && canScrollRight) || (distance < 0 && canScrollLeft)) {
1862
- setIsWheeling(true);
1863
- e.preventDefault();
1864
- } else if (isWheeling()) {
1865
- e.preventDefault();
1866
- }
1867
- dom.scrollLeft += distance;
1868
- }
1869
- }
1870
-
1871
- /** Prevent re-entrant requestAnimationFrame in onTableScroll */
1872
- let scrollRAFScheduled = false;
1873
-
1874
- /**
1875
- * @param e scrollEvent
1876
- */
1877
- function onTableScroll(e: Event) {
1878
- if (!e?.target || scrollRAFScheduled) return;
1879
- scrollRAFScheduled = true;
1880
- requestAnimationFrame(() => {
1881
- scrollRAFScheduled = false;
1882
- const { scrollTop, scrollLeft } = e.target as HTMLElement;
1883
- const { scrollTop: vScrollTop } = virtualScroll.value;
1884
- const { scrollLeft: vScrollLeft } = virtualScrollX.value;
1885
- // 在 experimental.scrollY 模式下,纵向滚动通过 transform 模拟,DOM scrollTop 始终为 0,
1886
- // 不能用来判断纵向滚动,否则横向滚动时会误触发纵向滚动重置
1887
- const isYScroll = isExperimentalScrollY.value ? false : scrollTop !== vScrollTop;
1888
- const isXScroll = scrollLeft !== vScrollLeft;
1889
-
1890
- if (isYScroll) {
1891
- updateVirtualScrollY(scrollTop);
1892
- }
1893
-
1894
- if (isXScroll) {
1895
- if (virtualX_on.value) {
1896
- updateVirtualScrollX(scrollLeft);
1897
- } else {
1898
- // en: Record the scroll position. Used to determine isXScroll
1899
- virtualScrollX.value.scrollLeft = scrollLeft;
1900
- }
1901
- updateFixedShadow(virtualScrollX);
1902
- }
1903
-
1904
- if (isYScroll) {
1905
- const { startIndex, endIndex } = virtualScroll.value;
1906
- emits('scroll', e, { startIndex, endIndex });
1907
- }
1908
- if (isXScroll) {
1909
- emits('scroll-x', e);
1910
- }
1911
-
1912
- updateCustomScrollbar();
1913
- });
1914
- }
1915
-
1916
- /** tr hover */
1917
- function onTrMouseOver(e: MouseEvent) {
1918
- const tr = getClosestTr(e.target as HTMLElement);
1919
- if (!tr) return;
1920
- const rowIndex = Number(tr.dataset.rowI);
1921
- const row = dataSourceCopy.value[rowIndex];
1922
- if (currentHoverRow === row) return;
1923
- currentHoverRow = row;
1924
- const rowKey = tr.dataset.rowKey;
1925
- if (props.showTrHoverClass) {
1926
- currentHoverRowKey.value = rowKey || null;
1927
- }
1928
- if (props.rowHover) {
1929
- updateHoverMergedCells(rowKey);
1930
- }
1931
- }
1932
-
1933
- /**
1934
- * 选中一行
1935
- *
1936
- * en: Select a row
1937
- * @param {string} rowKeyOrRow selected rowKey, undefined to unselect
1938
- * @param {boolean} option.silent if set true not emit `current-change`. default:false
1939
- * @param {boolean} option.deep if set true, deep search in children. default:false
1940
- */
1941
- function setCurrentRow(rowKeyOrRow: string | undefined | DT, option: { silent?: boolean; deep?: boolean } = { silent: false, deep: false }) {
1942
- const select = rowKeyOrRow !== void 0;
1943
- const currentRowTemp = currentRow.value;
1944
- if (!select) {
1945
- currentRow.value = void 0;
1946
- currentRowKey.value = void 0;
1947
- updateActiveMergedCells(true);
1948
- } else if (typeof rowKeyOrRow === 'string') {
1949
- const findRowByKey = (data: DT[], key: string): DT | null => {
1950
- for (let i = 0; i < data.length; i++) {
1951
- const item = data[i];
1952
- if (rowKeyGen(item) === key) {
1953
- return item;
1954
- }
1955
- if (option.deep && item.children?.length) {
1956
- const found = findRowByKey(item.children, key);
1957
- if (found) {
1958
- return found;
1959
- }
1960
- }
1961
- }
1962
- return null;
1963
- };
1964
-
1965
- currentRowKey.value = rowKeyOrRow;
1966
- updateActiveMergedCells(false, currentRowKey.value);
1967
- const row = findRowByKey(dataSourceCopy.value || [], rowKeyOrRow);
1968
- if (!row) {
1969
- console.warn('setCurrentRow failed.rowKey:', rowKeyOrRow);
1970
- return;
1971
- }
1972
- currentRow.value = row;
1973
- } else {
1974
- currentRow.value = rowKeyOrRow;
1975
- currentRowKey.value = rowKeyGen(rowKeyOrRow);
1976
- updateActiveMergedCells(false, currentRowKey.value);
1977
- }
1978
- if (!option.silent) {
1979
- emits('current-change', /** no Event */ null, select ? currentRow.value : currentRowTemp, { select });
1980
- }
1981
- }
1982
-
1983
- /**
1984
- * set highlight active cell (props.cellActive=true)
1985
- * @param row row if undefined, clear highlight
1986
- * @param col column
1987
- * @param option.silent if emit current-change. default:false(not emit `current-change`)
1988
- */
1989
- function setSelectedCell(row?: DT, col?: StkTableColumn<DT>, option = { silent: false }) {
1990
- if (!dataSourceCopy.value.length) return;
1991
- const select = row !== void 0 && col !== void 0;
1992
- currentSelectedCellKey.value = select ? cellKeyGen(row, col) : void 0;
1993
- if (!option.silent) {
1994
- emits('cell-selected', /** no Event */ null, { row, col, select });
1995
- }
1996
- }
1997
-
1998
- /** smooth 滚动动画时长(ms) */
1999
- const SMOOTH_SCROLL_DURATION = 300;
2000
- let smoothScrollRafId = 0;
2001
- /** 动画代际序号,取消后旧动画的 rAF 回调不再生效 */
2002
- let smoothScrollSeq = 0;
2003
- /** 当前动画的手势监听清理函数(监听直接挂 cancelSmoothScroll,无额外包装) */
2004
- let removeGestureListeners: (() => void) | null = null;
2005
-
2006
- /** 取消进行中的平滑滚动动画 */
2007
- function cancelSmoothScroll() {
2008
- smoothScrollSeq++;
2009
- if (smoothScrollRafId) {
2010
- cancelAnimationFrame(smoothScrollRafId);
2011
- smoothScrollRafId = 0;
2012
- }
2013
- if (removeGestureListeners) {
2014
- removeGestureListeners();
2015
- removeGestureListeners = null;
2016
- }
2017
- }
2018
-
2019
- /**
2020
- * 解析滚动轴目标为绝对像素坐标
2021
- * @param target 数字坐标或 { index, key, px } 目标,undefined 表示不改变该轴
2022
- * @param resolveByIndex 按 index 解析基准偏移,失败返回 null
2023
- * @param resolveByKey 按 key 解析基准偏移,失败返回 null
2024
- * @returns null 表示该轴不滚动
2025
- */
2026
- function resolveScrollAxis(
2027
- target: number | ScrollAxisTarget | undefined,
2028
- resolveByIndex: (index: number) => number | null,
2029
- resolveByKey: (key: string | number) => number | null,
2030
- ): number | null {
2031
- if (target === undefined) return null;
2032
- if (typeof target === 'number') return target;
2033
- let base: number | null;
2034
- if (target.index !== undefined) {
2035
- // index 优先于 key
2036
- base = resolveByIndex(target.index);
2037
- } else if (target.key !== undefined) {
2038
- base = resolveByKey(target.key);
2039
- } else {
2040
- // 仅传 px:基准为 0
2041
- base = 0;
2042
- }
2043
- if (base === null) return null;
2044
- return base + (target.px ?? 0);
2045
- }
2046
-
2047
- /** top 轴基准偏移:目标行之前所有行的累计高度 */
2048
- function resolveTopByIndex(index: number): number | null {
2049
- if (index < 0 || index >= dataSourceCopy.value.length) return null;
2050
- return getRowsHeight(index);
2051
- }
2052
- function resolveTopByKey(key: string | number): number | null {
2053
- const index = dataSourceCopy.value.findIndex(row => rowKeyGen(row) === key);
2054
- return index === -1 ? null : getRowsHeight(index);
2055
- }
2056
-
2057
- /** left 轴基准偏移:目标列之前所有列的宽度之和(与区域选择内部 getColPosition 同算法) */
2058
- function getColLeft(colIndex: number): number {
2059
- const cols = tableHeaderLast.value;
2060
- let left = 0;
2061
- for (let i = 0; i < colIndex; i++) {
2062
- left += getCalculatedColWidth(cols[i]);
2063
- }
2064
- return left;
2065
- }
2066
-
2067
- /** top 轴最大可滚动距离:基于理论内容高度(所有行高之和),不依赖 DOM 测量 */
2068
- function getMaxScrollTop(): number {
2069
- return Math.max(0, getRowsHeight(dataSourceCopy.value.length) - virtualScroll.value.containerHeight);
2070
- }
2071
-
2072
- function resolveLeftByIndex(index: number): number | null {
2073
- if (index < 0 || index >= tableHeaderLast.value.length) return null;
2074
- return getColLeft(index);
2075
- }
2076
- function resolveLeftByKey(key: string | number): number | null {
2077
- const index = tableHeaderLast.value.findIndex(col => col.dataIndex === key);
2078
- return index === -1 ? null : getColLeft(index);
2079
- }
2080
-
2081
- /** 执行滚动(数字重载与 options 重载共用) */
2082
- function applyScrollPosition(top: number | null, left: number | null) {
2083
- if (top !== null) {
2084
- if (isExperimentalScrollY.value) {
2085
- updateVirtualScrollY(top);
2086
- updateCustomScrollbar();
2087
- } else {
2088
- tableContainerRef.value!.scrollTop = top;
2089
- }
2090
- }
2091
- if (left !== null) tableContainerRef.value!.scrollLeft = left;
2092
- }
2093
-
2094
- /** 平滑滚动到目标位置(rAF 动画,ease-out);每帧走与用户滚动相同的路径 */
2095
- function smoothScrollTo(top: number | null, left: number | null) {
2096
- const container = tableContainerRef.value;
2097
- if (!container || (top === null && left === null)) return;
2098
- const startTop = isExperimentalScrollY.value ? virtualScroll.value.scrollTop : container.scrollTop;
2099
- const startLeft = container.scrollLeft;
2100
- const deltaTop = top === null ? 0 : top - startTop;
2101
- const deltaLeft = left === null ? 0 : left - startLeft;
2102
- if (!deltaTop && !deltaLeft) return;
2103
-
2104
- const startTime = Date.now();
2105
- const ease = (t: number) => 1 - Math.pow(1 - t, 3);
2106
- // 用户手势介入时取消动画
2107
- container.addEventListener('wheel', cancelSmoothScroll, { passive: true });
2108
- container.addEventListener('touchstart', cancelSmoothScroll, { passive: true });
2109
- removeGestureListeners = () => {
2110
- container.removeEventListener('wheel', cancelSmoothScroll);
2111
- container.removeEventListener('touchstart', cancelSmoothScroll);
2112
- };
2113
-
2114
- const seq = smoothScrollSeq;
2115
- const step = () => {
2116
- // 动画已被取消(新调用或用户手势),旧回调直接退出
2117
- if (seq !== smoothScrollSeq) return;
2118
- const t = Math.min(1, (Date.now() - startTime) / SMOOTH_SCROLL_DURATION);
2119
- const eased = ease(t);
2120
- applyScrollPosition(top === null ? null : startTop + deltaTop * eased, left === null ? null : startLeft + deltaLeft * eased);
2121
- if (t < 1) {
2122
- smoothScrollRafId = requestAnimationFrame(step);
2123
- } else {
2124
- smoothScrollRafId = 0;
2125
- removeGestureListeners!();
2126
- removeGestureListeners = null;
2127
- }
2128
- };
2129
- smoothScrollRafId = requestAnimationFrame(step);
2130
- }
2131
-
2132
- /**
2133
- * set scroll bar position
2134
- * - 数字重载(向后兼容):`scrollTo(top, left)`,null 表示不改变该轴,省略默认 0
2135
- * - options 重载:`scrollTo({ top, left, behavior })`,每轴可传像素数字或 `{ index, key, px }` 目标,
2136
- * 省略的轴不改变位置;目标无法解析(index 越界 / key 不存在)时该轴静默跳过
2137
- * @param options null to not change
2138
- * @param leftArg null to not change
2139
- */
2140
- function scrollTo(top?: number | null, left?: number | null): void;
2141
- function scrollTo(options: ScrollToOptions): void;
2142
- function scrollTo(options?: ScrollToOptions | number | null, leftArg?: number | null): void {
2143
- if (!tableContainerRef.value) return;
2144
- cancelSmoothScroll();
2145
- if (options === undefined || options === null || typeof options === 'number') {
2146
- // 数字重载:保持既有行为
2147
- applyScrollPosition(options === undefined ? 0 : options, leftArg === undefined ? 0 : leftArg);
2148
- return;
2149
- }
2150
- let top: number | null = resolveScrollAxis(options.top, resolveTopByIndex, resolveTopByKey);
2151
- let left: number | null = resolveScrollAxis(options.left, resolveLeftByIndex, resolveLeftByKey);
2152
- if (top === null && left === null) return;
2153
- // 钳制到合法滚动范围(基于理论内容尺寸,与滚动条语义一致)
2154
- if (top !== null) top = Math.min(Math.max(top, 0), getMaxScrollTop());
2155
- if (left !== null)
2156
- left = Math.min(Math.max(left, 0), Math.max(0, getColLeft(tableHeaderLast.value.length) - virtualScrollX.value.containerWidth));
2157
- if (options.behavior === 'smooth') {
2158
- smoothScrollTo(top, left);
2159
- } else {
2160
- applyScrollPosition(top, left);
2161
- }
2162
- }
2163
- // 编译期断言:实现签名与公共类型 ScrollToFn 保持一致(type-tests 依赖此类型)
2164
- void (scrollTo satisfies ScrollToFn);
2165
-
2166
- /** get current table data */
2167
- function getTableData() {
2168
- return toRaw(dataSourceCopy.value);
2169
- }
2170
-
2171
- /**
2172
- * 清空 mergeCells 结果缓存并强制重算合并结果。
2173
- *
2174
- * 缓存以「绝对行索引 + 叶子列索引」为键、行引用同一性校验命中,
2175
- * 仅在 dataSource/列配置变化时自动清空。业务代码原地修改行字段
2176
- * (行对象引用不变,常见于响应式行对象)且 mergeCells 结果依赖这些字段时,
2177
- * 缓存会命中旧结果,需调用本方法显式失效。
2178
- */
2179
- function clearMergeCellsCache() {
2180
- mergeCellsCache.clear();
2181
- // 重建 dataSourceCopy(新数组引用):触发缓存清空 watch 与渲染更新,
2182
- // 使 hiddenCellMap/hoverRowMap 及渲染出的 rowspan/colspan 全部重算
2183
- initDataSource();
2184
- updateMaxRowSpan();
2185
- }
2186
-
2187
- defineExpose({
2188
- /**
2189
- * 重新计算虚拟列表宽高
2190
- *
2191
- * en: calc virtual scroll x & y info
2192
- * @see {@link initVirtualScroll}
2193
- */
2194
- initVirtualScroll,
2195
- /**
2196
- * 重新计算虚拟列表宽度
2197
- *
2198
- * en: calc virtual scroll x
2199
- * @see {@link initVirtualScrollX}
2200
- */
2201
- initVirtualScrollX,
2202
- /**
2203
- * 重新计算虚拟列表高度
2204
- *
2205
- * en: calc virtual scroll y
2206
- * @see {@link initVirtualScrollY}
2207
- */
2208
- initVirtualScrollY,
2209
- /**
2210
- * 清空 mergeCells 结果缓存并强制重算合并结果
2211
- *
2212
- * en: Clear mergeCells result cache and force recomputation
2213
- * @see {@link clearMergeCellsCache}
2214
- */
2215
- clearMergeCellsCache,
2216
- /**
2217
- * 选中一行
2218
- *
2219
- * en:select a row
2220
- * @see {@link setCurrentRow}
2221
- */
2222
- setCurrentRow,
2223
- /**
2224
- * 取消选中单元格
2225
- *
2226
- * en: set highlight active cell (props.cellActive=true)
2227
- * @see {@link setSelectedCell}
2228
- */
2229
- setSelectedCell,
2230
- /**
2231
- * 设置高亮单元格
2232
- *
2233
- * en: Set highlight cell
2234
- * @see {@link setHighlightDimCell}
2235
- */
2236
- setHighlightDimCell,
2237
- /**
2238
- * 设置高亮行
2239
- *
2240
- * en: Set highlight row
2241
- * @see {@link setHighlightDimRow}
2242
- */
2243
- setHighlightDimRow,
2244
- /**
2245
- * 表格排序列colKey
2246
- *
2247
- * en: Table sort column colKey
2248
- */
2249
- sortCol,
2250
- /**
2251
- * 排序状态数组
2252
- *
2253
- * en: Multi-column sort states array
2254
- */
2255
- sortStates,
2256
- /**
2257
- * 表格排序列顺序
2258
- *
2259
- * en: get current sort info
2260
- * @see {@link getSortColumns}
2261
- */
2262
- getSortColumns,
2263
- /**
2264
- * 设置表头排序状态
2265
- *
2266
- * en: Set the sort status of the table header
2267
- * @see {@link setSorter}
2268
- */
2269
- setSorter,
2270
- /**
2271
- * 重置sorter状态
2272
- *
2273
- * en: Reset the sorter status
2274
- * @see {@link resetSorter}
2275
- */
2276
- resetSorter,
2277
- /**
2278
- * 滚动至
2279
- * - 数字参数:scrollTo(top, left),null 表示不改变该轴
2280
- * - 对象参数:scrollTo({ top, left, behavior }),top/left 可传像素数字或 { index, key, px } 目标
2281
- *
2282
- * en: Scroll to position or target row/column
2283
- * @see {@link scrollTo}
2284
- */
2285
- scrollTo,
2286
- /**
2287
- * 获取表格数据
2288
- *
2289
- * en: Get table data
2290
- * @see {@link getTableData}
2291
- */
2292
- getTableData,
2293
- getRowIndex,
2294
- getColumnIndex,
2295
- /**
2296
- * 设置展开的行
2297
- *
2298
- * en: Set expanded rows
2299
- * @see {@link setRowExpand}
2300
- */
2301
- setRowExpand,
2302
- /**
2303
- * 不定行高时,如果行高有变化,则调用此方法更新行高。
2304
- *
2305
- * en: When the row height is not fixed, call this method to update the row height if the row height changes.
2306
- * @see {@link setAutoHeight}
2307
- */
2308
- setAutoHeight,
2309
- /**
2310
- * 清除所有行高
2311
- *
2312
- * en: Clear all row heights
2313
- * @see {@link clearAllAutoHeight}
2314
- */
2315
- clearAllAutoHeight,
2316
- /**
2317
- * 设置树节点展开状态
2318
- *
2319
- * en: Set tree node expand state
2320
- * @see {@link setTreeExpand}
2321
- */
2322
- setTreeExpand,
2323
- /**
2324
- * 获取拖选选中的单元格信息
2325
- *
2326
- * en: Get selected cells info (areaSelection=true)
2327
- * @see {@link getSelectedArea}
2328
- */
2329
- getSelectedArea,
2330
- /**
2331
- * 设置拖选选区
2332
- *
2333
- * en: Set cell selection range (areaSelection=true)
2334
- * @see {@link setAreaSelection}
2335
- */
2336
- setAreaSelection,
2337
- /**
2338
- * 清空拖选选区
2339
- *
2340
- * en: Clear cell selection range (areaSelection=true)
2341
- * @see {@link clearSelectedArea}
2342
- */
2343
- clearSelectedArea,
2344
- /**
2345
- * 复制选区内容到剪贴板
2346
- *
2347
- * en: Copy selected area to clipboard (areaSelection=true)
2348
- * @see {@link copySelectedArea}
2349
- */
2350
- copySelectedArea,
2351
- /**
2352
- * 设置筛选状态(Beta)
2353
- *
2354
- * en: Set filter status(Beta)
2355
- * @see {@link setFilter}
2356
- */
2357
- setFilter,
2358
- });
2359
- </script>
1
+ <!-- eslint-disable vue/attribute-hyphenation -->
2
+ <template>
3
+ <div
4
+ ref="tableContainerRef"
5
+ class="stk-table"
6
+ :class="{
7
+ virtual,
8
+ 'virtual-x': virtualX,
9
+ 'vt-on': virtual_on,
10
+ light: theme === 'light',
11
+ dark: theme === 'dark',
12
+ headless,
13
+ 'is-col-resizing': isColResizing,
14
+ 'col-resizable': props.colResizable,
15
+ bordered: props.bordered,
16
+ [`bordered-${props.bordered}`]: typeof props.bordered === 'string',
17
+ stripe: props.stripe,
18
+ 'cell-hover': props.cellHover,
19
+ 'cell-active': props.cellActive,
20
+ 'row-hover': props.rowHover,
21
+ 'row-active': rowActiveProp.enabled,
22
+ 'text-overflow': props.showOverflow,
23
+ 'header-text-overflow': props.showHeaderOverflow,
24
+ 'fixed-relative-mode': isRelativeMode,
25
+ 'auto-row-height': props.autoRowHeight,
26
+ 'scroll-row-by-row': isSRBRActive,
27
+ 'scrollbar-on': scrollbarOptions.enabled,
28
+ 'area-selection': areaSelectionConfig.enabled,
29
+ 'is-area-selecting': isAreaSelecting,
30
+ 'exp-scroll-y': isExperimentalScrollY,
31
+ }"
32
+ :tabindex="areaSelectionConfig.enabled ? 0 : void 0"
33
+ :style="{
34
+ '--row-height': props.autoRowHeight ? void 0 : virtualScroll.rowHeight + 'px',
35
+ '--header-row-height': props.headerRowHeight + 'px',
36
+ '--footer-row-height': props.footerRowHeight + 'px',
37
+ '--highlight-duration': props.highlightConfig.duration && props.highlightConfig.duration + 's',
38
+ '--highlight-timing-function': highlightSteps ? `steps(${highlightSteps})` : void 0,
39
+ '--sb-width': `${scrollbarOptions.width}px`,
40
+ '--sb-height': `${scrollbarOptions.height}px`,
41
+ }"
42
+ @scroll="onTableScroll"
43
+ @wheel="onTableWheel"
44
+ >
45
+ <div v-if="!isExperimentalScrollY && SRBRTotalHeight" class="row-by-row-table-height" :style="`height: ${SRBRTotalHeight}px`"></div>
46
+
47
+ <div v-if="colResizable" ref="colResizeIndicatorRef" class="column-resize-indicator"></div>
48
+
49
+ <div class="stk-table-scroll-container">
50
+ <table
51
+ class="stk-table-main"
52
+ :style="{ width, minWidth, maxWidth }"
53
+ :class="{
54
+ 'fixed-mode': props.fixedMode,
55
+ }"
56
+ @dragover="onTrDragOver"
57
+ @dragenter="onTrDragEnter"
58
+ @dragend="onTrDragEnd"
59
+ @click="onRowClick"
60
+ @dblclick="onRowDblclick"
61
+ @contextmenu="onRowMenu"
62
+ @mouseover="onTrMouseOver"
63
+ >
64
+ <!-- table-layout:fixed 下浏览器仅依据首行/colgroup 决定列宽,多级表头时子列宽度位于非首行会被忽略。
65
+ 故固定模式下通过 colgroup 显式声明每个叶子列宽度,保证子列 width 生效。 -->
66
+ <colgroup v-if="props.fixedMode && !virtualX_on">
67
+ <col v-for="col in tableHeaderLast" :key="colKeyGen(col)" :style="getColGroupColStyle(col)" />
68
+ </colgroup>
69
+ <thead v-if="!headless">
70
+ <tr
71
+ v-for="(row, rowIndex) in virtualX_on ? virtualX_tableHeaders : tableHeaders"
72
+ :key="rowIndex"
73
+ @contextmenu="onHeaderMenu($event)"
74
+ >
75
+ <th
76
+ v-if="virtualX_on"
77
+ class="vt-x-left"
78
+ :style="`min-width:${theadVirtualX.offsetLeft}px;width:${theadVirtualX.offsetLeft}px`"
79
+ ></th>
80
+ <th
81
+ v-for="(col, colIndex) in row"
82
+ :key="colKeyGen(col)"
83
+ v-bind="getTHProps(col)"
84
+ @click="e => onHeaderCellClick(e, col)"
85
+ @dragstart="onThDragStart"
86
+ @drop="onThDrop"
87
+ @dragover="onThDragOver"
88
+ >
89
+ <div
90
+ v-if="colResizeOn(col) && colIndex > 0"
91
+ class="table-header-resizer left"
92
+ @mousedown="onThResizeMouseDown($event, col, true)"
93
+ ></div>
94
+ <div class="table-header-cell-wrapper" :style="col.__R_SP__ ? `--row-span:${col.__R_SP__}` : null">
95
+ <component
96
+ :is="col.customHeaderCell"
97
+ v-if="col.customHeaderCell"
98
+ :col="col"
99
+ :colIndex="colIndex"
100
+ :rowIndex="rowIndex"
101
+ />
102
+ <template v-else>
103
+ <slot name="tableHeader" :col="col">
104
+ <span class="table-header-title">{{ col.title }}</span>
105
+ </slot>
106
+ </template>
107
+ <SortIcon v-if="col.sorter" class="table-header-sorter" />
108
+ </div>
109
+ <div v-if="colResizeOn(col)" class="table-header-resizer right" @mousedown="onThResizeMouseDown($event, col)"></div>
110
+ </th>
111
+ <th v-if="virtualX_on" class="vt-x-right" :style="`min-width:${virtualX_offsetRight}px;width:${virtualX_offsetRight}px`"></th>
112
+ </tr>
113
+ </thead>
114
+
115
+ <component
116
+ :is="footerTagName"
117
+ v-if="footerData && footerData.length > 0"
118
+ class="stk-footer"
119
+ :style="isFooterTop ? `top:${tableHeaderHeight}px` : ''"
120
+ >
121
+ <tr v-for="(footRow, footRowIndex) in footerData" :key="footRowIndex">
122
+ <td
123
+ v-if="virtualX_on"
124
+ class="vt-x-left"
125
+ :style="`min-width:${theadVirtualX.offsetLeft}px;width:${theadVirtualX.offsetLeft}px`"
126
+ ></td>
127
+ <template v-for="(col, _colIdx) in virtualX_columnPart" :key="col.__VT_C_SP__ ? `spacer-${_colIdx}` : colKeyGen(col)">
128
+ <td v-if="col.__VT_C_SP__" class="vt-x-spacer" :colspan="col.__VT_C_SP__"></td>
129
+ <td v-else v-bind="getTFProps(col)">
130
+ <component
131
+ :is="col.customFooterCell"
132
+ v-if="col.customFooterCell"
133
+ class="table-cell-wrapper"
134
+ tabindex="-1"
135
+ :col="col"
136
+ :row="footRow"
137
+ :rowIndex="footRowIndex"
138
+ :cellValue="footRow[col.dataIndex]"
139
+ />
140
+ <div class="table-cell-wrapper" tabindex="-1" :title="footRow[col.dataIndex] || ''">
141
+ <span v-if="footRow[col.dataIndex] != null">{{ footRow[col.dataIndex] }}</span>
142
+ </div>
143
+ </td>
144
+ </template>
145
+ <td v-if="virtualX_on" class="vt-x-right" :style="`min-width:${virtualX_offsetRight}px;width:${virtualX_offsetRight}px`"></td>
146
+ </tr>
147
+ </component>
148
+
149
+ <tbody
150
+ class="stk-tbody-main"
151
+ :style="isExperimentalScrollY ? `transform:translateY(${virtualScroll.translateY}px)` : ''"
152
+ @click="onCellClick"
153
+ @mousedown="onCellMouseDown"
154
+ @mouseover="onCellMouseOver"
155
+ @mouseout="onTbodyMouseOut"
156
+ @drop="onBodyDrop"
157
+ >
158
+ <tr v-if="!isExperimentalScrollY && virtual_on && !isSRBRActive" :style="paddingTopStyle" class="padding-top-tr">
159
+ <template v-if="fixedMode && headless">
160
+ <td
161
+ v-if="virtualX_on"
162
+ class="vt-x-left"
163
+ :style="`min-width:${theadVirtualX.offsetLeft}px;width:${theadVirtualX.offsetLeft}px`"
164
+ ></td>
165
+ <template v-for="(col, _colIdx) in virtualX_columnPart" :key="col.__VT_C_SP__ ? `spacer-${_colIdx}` : colKeyGen(col)">
166
+ <td v-if="col.__VT_C_SP__" class="vt-x-spacer" :colspan="col.__VT_C_SP__"></td>
167
+ <td v-else :style="cellStyleMap[TagType.TD].get(colKeyGen(col))"></td>
168
+ </template>
169
+ <td
170
+ v-if="virtualX_on"
171
+ class="vt-x-right"
172
+ :style="`min-width:${virtualX_offsetRight}px;width:${virtualX_offsetRight}px`"
173
+ ></td>
174
+ </template>
175
+ </tr>
176
+ <!-- tbody 渲染列表(异构):
177
+ - row: 数据行(视口行 + 视口上方保留的锚点行/孤立空行)
178
+ - above-ph: 视口上方连续空行(无 td)合并的占位段
179
+ - below-ph: 视口下方行(rowspan 修正产生的 viewportEndIndex ~ endIndex 区域)
180
+ 按跨界 rowspan 结束行切分的占位段
181
+ 占位 tr 以 height: calc(var(--row-height) * 行数) 保持总高度,大 rowspan 场景显著减少 DOM 节点数 -->
182
+ <!-- key 必须在 template 上:v-for 模板含多个条件分支时子节点 key 不会提升到 Fragment,
183
+ 会导致 Fragment 无 key 走 unkeyed diff,滚动时整行重建 -->
184
+ <template v-for="item in bodyRenderItems" :key="item.key">
185
+ <tr v-if="item.type === 'row'" ref="trRef" v-bind="getTRProps(item.row, item.rowIndex)">
186
+ <td v-if="item.row && item.row.__EXP_R__" :colspan="expandRowColspan">
187
+ <div class="table-cell-wrapper" tabindex="-1">
188
+ <slot name="expand" :row="item.row.__EXP_R__" :col="item.row.__EXP_C__">
189
+ {{ (item.row.__EXP_R__ && item.row.__EXP_C__ && item.row.__EXP_R__[item.row.__EXP_C__.dataIndex]) || '' }}
190
+ </slot>
191
+ </div>
192
+ </td>
193
+ <template v-else>
194
+ <td v-if="virtualX_on" class="vt-x-left"></td>
195
+ <template
196
+ v-for="(col, _colIdx) in getBodyColumns(item.row, item.rowIndex)"
197
+ :key="col.__VT_C_SP__ ? `spacer-${_colIdx}` : col.__VT_PH__ ? `ph-${_colIdx}` : colKeyGen(col)"
198
+ >
199
+ <td v-if="col.__VT_C_SP__" class="vt-x-spacer" :colspan="col.__VT_C_SP__"></td>
200
+ <td v-else-if="col.__VT_PH__" class="vt-above-viewport-ph" :colspan="col.__VT_PH__"></td>
201
+ <td
202
+ v-else-if="!shouldHideCell(item.row, col)"
203
+ v-bind="getTDProps(item.row, col, item.rowIndex, (col as PrivateStkTableColumn<DT>).__LF_S__ ?? 0)"
204
+ >
205
+ <component
206
+ :is="col.customCell"
207
+ v-if="col.customCell"
208
+ class="table-cell-wrapper"
209
+ tabindex="-1"
210
+ :col="col"
211
+ :row="item.row"
212
+ :rowIndex="getAbsoluteRowIndex(item.rowIndex)"
213
+ :colIndex="(col as PrivateStkTableColumn<DT>).__LF_S__ ?? 0"
214
+ :cellValue="item.row && item.row[col.dataIndex]"
215
+ :expanded="item.row && item.row.__EXP__"
216
+ :tree-expanded="item.row && item.row.__T_EXP__"
217
+ >
218
+ <template #stkFoldIcon>
219
+ <TriangleIcon @click="triangleClick($event, item.row, col)"></TriangleIcon>
220
+ </template>
221
+ <template #stkDragIcon>
222
+ <DragHandle @dragstart="onTrDragStart($event, getAbsoluteRowIndex(item.rowIndex))" />
223
+ </template>
224
+ </component>
225
+ <div v-else-if="!col.type" class="table-cell-wrapper" tabindex="-1" :title="item.row[col.dataIndex] || ''">
226
+ {{
227
+ (item.row && item.row[col.dataIndex]) != null
228
+ ? item.row && item.row[col.dataIndex]
229
+ : getEmptyCellText(col, item.row)
230
+ }}
231
+ </div>
232
+ <div v-else-if="col.type === 'seq'" class="table-cell-wrapper" tabindex="-1">
233
+ {{ (props.seqConfig.startIndex || 0) + getAbsoluteRowIndex(item.rowIndex) + 1 }}
234
+ </div>
235
+ <TreeNodeCell
236
+ v-else-if="col.type === 'tree-node'"
237
+ class="table-cell-wrapper"
238
+ tabindex="-1"
239
+ :col="col"
240
+ :row="item.row"
241
+ ></TreeNodeCell>
242
+ <div v-else class="table-cell-wrapper" tabindex="-1" :title="item.row[col.dataIndex] || ''">
243
+ <DragHandle
244
+ v-if="col.type === 'dragRow'"
245
+ @dragstart="onTrDragStart($event, getAbsoluteRowIndex(item.rowIndex))"
246
+ />
247
+ <TriangleIcon v-else-if="col.type === 'expand'" />
248
+ <span v-if="item.row[col.dataIndex] != null">{{ item.row[col.dataIndex] }}</span>
249
+ </div>
250
+ </td>
251
+ </template>
252
+ <td v-if="virtualX_on" class="vt-x-right"></td>
253
+ </template>
254
+ </tr>
255
+ <tr
256
+ v-else-if="item.type === 'above-ph'"
257
+ class="vt-above-viewport-ph-row"
258
+ :data-above-count="item.count"
259
+ :style="`height: calc(var(--row-height) * ${item.count})`"
260
+ ></tr>
261
+ <tr
262
+ v-else
263
+ class="vt-below-viewport-ph"
264
+ :data-below-count="item.count"
265
+ :style="`height: calc(var(--row-height) * ${item.count})`"
266
+ ></tr>
267
+ </template>
268
+ <template v-if="!isExperimentalScrollY">
269
+ <tr v-if="virtual_on && !isSRBRActive" :style="offsetBottomStyle"></tr>
270
+ <tr v-if="SRBRBottomHeight" :style="SRBRBottomStyle"></tr>
271
+ </template>
272
+ </tbody>
273
+ </table>
274
+ <div
275
+ v-if="scrollbarOptions.enabled && showScrollbar.y"
276
+ class="stk-sb-thumb vertical"
277
+ :style="`height:${scrollbar.h}px;transform:translateY(${scrollbar.t}px)`"
278
+ @mousedown="onVerticalScrollbarMouseDown"
279
+ @touchstart.passive="onVerticalScrollbarMouseDown"
280
+ ></div>
281
+ </div>
282
+ <div v-if="(!dataSourceCopy || !dataSourceCopy.length) && showNoData" class="stk-table-no-data" :class="{ 'no-data-full': noDataFull }">
283
+ <slot name="empty">暂无数据</slot>
284
+ </div>
285
+ <slot name="customBottom"></slot>
286
+ <div
287
+ v-if="scrollbarOptions.enabled && showScrollbar.x"
288
+ class="stk-sb-thumb horizontal"
289
+ :style="`width:${scrollbar.w}px;transform:translateX(${scrollbar.l}px)`"
290
+ @mousedown="onHorizontalScrollbarMouseDown"
291
+ @touchstart.passive="onHorizontalScrollbarMouseDown"
292
+ ></div>
293
+ </div>
294
+ </template>
295
+
296
+ <!-- Vue 2.7 compatible: add component name -->
297
+ <script lang="ts">
298
+ export default {
299
+ name: 'StkTable',
300
+ };
301
+ </script>
302
+
303
+ <script setup lang="ts">
304
+ /**
305
+ * StkTable 主组件(Vue 3 / Vue 2.7)
306
+ *
307
+ * 这是一个超大的核心组件文件,约 1800+ 行。理解它时请按以下「分区地图」定位:
308
+ *
309
+ * 1. 顶部(template 261 行前)
310
+ * 渲染结构:滚动容器 + 原生 <table>。包括 colgroup(固定模式列宽)、thead(多级表头、
311
+ * sticky 固定)、tbody(虚拟滚动 tr / 空 td 定位)、tfoot(汇总行)、自定义滚动条容器。
312
+ * 通过 CSS 类(如 virtual / virtual-x / area-selection / theme 等)联动对应 hook 状态。
313
+ *
314
+ * 2. script setup(270 行起)
315
+ * - props 定义:全部配置项,即公共 API(对应 types/index.ts 与文档 table-props.md)。
316
+ * - 大量 useXxx hooks 的组装:本文件只做「装配」,具体逻辑在对应 hook 中。
317
+ *
318
+ * 3. 核心 hook 与职责(均在 src/StkTable/use*.ts):
319
+ * - useVirtualScroll XY 轴虚拟滚动(可视区裁剪、占位 tr/th 定位、scroll 计算)
320
+ * - useTableColumns 列配置归一化 / 多级表头展开为叶子列
321
+ * - useFixedCol/useFixedStyle/getFixedColPosition 固定列 sticky/relative 定位
322
+ * - useSorter/useTableColumns.sort 排序(本地/远端、多列)
323
+ * - useColResize/useThDrag/useTrDrag 列宽调整 / 表头拖拽 / 行拖拽
324
+ * - useMergeCells/useMaxRowSpan 单元格合并与最大行跨
325
+ * - useRowExpand/useTree 展开行 / 树形
326
+ * - useHighlight 单元格/行高亮(Web Animations API 或 css)
327
+ * - useAreaSelection (features/) 区域选取(Excel 式键盘操作)
328
+ * - useScrollbar/useScrollRowByRow/useWheeling 自定义滚动条 / 按行滚动 / 滚轮
329
+ * - useAutoResize 容器尺寸自动适配
330
+ *
331
+ * 4. expose:对外暴露的实例方法(对应文档 expose.md),供 ref 调用。
332
+ *
333
+ * 注意:此文件与 vue2.7 兼容,props 不可抽离到独立文件(见下方注释),修改时注意不要破坏兼容性。
334
+ *
335
+ * @author japlus
336
+ */
337
+ import { computed, nextTick, onMounted, provide, ref, shallowRef, toRaw, toRef, watch } from 'vue';
338
+ import DragHandle from './components/DragHandle.vue';
339
+ import SortIcon from './components/SortIcon.vue';
340
+ import TreeNodeCell from './components/TreeNodeCell.vue';
341
+ import TriangleIcon from './components/TriangleIcon.vue';
342
+ import {
343
+ CELL_KEY_SEPARATE,
344
+ DEFAULT_ROW_ACTIVE_CONFIG,
345
+ DEFAULT_ROW_HEIGHT,
346
+ DEFAULT_SMOOTH_SCROLL,
347
+ DEFAULT_SORT_CONFIG,
348
+ IS_LEGACY_MODE,
349
+ } from './const';
350
+ import type { FilterStatus } from './custom-cells/FilterCell/types';
351
+ import { useAreaSelectionName } from './features';
352
+ import { ON_DEMAND_FEATURE } from './registerFeature';
353
+ import {
354
+ AreaSelectionConfig,
355
+ AreaSelectionRange,
356
+ AutoRowHeightConfig,
357
+ ColResizableConfig,
358
+ DragRowConfig,
359
+ ExpandConfig,
360
+ ExperimentalConfig,
361
+ FooterConfig,
362
+ HeaderDragConfig,
363
+ HighlightConfig,
364
+ Order,
365
+ PrivateRowDT,
366
+ PrivateStkTableColumn,
367
+ RowActiveOption,
368
+ ScrollAxisTarget,
369
+ ScrollToFn,
370
+ ScrollToOptions,
371
+ SeqConfig,
372
+ SortConfig,
373
+ StkTableColumn,
374
+ TagType,
375
+ TreeConfig,
376
+ UniqKey,
377
+ UniqKeyProp,
378
+ } from './types/index';
379
+ import { useAutoResize } from './useAutoResize';
380
+ import { useColResize } from './useColResize';
381
+ import { useFixedCol } from './useFixedCol';
382
+ import { useFixedStyle } from './useFixedStyle';
383
+ import { useGetFixedColPosition } from './useGetFixedColPosition';
384
+ import { useHighlight } from './useHighlight';
385
+ import { useKeyboardArrowScroll } from './useKeyboardArrowScroll';
386
+ import { useMaxRowSpan } from './useMaxRowSpan';
387
+ import { createMergeCellsCache } from './mergeCellsCache';
388
+ import { useMergeCells } from './useMergeCells';
389
+ import { useRowExpand } from './useRowExpand';
390
+ import { useScrollbar, type ScrollbarOptions } from './useScrollbar';
391
+ import { useScrollRowByRow } from './useScrollRowByRow';
392
+ import { useSorter } from './useSorter';
393
+ import { useTableColumns } from './useTableColumns';
394
+ import { useThDrag } from './useThDrag';
395
+ import { useTrDrag } from './useTrDrag';
396
+ import { useTree } from './useTree';
397
+ // import { useIndexResolver } from './useIndexResolver';
398
+ import { useVirtualScroll } from './useVirtualScroll';
399
+ import { useWheeling } from './useWheeling';
400
+ import { createStkTableId, getCalculatedColWidth } from './utils/constRefUtils';
401
+ import { getClosestColKey, getClosestTd, getClosestTr, getClosestTrIndex, rafThrottle, transformWidthToStr } from './utils/index';
402
+
403
+ /** Generic stands for DataType */
404
+ type DT = any & PrivateRowDT;
405
+
406
+ /** generate table instance id */
407
+ const stkTableId = createStkTableId();
408
+
409
+ // Vue 2.7 compatible: use export default instead of defineOptions
410
+ // defineOptions({ name: 'StkTable' });
411
+ /**
412
+ * props cannot be placed in a separate file. It will cause compilation errors with vue 2.7 compiler.
413
+ */
414
+ const props = withDefaults(
415
+ defineProps<{
416
+ /** 表格宽度*/
417
+ width?: string;
418
+ /** 最小表格宽度 @deprecated*/
419
+ minWidth?: string;
420
+ /** 表格最大宽度 @deprecated*/
421
+ maxWidth?: string;
422
+ /** 斑马线条纹 */
423
+ stripe?: boolean;
424
+ /** 是否使用 table-layout:fixed(低版本浏览器需要设置table) */
425
+ fixedMode?: boolean;
426
+ /** 是否隐藏表头 */
427
+ headless?: boolean;
428
+ /** 主题,亮、暗 */
429
+ theme?: 'light' | 'dark';
430
+ /**
431
+ * 行高
432
+ * - `props.autoRowHeight` 为 `true` 时,将表示为期望行高,用于计算。不再影响实际行高。
433
+ * - 支持动态修改,修改后自动重算可视区行数与滚动高度,并与 `--row-height` 保持同步。
434
+ */
435
+ rowHeight?: number;
436
+ /**
437
+ * 是否可变行高
438
+ * - 设置为 `true` 时, `props.rowHeight` 将表示为期望行高,用于计算。不再影响实际行高。
439
+ */
440
+ autoRowHeight?: boolean | AutoRowHeightConfig<DT>;
441
+ /** 是否高亮鼠标悬浮的行 */
442
+ rowHover?: boolean;
443
+ /** 是否高亮选中的行 */
444
+ rowActive?: boolean | RowActiveOption<DT>;
445
+ /**
446
+ * @deprecated
447
+ */
448
+ rowCurrentRevokable?: boolean;
449
+ /**
450
+ * 表头行高。default = rowHeight
451
+ * - 支持动态修改,表头占用的表体行数(pageSize)随之重算。
452
+ */
453
+ headerRowHeight?: number | string;
454
+ /**
455
+ * 表尾行高。default = rowHeight
456
+ * - 支持动态修改。表尾行悬浮于滚动区底部,不计入虚拟滚动的总高度。
457
+ */
458
+ footerRowHeight?: number | string;
459
+ /** 虚拟滚动 */
460
+ virtual?: boolean;
461
+ /** x轴虚拟滚动(必须设置列宽)*/
462
+ virtualX?: boolean;
463
+ /** 表格列配置 */
464
+ columns?: StkTableColumn<DT>[];
465
+ /** 表格数据源 */
466
+ dataSource?: DT[];
467
+ /** 行唯一键 (行唯一值不能为undefined) */
468
+ rowKey?: UniqKeyProp;
469
+ /** 列唯一键 */
470
+ colKey?: UniqKeyProp;
471
+ /** 空值展示文字 */
472
+ emptyCellText?: string | ((option: { row: DT; col: StkTableColumn<DT> }) => string);
473
+ /** 暂无数据兜底高度是否撑满 */
474
+ noDataFull?: boolean;
475
+ /** 是否展示暂无数据 */
476
+ showNoData?: boolean;
477
+ /** 是否服务端排序,true则不排序数据 */
478
+ sortRemote?: boolean;
479
+ /** 表头是否溢出展示... */
480
+ showHeaderOverflow?: boolean;
481
+ /** 表体溢出是否展示... */
482
+ showOverflow?: boolean;
483
+ /** 是否增加行hover class $*$ rename*/
484
+ showTrHoverClass?: boolean;
485
+ /** 是否高亮鼠标悬浮的单元格 */
486
+ cellHover?: boolean;
487
+ /** 是否高亮选中的单元格 */
488
+ cellActive?: boolean;
489
+ /** 单元格再次点击否可以取消选中 (cellActive=true)*/
490
+ selectedCellRevokable?: boolean;
491
+ /** 是否启用单元格范围选中(拖拽选区) */
492
+ areaSelection?: boolean | AreaSelectionConfig;
493
+ /** 表头是否可拖动。支持回调函数。 */
494
+ headerDrag?: boolean | HeaderDragConfig<DT>;
495
+ /**
496
+ * 给行附加className<br>
497
+ * FIXME: 是否需要优化,因为不传此prop会使表格行一直执行空函数,是否有影响
498
+ */
499
+ rowClassName?: (row: DT, i: number) => string | undefined;
500
+ /**
501
+ * 列宽是否可拖动(需要设置v-model:columns)<br>
502
+ * **不要设置**列minWidth,**必须**设置width<br>
503
+ * 列宽拖动时,每一列都必须要有width,且minWidth/maxWidth不生效。table width会变为"fit-content"。
504
+ * - 会自动更新props.columns中的with属性
505
+ */
506
+ colResizable?: boolean | ColResizableConfig<DT>;
507
+ /** 可拖动至最小的列宽 */
508
+ colMinWidth?: number;
509
+ /**
510
+ * 单元格分割线。
511
+ * 默认横竖都有
512
+ * "h" - 仅展示横线
513
+ * "v" - 仅展示竖线
514
+ * "body-v" - 仅表体展示竖线
515
+ */
516
+ bordered?: boolean | 'h' | 'v' | 'body-v' | 'body-h';
517
+ /**
518
+ * 自动重新计算虚拟滚动高度宽度。默认true
519
+ * [非响应式]
520
+ * 传入方法表示resize后的回调
521
+ */
522
+ autoResize?: boolean | (() => void);
523
+ /** 是否展示固定列阴影。为节省性能,默认false。 */
524
+ fixedColShadow?: boolean;
525
+ /** 优化vue2 滚动 */
526
+ optimizeVue2Scroll?: boolean;
527
+ /** 排序配置 */
528
+ sortConfig?: SortConfig<DT>;
529
+ /** 隐藏头部title。可传入colKey数组 */
530
+ hideHeaderTitle?: boolean | string[];
531
+ /** 高亮配置 */
532
+ highlightConfig?: HighlightConfig;
533
+ /** 序号列配置 */
534
+ seqConfig?: SeqConfig;
535
+ /** 展开行配置 */
536
+ expandConfig?: ExpandConfig;
537
+ /** 行拖动配置 */
538
+ dragRowConfig?: DragRowConfig;
539
+ /** 树形配置 */
540
+ treeConfig?: TreeConfig;
541
+ /**
542
+ * 固定头,固定列实现方式。(非响应式)
543
+ *
544
+ * relative:固定列只会放在props.columns的两侧。
545
+ * - 如果列宽会变动则谨慎使用。
546
+ * - 多级表头固定列慎用
547
+ *
548
+ * 低版本浏览器强制为'relative',
549
+ */
550
+ cellFixedMode?: 'sticky' | 'relative';
551
+ /**
552
+ * 是否平滑滚动。default: chrome < 85 ? true : false
553
+ * - false: 使用 onwheel 滚动。为了防止滚动过快导致白屏。
554
+ * - true: 不使用 onwheel 滚动。鼠标滚轮滚动时更加平滑。滚动过快时会白屏。
555
+ */
556
+ smoothScroll?: boolean;
557
+ /**
558
+ * 按整数行纵向滚动
559
+ * - scrollbar:仅拖动滚动条生效
560
+ */
561
+ scrollRowByRow?: boolean | 'scrollbar';
562
+ /**
563
+ * 自定义滚动条配置
564
+ * - false: 禁用自定义滚动条
565
+ * - true: 启用默认配置的自定义滚动条
566
+ * - ScrollbarOptions: 启用并配置自定义滚动条
567
+ */
568
+ scrollbar?: boolean | ScrollbarOptions;
569
+ /**
570
+ * 实验性功能配置
571
+ */
572
+ experimental?: ExperimentalConfig;
573
+ /** 表格底部合计行数据 */
574
+ footerData?: DT[];
575
+ /** 表格底部配置 */
576
+ footerConfig?: FooterConfig;
577
+ }>(),
578
+ {
579
+ width: '',
580
+ fixedMode: false,
581
+ stripe: false,
582
+ minWidth: '',
583
+ maxWidth: '',
584
+ headless: false,
585
+ theme: 'light',
586
+ rowHeight: DEFAULT_ROW_HEIGHT,
587
+ autoRowHeight: () => false,
588
+ footerData: () => [],
589
+ rowHover: true,
590
+ rowActive: () => DEFAULT_ROW_ACTIVE_CONFIG,
591
+ rowCurrentRevokable: true,
592
+ headerRowHeight: DEFAULT_ROW_HEIGHT,
593
+ footerRowHeight: DEFAULT_ROW_HEIGHT,
594
+ virtual: false,
595
+ virtualX: false,
596
+ columns: () => [],
597
+ dataSource: () => [],
598
+ rowKey: '',
599
+ colKey: void 0,
600
+ emptyCellText: '--',
601
+ noDataFull: false,
602
+ showNoData: true,
603
+ sortRemote: false,
604
+ showHeaderOverflow: false,
605
+ showOverflow: false,
606
+ showTrHoverClass: false,
607
+ cellHover: false,
608
+ cellActive: false,
609
+ selectedCellRevokable: true,
610
+ areaSelection: false,
611
+ headerDrag: () => false,
612
+ rowClassName: () => '',
613
+ colResizable: () => false,
614
+ colMinWidth: 10,
615
+ bordered: true,
616
+ autoResize: true,
617
+ fixedColShadow: false,
618
+ optimizeVue2Scroll: false,
619
+ sortConfig: () => DEFAULT_SORT_CONFIG,
620
+ hideHeaderTitle: false,
621
+ highlightConfig: () => ({}),
622
+ seqConfig: () => ({}),
623
+ expandConfig: () => ({}),
624
+ dragRowConfig: () => ({}),
625
+ treeConfig: () => ({}),
626
+ cellFixedMode: 'sticky',
627
+ smoothScroll: DEFAULT_SMOOTH_SCROLL,
628
+ scrollRowByRow: false,
629
+ scrollbar: false,
630
+ experimental: () => ({}),
631
+ footerConfig: () => ({ position: 'bottom' }),
632
+ },
633
+ );
634
+
635
+ provide('stkTheme', toRef(props, 'theme'));
636
+
637
+ const emits = defineEmits<{
638
+ /**
639
+ * 排序变更触发。defaultSort.dataIndex 找不到时,col 将返回null。
640
+ *
641
+ * ```(col: StkTableColumn<DT> | null, order: Order, data: DT[], sortConfig: SortConfig<DT>)```
642
+ */
643
+ (e: 'sort-change', col: StkTableColumn<DT> | null, order: Order, data: DT[], sortConfig: SortConfig<DT>): void;
644
+ /**
645
+ * 一行点击事件
646
+ *
647
+ * ```(ev: MouseEvent, row: DT, data: { rowIndex: number })```
648
+ */
649
+ (e: 'row-click', ev: MouseEvent, row: DT, data: { rowIndex: number }): void;
650
+ /**
651
+ * 选中一行触发。ev返回null表示不是点击事件触发的
652
+ *
653
+ * ```(ev: MouseEvent | null, row: DT | undefined, data: { select: boolean} })```
654
+ */
655
+ (e: 'current-change', ev: MouseEvent | null, row: DT | undefined, data: { select: boolean }): void;
656
+ /**
657
+ * 选中单元格触发。ev返回null表示不是点击事件触发的
658
+ *
659
+ * ```(ev: MouseEvent | null, data: { select: boolean; row: DT | undefined; col: StkTableColumn<DT> | null })```
660
+ */
661
+ (e: 'cell-selected', ev: MouseEvent | null, data: { select: boolean; row: DT | undefined; col: StkTableColumn<DT> | undefined }): void;
662
+ /**
663
+ * 行双击事件
664
+ *
665
+ * ```(ev: MouseEvent, row: DT, data: { rowIndex: number })```
666
+ */
667
+ (e: 'row-dblclick', ev: MouseEvent, row: DT, data: { rowIndex: number }): void;
668
+ /**
669
+ * 表头右键事件
670
+ *
671
+ * ```(ev: MouseEvent)```
672
+ */
673
+ (e: 'header-row-menu', ev: MouseEvent): void;
674
+ /**
675
+ * 表体行右键点击事件
676
+ *
677
+ * ```(ev: MouseEvent, row: DT, data: { rowIndex: number })```
678
+ */
679
+ (e: 'row-menu', ev: MouseEvent, row: DT, data: { rowIndex: number }): void;
680
+ /**
681
+ * 单元格点击事件
682
+ *
683
+ * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>, data: { rowIndex: number })```
684
+ */
685
+ (e: 'cell-click', ev: MouseEvent, row: DT, col: StkTableColumn<DT>, data: { rowIndex: number }): void;
686
+ /**
687
+ * 单元格鼠标进入事件
688
+ *
689
+ * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>)```
690
+ */
691
+ (e: 'cell-mouseenter', ev: MouseEvent, row: DT, col: StkTableColumn<DT>): void;
692
+ /**
693
+ * 单元格鼠标移出事件
694
+ *
695
+ * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>)```
696
+ */
697
+ (e: 'cell-mouseleave', ev: MouseEvent, row: DT, col: StkTableColumn<DT>): void;
698
+ /**
699
+ * 单元格悬浮事件
700
+ *
701
+ * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>)```
702
+ */
703
+ (e: 'cell-mouseover', ev: MouseEvent, row: DT, col: StkTableColumn<DT>): void;
704
+ /**
705
+ * 单元格鼠标按下事件
706
+ *
707
+ * ```(ev: MouseEvent, row: DT, col: StkTableColumn<DT>, data: { rowIndex: number })```
708
+ */
709
+ (e: 'cell-mousedown', ev: MouseEvent, row: DT, col: StkTableColumn<DT>, data: { rowIndex: number }): void;
710
+ /**
711
+ * 表头单元格点击事件
712
+ *
713
+ * ```(ev: MouseEvent, col: StkTableColumn<DT>)```
714
+ */
715
+ (e: 'header-cell-click', ev: MouseEvent, col: StkTableColumn<DT>): void;
716
+ /**
717
+ * 表格滚动事件
718
+ *
719
+ * ```(ev: Event, data: { startIndex: number; endIndex: number })```
720
+ */
721
+ (e: 'scroll', ev: Event, data: { startIndex: number; endIndex: number }): void;
722
+ /**
723
+ * 表格横向滚动事件
724
+ *
725
+ * ```(ev: Event)```
726
+ */
727
+ (e: 'scroll-x', ev: Event): void;
728
+ /**
729
+ * 表头列拖动事件
730
+ *
731
+ * ```(dragStartKey: string, targetColKey: string)```
732
+ */
733
+ (e: 'col-order-change', dragStartKey: string, targetColKey: string): void;
734
+ /**
735
+ * 表头列拖动开始
736
+ *
737
+ * ```(dragStartKey: string)```
738
+ */
739
+ (e: 'th-drag-start', dragStartKey: string): void;
740
+ /**
741
+ * 表头列拖动drop
742
+ *
743
+ * ```(targetColKey: string)```
744
+ */
745
+ (e: 'th-drop', targetColKey: string): void;
746
+ /**
747
+ * 行拖动事件
748
+ *
749
+ * ```(dragStartKey: string, targetRowKey: string)```
750
+ */
751
+ (e: 'row-order-change', dragStartKey: string, targetRowKey: string): void;
752
+ /**
753
+ * 列宽变动时触发
754
+ *
755
+ * ```(col: StkTableColumn<DT>)```
756
+ */
757
+ (e: 'col-resize', col: StkTableColumn<DT>): void;
758
+ /**
759
+ * 展开行触发
760
+ *
761
+ * ```( data: { expanded: boolean; row: DT; col: StkTableColumn<DT> })```
762
+ */
763
+ (e: 'toggle-row-expand', data: { expanded: boolean; row: DT; col: StkTableColumn<DT> | null }): void;
764
+ /**
765
+ * 点击展开树行触发
766
+ *
767
+ * ```( data: { expanded: boolean; row: DT; col: StkTableColumn<DT> })```
768
+ */
769
+ (e: 'toggle-tree-expand', data: { expanded: boolean; row: DT; col: StkTableColumn<DT> | null }): void;
770
+ /**
771
+ * 单元格选区变更事件
772
+ *
773
+ * ```(ranges: AreaSelectionRange[])```
774
+ */
775
+ (e: 'area-selection-change', ranges: AreaSelectionRange[]): void;
776
+ /**
777
+ * 筛选变更触发(Beta)
778
+ *
779
+ * ```(status: Record<UniqKey, FilterStatus>)```
780
+ */
781
+ (e: 'filter-change', status: Record<UniqKey, FilterStatus>): void;
782
+ /**
783
+ * v-model:columns col resize 时更新宽度
784
+ */
785
+ (e: 'update:columns', cols: StkTableColumn<DT>[]): void;
786
+ }>();
787
+
788
+ // 仅支持vue3.3+
789
+ // const slots = defineSlots<{
790
+ // /** 表头插槽 */
791
+ // tableHeader(props: { col: StkTableColumn<DT> }): void;
792
+ // /** 空状态插槽 */
793
+ // empty(): void;
794
+ // }>();
795
+
796
+ const tableContainerRef = ref<HTMLDivElement>();
797
+ const colResizeIndicatorRef = ref<HTMLDivElement>();
798
+ const trRef = ref<HTMLTableRowElement[]>();
799
+
800
+ /** 是否使用 relative 固定头和列 */
801
+ const isRelativeMode = ref(IS_LEGACY_MODE ? true : props.cellFixedMode === 'relative');
802
+
803
+ /** 表格底部是否吸附在顶部 */
804
+ const isFooterTop = computed(() => props.footerConfig?.position === 'top');
805
+
806
+ /** 表格底部标签名:顶部吸附用 tbody,底部吸附用 tfoot */
807
+ const footerTagName = computed(() => (isFooterTop.value ? 'tbody' : 'tfoot'));
808
+
809
+ /**
810
+ * 当前选中的一行
811
+ * - shallowRef: 使 currentRow.value === row 地址相同。防止rowKeyGen 的WeakMap key不一致。
812
+ */
813
+ const currentRow = shallowRef<DT>();
814
+ /**
815
+ * 保存当前选中行的key<br>
816
+ * 原因:vue3 不用ref包dataSource时,row为原始对象,与currentItem(Ref)相比会不相等。
817
+ */
818
+ const currentRowKey = ref<UniqKey | undefined>();
819
+ /** 当前选中的单元格key */
820
+ const currentSelectedCellKey = ref<string | undefined>();
821
+ /** 当前hover行 */
822
+ let currentHoverRow: DT | null = null;
823
+ /** 当前hover的行的key */
824
+ const currentHoverRowKey = ref<UniqKey | null>(null);
825
+ /** 当前hover的列的key */
826
+ // const currentColHoverKey = ref(null);
827
+
828
+ const [tableHeaders, tableHeadersForCalc, dealColumns] = useTableColumns<DT>(props.virtualX, isRelativeMode);
829
+
830
+ const filterStatus = ref<Record<UniqKey, FilterStatus>>({});
831
+
832
+ /** 最后一行的tableHeaders.内容是 props.columns 的引用集合 */
833
+ const tableHeaderLast = computed(() => tableHeadersForCalc.value.slice(-1)[0] || []);
834
+
835
+ const isTreeData = computed(() => {
836
+ return props.columns.some(col => col.type === 'tree-node');
837
+ });
838
+
839
+ const rowActiveProp = computed<Required<RowActiveOption<DT>>>(() => {
840
+ const { rowActive } = props;
841
+ if (typeof rowActive === 'boolean') {
842
+ return {
843
+ ...DEFAULT_ROW_ACTIVE_CONFIG,
844
+ enabled: rowActive ?? true,
845
+ revokable: Boolean(props.rowCurrentRevokable),
846
+ };
847
+ } else {
848
+ return { ...DEFAULT_ROW_ACTIVE_CONFIG, ...rowActive };
849
+ }
850
+ });
851
+
852
+ const dataSourceCopy = shallowRef<DT[]>([]);
853
+
854
+ const rowKeyGenComputed = computed(() => {
855
+ const { rowKey } = props;
856
+ if (typeof rowKey === 'function') {
857
+ return (row: DT) => (rowKey as (row: DT) => string)(row);
858
+ } else {
859
+ return (row: DT) => row[rowKey];
860
+ }
861
+ });
862
+
863
+ const colKeyGen = computed<(col: StkTableColumn<DT>) => string>(() => {
864
+ const { colKey } = props;
865
+ if (colKey === void 0) {
866
+ return col => col.key || col.dataIndex;
867
+ } else if (typeof colKey === 'function') {
868
+ return col => (colKey as (col: StkTableColumn<DT>) => string)(col);
869
+ } else {
870
+ return col => (col as any)[colKey];
871
+ }
872
+ });
873
+
874
+ const getEmptyCellText = computed(() => {
875
+ const { emptyCellText } = props;
876
+ if (typeof emptyCellText === 'string') {
877
+ return () => emptyCellText;
878
+ } else {
879
+ return (col: StkTableColumn<DT>, row: DT) => emptyCellText({ row, col });
880
+ }
881
+ });
882
+
883
+ /** scroll-row-by-row total-height */
884
+ const SRBRTotalHeight = computed(() => {
885
+ if (!isSRBRActive.value || !props.virtual) return 0;
886
+ return (
887
+ dataSourceCopy.value.length * virtualScroll.value.rowHeight + tableHeaderHeight.value //+
888
+ );
889
+ });
890
+ const SRBRBottomHeight = computed(() => {
891
+ if (!isSRBRActive.value || !props.virtual) return 0;
892
+ const { containerHeight, rowHeight } = virtualScroll.value;
893
+ return (containerHeight - tableHeaderHeight.value) % rowHeight;
894
+ });
895
+
896
+ const scrollbarOptions = computed(() => ({
897
+ enabled: true,
898
+ minHeight: 20,
899
+ minWidth: 20,
900
+ width: 8,
901
+ height: 8,
902
+ ...(typeof props.scrollbar === 'boolean' ? { enabled: props.scrollbar } : props.scrollbar),
903
+ }));
904
+
905
+ const isExperimentalScrollY = computed(() => {
906
+ if (scrollbarOptions.value?.enabled && props.scrollRowByRow) {
907
+ return true;
908
+ }
909
+ return props.experimental?.scrollY;
910
+ });
911
+
912
+ const rowKeyGenCache = new WeakMap();
913
+
914
+ // 使用 useSorter hook 管理排序逻辑
915
+ const [sortStates, sortCol, onColumnSort, setSorter, resetSorter, getSortColumns, dealDefaultSorter, getColumnSortState, sortData] = useSorter<DT>(
916
+ props,
917
+ emits,
918
+ colKeyGen,
919
+ tableHeaderLast,
920
+ dataSourceCopy,
921
+ initDataSource,
922
+ onDataSourceChange,
923
+ );
924
+
925
+ const [isSRBRActive] = useScrollRowByRow(props, tableContainerRef);
926
+
927
+ const [onThDragStart, onThDragOver, onThDrop, isHeaderDraggable] = useThDrag(props, emits, colKeyGen);
928
+
929
+ const [onTrDragStart, onTrDragEnter, onTrDragOver, onTrDrop, onTrDragEnd] = useTrDrag(props, emits, dataSourceCopy);
930
+
931
+ const [maxRowSpan, updateMaxRowSpan, getMaxRowSpanValue] = useMaxRowSpan(props, tableHeaderLast, rowKeyGen, dataSourceCopy);
932
+
933
+ /**
934
+ * mergeCells 结果共享缓存:useVirtualScroll(可视列范围修正)与 useMergeCells
935
+ * (hidden map 构建 / 视口上方占位 / 渲染)共用,同一单元格只调用一次用户回调,
936
+ * 且可跨滚动帧复用(数据/列变化时才清空)。
937
+ */
938
+ const mergeCellsCache = createMergeCellsCache();
939
+
940
+ const [
941
+ virtualScroll,
942
+ virtualScrollX,
943
+ virtual_on,
944
+ virtual_dataSourcePart,
945
+ virtual_offsetBottom,
946
+ virtualX_on,
947
+ virtualX_offsetRight,
948
+ tableHeaderHeight,
949
+ initVirtualScroll,
950
+ initVirtualScrollY,
951
+ initVirtualScrollX,
952
+ updateVirtualScrollY,
953
+ updateVirtualScrollX,
954
+ setAutoHeight,
955
+ clearAllAutoHeight,
956
+ getRowsHeight,
957
+ clearColWidthCache,
958
+ virtualX_tableHeaders,
959
+ expandRowColspan,
960
+ theadVirtualX,
961
+ virtualX_columnPart,
962
+ virtualX_expandColSegments,
963
+ // 内部诊断用(测试通过 setupState 访问),不加入 defineExpose
964
+ getRowHeightCacheInfo,
965
+ ] = useVirtualScroll(
966
+ props,
967
+ tableContainerRef,
968
+ trRef,
969
+ dataSourceCopy,
970
+ tableHeaderLast,
971
+ tableHeaders,
972
+ rowKeyGen,
973
+ maxRowSpan,
974
+ getMaxRowSpanValue,
975
+ scrollbarOptions,
976
+ isExperimentalScrollY,
977
+ isRelativeMode,
978
+ mergeCellsCache,
979
+ );
980
+
981
+ /** requestAnimationFrame throttled version of updateVirtualScrollY for smoother wheel scrolling */
982
+ const rafUpdateVirtualScrollYForWheel = rafThrottle(updateVirtualScrollY);
983
+
984
+ const [scrollbar, showScrollbar, onVerticalScrollbarMouseDown, onHorizontalScrollbarMouseDown, updateCustomScrollbar] = useScrollbar(
985
+ props,
986
+ tableContainerRef,
987
+ virtualScroll,
988
+ virtualScrollX,
989
+ updateVirtualScrollY,
990
+ scrollbarOptions,
991
+ isExperimentalScrollY,
992
+ );
993
+
994
+ /**
995
+ * 是否允许把虚拟窗口内的连续空行合并为占位 tr。
996
+ * autoRowHeight 时容器不定义 --row-height 且行高不均匀,不能合并。
997
+ */
998
+ const canMergeEmptyRows = computed(() => virtual_on.value && !props.autoRowHeight);
999
+
1000
+ /** 视口上方行数(rowspan/stripe 修正区域)。不允许合并时为 0(上方行走常规行渲染) */
1001
+ const aboveViewportRowCount = computed(() => {
1002
+ if (!canMergeEmptyRows.value) return 0;
1003
+ const part = virtual_dataSourcePart.value;
1004
+ const { startIndex, viewportStartIndex } = virtualScroll.value;
1005
+ return Math.min(part.length, Math.max(0, viewportStartIndex - startIndex));
1006
+ });
1007
+
1008
+ /**
1009
+ * 视口下方可合并的占位行数。
1010
+ * rowspan 修正会使 endIndex 超出 viewportEndIndex,二者之间的行无可见内容
1011
+ * (跨视口的合并单元格由视口内锚点覆盖),此前逐行渲染为空 tr。
1012
+ * 这里将其合并为占位 tr(按跨界 rowspan 结束行切段,见 belowPhSegments),
1013
+ * 用 `height: calc(var(--row-height) * N)` 保持总高度,
1014
+ * 大 rowspan 场景下可显著减少 DOM 节点数。
1015
+ * 行高不均匀时(autoRowHeight / 该区域存在展开行)不能按统一行高合并,返回 0。
1016
+ */
1017
+ const belowViewportRowCount = computed(() => {
1018
+ if (!canMergeEmptyRows.value) return 0;
1019
+ const part = virtual_dataSourcePart.value;
1020
+ const { startIndex, viewportEndIndex } = virtualScroll.value;
1021
+ // 视口行数(含 rowspan/stripe 修正的越界部分),钳制到实际数据长度
1022
+ const viewportCount = Math.min(part.length, Math.max(0, viewportEndIndex - startIndex + 1));
1023
+ const count = part.length - viewportCount;
1024
+ if (count <= 0) return 0;
1025
+ // 展开行有独立行高(--row-height 被行内覆写),不能参与统一行高合并
1026
+ for (let i = viewportCount; i < part.length; i++) {
1027
+ if (part[i]?.__EXP_R__) return 0;
1028
+ }
1029
+ return count;
1030
+ });
1031
+
1032
+ const [
1033
+ hiddenCellMap,
1034
+ mergeCellsWrapper,
1035
+ hoverMergedCells,
1036
+ updateHoverMergedCells,
1037
+ activeMergedCells,
1038
+ updateActiveMergedCells,
1039
+ aboveViewportColumnMap,
1040
+ aboveEmptyBlocks,
1041
+ belowPhSegments,
1042
+ ] = useMergeCells(
1043
+ rowActiveProp,
1044
+ tableHeaderLast,
1045
+ rowKeyGen,
1046
+ colKeyGen,
1047
+ virtual_dataSourcePart,
1048
+ virtualScroll,
1049
+ virtualX_columnPart,
1050
+ dataSourceCopy,
1051
+ mergeCellsCache,
1052
+ canMergeEmptyRows,
1053
+ belowViewportRowCount,
1054
+ );
1055
+
1056
+ const getFixedColPosition = useGetFixedColPosition(tableHeadersForCalc, colKeyGen);
1057
+
1058
+ const getFixedStyle = useFixedStyle<DT>(props, isRelativeMode, getFixedColPosition, virtualScroll, virtualScrollX, virtualX_on, virtualX_offsetRight);
1059
+
1060
+ const [highlightSteps, setHighlightDimRow, setHighlightDimCell] = useHighlight(props, stkTableId, tableContainerRef);
1061
+
1062
+ function getRowIndex(row: DT): number {
1063
+ const targetKey = rowKeyGen(row);
1064
+ return dataSourceCopy.value.findIndex(item => rowKeyGen(item) === targetKey);
1065
+ }
1066
+
1067
+ function getColumnIndex(column: PrivateStkTableColumn<DT>): number {
1068
+ const targetKey = colKeyGen.value(column);
1069
+ return tableHeaderLast.value.findIndex(item => colKeyGen.value(item) === targetKey);
1070
+ }
1071
+
1072
+ const {
1073
+ config: areaSelectionConfig,
1074
+ isSelecting: isAreaSelecting,
1075
+ onMD: onSelectionMouseDown,
1076
+ // getClass: getAreaSelectionClasses,
1077
+ // getRowClass: getAreaSelectionRowClass,
1078
+ get: getSelectedArea,
1079
+ set: setAreaSelection,
1080
+ clear: clearSelectedArea,
1081
+ copy: copySelectedArea,
1082
+ } = ON_DEMAND_FEATURE[useAreaSelectionName](
1083
+ props,
1084
+ emits,
1085
+ tableContainerRef,
1086
+ dataSourceCopy,
1087
+ tableHeaderLast,
1088
+ colKeyGen,
1089
+ cellKeyGen,
1090
+ scrollTo,
1091
+ virtualScroll,
1092
+ virtualScrollX,
1093
+ getRowIndex,
1094
+ getColumnIndex,
1095
+ );
1096
+
1097
+ /** 键盘箭头滚动 */
1098
+ useKeyboardArrowScroll(tableContainerRef, props, scrollTo, virtualScroll, virtualScrollX, tableHeaders, virtual_on, areaSelectionConfig);
1099
+
1100
+ /** 固定列处理 */
1101
+ const [fixedCols, fixedColClassMap, updateFixedShadow] = useFixedCol(
1102
+ props,
1103
+ colKeyGen,
1104
+ getFixedColPosition,
1105
+ tableHeaders,
1106
+ tableHeadersForCalc,
1107
+ tableContainerRef,
1108
+ );
1109
+
1110
+ if (props.autoResize) {
1111
+ useAutoResize(
1112
+ tableContainerRef,
1113
+ () => {
1114
+ initVirtualScroll();
1115
+ // 容器宽度变化后,需重新计算固定列状态
1116
+ updateFixedShadow();
1117
+ // initVirtualScroll 只是把新的 containerWidth/Height 写回 store,
1118
+ // 自定义滚动条 thumb 长度依赖 store 值,须在其后重算,否则 resize 后横向滚动条长度不更新
1119
+ updateCustomScrollbar();
1120
+ },
1121
+ props,
1122
+ 200,
1123
+ );
1124
+ }
1125
+
1126
+ const [colResizeOn, isColResizing, onThResizeMouseDown] = useColResize(
1127
+ props,
1128
+ emits,
1129
+ tableContainerRef,
1130
+ tableHeaderLast,
1131
+ colResizeIndicatorRef,
1132
+ colKeyGen,
1133
+ fixedCols,
1134
+ clearColWidthCache,
1135
+ );
1136
+
1137
+ const [toggleExpandRow, setRowExpand] = useRowExpand(emits, dataSourceCopy, rowKeyGen, onDataSourceChange);
1138
+
1139
+ const [toggleTreeNode, setTreeExpand, flatTreeData] = useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange);
1140
+
1141
+ /** style cache */
1142
+ const paddingTopStyle = computed(() => `height:${virtualScroll.value.offsetTop}px`);
1143
+ const offsetBottomStyle = computed(() => `height:${virtual_offsetBottom.value}px`);
1144
+ const SRBRBottomStyle = computed(() => `height:${SRBRBottomHeight.value}px`);
1145
+
1146
+ /** tbody 渲染项:数据行 | 视口上方合并占位段 | 视口下方合并占位段 */
1147
+ type BodyRenderItem =
1148
+ | { type: 'row'; row: DT; rowIndex: number; key: UniqKey }
1149
+ | { type: 'above-ph'; count: number; key: string }
1150
+ | { type: 'below-ph'; count: number; key: string };
1151
+
1152
+ /**
1153
+ * 视口上方区域(startIndex..viewportStartIndex-1)的有序渲染段。
1154
+ * 含 must-render 单元格(rowspan 跨入视口)的锚点行保留独立 tr;
1155
+ * 连续「无 td」的空行(aboveEmptyBlocks,>= 2 行)合并为单个占位段,
1156
+ * 以 height: calc(var(--row-height) * N) 保持总高度。
1157
+ *
1158
+ * 注意:占位段在 DOM 中只算 1 行,跨越它的 rowspan 属性已在
1159
+ * mergeCellsWrapper 中按合并行数修正,保证单元格不会多跨行。
1160
+ * 不允许合并时返回空数组。
1161
+ */
1162
+ const aboveRenderParts = computed<BodyRenderItem[]>(() => {
1163
+ const aboveCount = aboveViewportRowCount.value;
1164
+ if (aboveCount <= 0) return [];
1165
+ const part = virtual_dataSourcePart.value;
1166
+ const blocks = aboveEmptyBlocks.value;
1167
+ const { startIndex } = virtualScroll.value;
1168
+
1169
+ const items: BodyRenderItem[] = [];
1170
+ let i = 0;
1171
+ let blockIdx = 0;
1172
+ while (i < aboveCount) {
1173
+ const block = blocks[blockIdx];
1174
+ if (block && block.start === startIndex + i) {
1175
+ items.push({ type: 'above-ph', count: block.count, key: `vt-above-ph-${i}` });
1176
+ i += block.count;
1177
+ blockIdx++;
1178
+ } else {
1179
+ const row = part[i];
1180
+ items.push({ type: 'row', row, rowIndex: i, key: rowKeyGen(row) });
1181
+ i++;
1182
+ }
1183
+ }
1184
+ return items;
1185
+ });
1186
+
1187
+ /**
1188
+ * tbody 渲染列表(异构):视口上方段 + 视口行 + 视口下方占位段。
1189
+ * 不允许合并时退化为全量数据行列表。
1190
+ */
1191
+ const bodyRenderItems = computed<BodyRenderItem[]>(() => {
1192
+ const part = virtual_dataSourcePart.value;
1193
+ const items = aboveRenderParts.value.slice();
1194
+ const rowEnd = part.length - belowViewportRowCount.value;
1195
+ for (let i = aboveViewportRowCount.value; i < rowEnd; i++) {
1196
+ const row = part[i];
1197
+ items.push({ type: 'row', row, rowIndex: i, key: rowKeyGen(row) });
1198
+ }
1199
+ // 下方占位按跨界 rowspan 结束行分段输出,保证不同结束行的单元格止于不同 tr
1200
+ const belowSegs = belowPhSegments.value;
1201
+ for (let i = 0; i < belowSegs.length; i++) {
1202
+ items.push({ type: 'below-ph', count: belowSegs[i].count, key: `vt-below-ph-${i}` });
1203
+ }
1204
+ return items;
1205
+ });
1206
+
1207
+ watch(
1208
+ () => props.columns,
1209
+ () => {
1210
+ handleDealColumns();
1211
+ updateMaxRowSpan();
1212
+ // nextTick: initVirtualScrollX need get container width。
1213
+ nextTick(() => {
1214
+ initVirtualScrollX();
1215
+ updateFixedShadow();
1216
+ updateCustomScrollbar();
1217
+ });
1218
+ },
1219
+ );
1220
+ watch(
1221
+ () => props.virtual,
1222
+ () => {
1223
+ nextTick(initVirtualScrollY);
1224
+ },
1225
+ );
1226
+
1227
+ /**
1228
+ * 行高类配置变化后重算虚拟滚动几何。
1229
+ * 合并为一个 watcher:同一 tick 内多项同时变化只重算一次。
1230
+ * 必须无参调用 initVirtualScrollY(其首参是虚拟滚动容器高度,不能把行高值传入),
1231
+ * 且需 nextTick 等新的 --row-height / 表头行高等样式应用到 DOM 后再测量。
1232
+ *
1233
+ * 监听值拼接为单个原始值(字符串)而非返回数组:getter 返回数组时每次求值都是新数组,
1234
+ * 身份比较必然变化,父组件传入内联对象字面量(如 :expand-config="{ height: 40 }",官方示例即如此写法)
1235
+ * 会让本 watcher 在父组件每次重渲染时误触发;展开行/变高大表下误触发一次会连带行高树 O(n) 重建
1236
+ * (实测 50K 行每次父重渲染多付 ~10ms)。拼接为原始值后仅在值真正变化时触发,且兼容 Vue 2.7。
1237
+ */
1238
+ watch(
1239
+ () => `${props.rowHeight}|${props.headerRowHeight}|${props.footerRowHeight}|${props.expandConfig?.height}`,
1240
+ () => nextTick(initVirtualScrollY),
1241
+ );
1242
+
1243
+ watch(
1244
+ () => props.virtualX,
1245
+ () => {
1246
+ handleDealColumns();
1247
+ // initVirtualScrollX 需要获取容器滚动宽度等。必须等渲染完成后再调用。因此使用nextTick。
1248
+ nextTick(() => {
1249
+ initVirtualScrollX();
1250
+ updateFixedShadow();
1251
+ });
1252
+ },
1253
+ );
1254
+
1255
+ watch(
1256
+ () => props.dataSource,
1257
+ val => {
1258
+ updateDataSource(val);
1259
+ },
1260
+ );
1261
+
1262
+ watch(
1263
+ () => props.fixedColShadow,
1264
+ () => updateFixedShadow(),
1265
+ );
1266
+
1267
+ handleDealColumns();
1268
+ initDataSource();
1269
+ updateMaxRowSpan();
1270
+
1271
+ onMounted(() => {
1272
+ initVirtualScroll();
1273
+ updateFixedShadow();
1274
+ dealDefaultSorter();
1275
+ });
1276
+
1277
+ async function onDataSourceChange() {
1278
+ // 数据顺序/行数变化后重算 rowspan 映射(排序后 mergeCells 的 rowIndex 会变化)
1279
+ updateMaxRowSpan();
1280
+ await nextTick();
1281
+ initVirtualScrollY();
1282
+ updateCustomScrollbar();
1283
+ }
1284
+
1285
+ function initDataSource(v = props.dataSource, option?: { forceSort?: boolean }) {
1286
+ let dataSourceTemp = v.slice(); // shallow copy
1287
+
1288
+ // 排序(tableSort 内部会根据 sortChildren 自动处理树形递归排序)
1289
+ if (!props.sortRemote || option?.forceSort) {
1290
+ dataSourceTemp = sortData(dataSourceTemp);
1291
+ }
1292
+
1293
+ if (isTreeData.value) {
1294
+ dataSourceTemp = flatTreeData(dataSourceTemp);
1295
+ }
1296
+ dataSourceTemp = filterDataSource(dataSourceTemp);
1297
+ dataSourceCopy.value = dataSourceTemp;
1298
+ }
1299
+
1300
+ function setFilter(
1301
+ status: Record<UniqKey, FilterStatus> | null,
1302
+ option?: {
1303
+ remote?: boolean;
1304
+ silent?: boolean;
1305
+ },
1306
+ ) {
1307
+ status = status || {};
1308
+ filterStatus.value = status;
1309
+ if (!option?.remote) {
1310
+ initDataSource();
1311
+ // #80:筛选改变 dataSourceCopy 后重算虚拟滚动窗口,
1312
+ // 否则残留 startIndex/endIndex/scrollHeight 会导致只渲染 1 行或空白视口
1313
+ onDataSourceChange();
1314
+ }
1315
+ if (!option?.silent) {
1316
+ emits('filter-change', status);
1317
+ }
1318
+ }
1319
+
1320
+ function filterDataSource(dataSource: DT[]) {
1321
+ const filterKeys = Object.keys(filterStatus.value);
1322
+ if (!filterKeys?.length) return dataSource;
1323
+ let result = dataSource;
1324
+ for (const key of filterKeys) {
1325
+ const { value, filter } = filterStatus.value[key];
1326
+ if (!value?.length) continue;
1327
+ result = result.filter(row => {
1328
+ const cellValue = row[key];
1329
+ if (filter) {
1330
+ return filter({ row, cellValue, filterValues: value });
1331
+ }
1332
+ return value.some(v => cellValue == v);
1333
+ });
1334
+ }
1335
+ return result;
1336
+ }
1337
+
1338
+ /**
1339
+ * Wrapper for dealColumns to pass props.columns
1340
+ */
1341
+ function handleDealColumns() {
1342
+ dealColumns(props.columns);
1343
+ }
1344
+
1345
+ function updateDataSource(val: DT[]) {
1346
+ if (!Array.isArray(val)) {
1347
+ console.warn('invalid dataSource');
1348
+ return;
1349
+ }
1350
+
1351
+ let needInitVirtualScrollY = false;
1352
+ if (/* slots.customBottom || */ dataSourceCopy.value.length !== val.length) {
1353
+ needInitVirtualScrollY = true;
1354
+ }
1355
+ initDataSource(val);
1356
+ updateMaxRowSpan();
1357
+
1358
+ // #47
1359
+ if (!val.length) {
1360
+ clearSelectedArea();
1361
+ }
1362
+
1363
+ // if data length is not change, not init virtual scroll
1364
+ if (needInitVirtualScrollY) {
1365
+ // wait for table render,initVirtualScrollY has get `dom` operation.
1366
+ nextTick(() => initVirtualScrollY());
1367
+ }
1368
+ nextTick(updateCustomScrollbar);
1369
+ }
1370
+
1371
+ /** tr key */
1372
+ function rowKeyGen(row: DT | null | undefined) {
1373
+ if (!row) return row;
1374
+
1375
+ let key = rowKeyGenCache.get(row);
1376
+ if (key !== undefined) return key;
1377
+
1378
+ // Check for cached key in row object
1379
+ const cachedRowKey = (row as PrivateRowDT).__R_K__;
1380
+ if (cachedRowKey !== undefined) {
1381
+ rowKeyGenCache.set(row, cachedRowKey);
1382
+ return cachedRowKey;
1383
+ }
1384
+
1385
+ key = rowKeyGenComputed.value(row);
1386
+
1387
+ if (key === void 0) {
1388
+ // key为undefined时,不应该高亮行。因此重新生成key
1389
+ key = Math.random().toString(36).slice(2);
1390
+ }
1391
+ rowKeyGenCache.set(row, key);
1392
+ return key;
1393
+ }
1394
+
1395
+ /** td key */
1396
+ function cellKeyGen(row: DT | null | undefined, col: StkTableColumn<DT>) {
1397
+ return rowKeyGen(row) + CELL_KEY_SEPARATE + colKeyGen.value(col);
1398
+ }
1399
+
1400
+ const cellStyleMap = computed(() => {
1401
+ const thMap = new Map();
1402
+ const tdMap = new Map();
1403
+ const tfMap = new Map();
1404
+ const { virtualX } = props;
1405
+ const headers = tableHeaders.value;
1406
+ const colKeyGenValue = colKeyGen.value;
1407
+
1408
+ for (let depth = 0, depthLen = headers.length; depth < depthLen; depth++) {
1409
+ const cols = headers[depth];
1410
+ for (let i = 0, colsLen = cols.length; i < colsLen; i++) {
1411
+ const col = cols[i];
1412
+ const width = virtualX ? getCalculatedColWidth(col) + 'px' : transformWidthToStr(col.width);
1413
+ const minWidthStr = transformWidthToStr(col.minWidth);
1414
+ const maxWidthStr = transformWidthToStr(col.maxWidth);
1415
+ // en: Use string instead of object to reduce patchStyle overhead
1416
+ let styleStr = '';
1417
+ if (width) styleStr += `--cw:${width}`;
1418
+ if (minWidthStr) styleStr += `;min-width:${minWidthStr}`;
1419
+ if (maxWidthStr) styleStr += `;max-width:${maxWidthStr}`;
1420
+ const colKey = colKeyGenValue(col);
1421
+ thMap.set(colKey, styleStr + ';' + getFixedStyle(TagType.TH, col, depth));
1422
+ tdMap.set(colKey, styleStr + ';' + getFixedStyle(TagType.TD, col, depth));
1423
+ tfMap.set(colKey, 'position:sticky;' + styleStr + ';' + getFixedStyle(TagType.TF, col, depth));
1424
+ }
1425
+ }
1426
+ return {
1427
+ [TagType.TH]: thMap,
1428
+ [TagType.TD]: tdMap,
1429
+ [TagType.TF]: tfMap,
1430
+ };
1431
+ });
1432
+
1433
+ /**
1434
+ * fixed 模式下 colgroup 中单个 col 的样式。
1435
+ * 仅取叶子列的 width(与 cellStyleMap 中 --cw 保持一致);未设置 width 的列不声明宽度,
1436
+ * 由 table-layout:fixed 将剩余空间平分,符合“一列固定、其余列平分”的预期。
1437
+ */
1438
+ function getColGroupColStyle(col: PrivateStkTableColumn<DT>) {
1439
+ const width = transformWidthToStr(col.width);
1440
+ return width ? `width:${width}` : null;
1441
+ }
1442
+
1443
+ function getAbsoluteRowIndex(rowIndex: number) {
1444
+ return rowIndex + virtualScroll.value.startIndex;
1445
+ }
1446
+
1447
+ function shouldHideCell(row: PrivateRowDT | null | undefined, col: StkTableColumn<PrivateRowDT>): boolean | undefined {
1448
+ if (!hiddenCellMap.value || !row) return;
1449
+ return hiddenCellMap.value[rowKeyGen(row)]?.has(colKeyGen.value(col));
1450
+ }
1451
+
1452
+ /**
1453
+ * Get the column list for a body row.
1454
+ * - Above-viewport rows: returns a modified list with placeholder entries (__VT_PH__) to reduce DOM nodes.
1455
+ * Rows without any must-render cell return [] and consecutive such rows are merged into a single
1456
+ * placeholder tr (see aboveRenderParts).
1457
+ * - Below-viewport rows: normally merged into placeholder tr segments split at crossing rowspan
1458
+ * ends (see belowPhSegments); when still rendered individually (fallback), returns []
1459
+ * (tr already has height via CSS, no td needed).
1460
+ *
1461
+ * 仅在 canMergeEmptyRows(定高虚拟列表)时启用上述剔除:空 tr 依赖 CSS `height: var(--row-height)`
1462
+ * 保持高度,autoRowHeight 模式行高靠 td 内容撑开(容器也不定义 --row-height),
1463
+ * 剔除 td 会让视口上方行(stripe/rowspan 修正保留的行)塌陷为 0 高,且塌陷高度会被
1464
+ * DOM 测量写入行高缓存,导致行高无法正常撑开。
1465
+ */
1466
+ function getBodyColumns(row: PrivateRowDT | null | undefined, rowIndex: number): PrivateStkTableColumn<PrivateRowDT>[] {
1467
+ if (!row) return virtualX_columnPart.value;
1468
+ if (canMergeEmptyRows.value) {
1469
+ const { startIndex, viewportStartIndex, viewportEndIndex } = virtualScroll.value;
1470
+ const aboveCount = viewportStartIndex - startIndex;
1471
+ if (aboveCount > 0 && rowIndex < aboveCount) {
1472
+ return (aboveViewportColumnMap.value.get(rowKeyGen(row)) || virtualX_columnPart.value) as PrivateStkTableColumn<PrivateRowDT>[];
1473
+ }
1474
+ // Below-viewport rows: no td needed
1475
+ if (startIndex + rowIndex > viewportEndIndex) {
1476
+ return [];
1477
+ }
1478
+ }
1479
+ const segments = virtualX_expandColSegments.value;
1480
+ if (!segments) return virtualX_columnPart.value;
1481
+ return buildExpandColumns(row, rowIndex, segments) as PrivateStkTableColumn<PrivateRowDT>[];
1482
+ }
1483
+
1484
+ /**
1485
+ * 超长 colspan 优化:收缩修正扩展区域(virtualX_expandColSegments)内的行渲染列列表。
1486
+ * - 左扩展区:colspan/rowspan 锚点单元格保留真实渲染(否则行列占位断裂);
1487
+ * 被覆盖单元格照旧不渲染(槽位由跨越它的单元格占用);其余连续普通单元格
1488
+ * 合并为单个 colspan 占位 td(fixed 布局下恰好占用相同列槽位,保持对齐);
1489
+ * - 右扩展区:完全不渲染(均在可视视口外,不影响可视单元格对齐)。
1490
+ */
1491
+ function buildExpandColumns(
1492
+ row: PrivateRowDT,
1493
+ rowIndex: number,
1494
+ segments: {
1495
+ prefix: PrivateStkTableColumn<PrivateRowDT>[];
1496
+ leftExpand: PrivateStkTableColumn<PrivateRowDT>[];
1497
+ viewport: PrivateStkTableColumn<PrivateRowDT>[];
1498
+ suffix: PrivateStkTableColumn<PrivateRowDT>[];
1499
+ leftExpandStart: number;
1500
+ },
1501
+ ) {
1502
+ const { prefix, leftExpand, viewport, suffix, leftExpandStart } = segments;
1503
+ const result: (PrivateStkTableColumn<PrivateRowDT> | { __VT_PH__: number })[] = prefix.slice();
1504
+
1505
+ if (leftExpand.length) {
1506
+ const hiddenSet = hiddenCellMap.value ? hiddenCellMap.value[rowKeyGen(row)] : void 0;
1507
+ const colKeyGenValue = colKeyGen.value;
1508
+ const absRowIndex = (virtual_on.value ? virtualScroll.value.startIndex : 0) + rowIndex;
1509
+ let run = 0;
1510
+ for (let i = 0; i < leftExpand.length; i++) {
1511
+ const col = leftExpand[i];
1512
+ // 被覆盖单元格:不渲染 td(槽位由上方/左侧的跨越单元格占用)
1513
+ if (hiddenSet && hiddenSet.has(colKeyGenValue(col))) continue;
1514
+ // 锚点单元格(colspan/rowspan > 1)必须真实渲染,不能占位替代
1515
+ if (col.mergeCells) {
1516
+ const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, col.__LF_S__ ?? leftExpandStart + i);
1517
+ if (colspan > 1 || rowspan > 1) {
1518
+ if (run > 0) {
1519
+ result.push({ __VT_PH__: run } as PrivateStkTableColumn<PrivateRowDT>);
1520
+ run = 0;
1521
+ }
1522
+ result.push(col);
1523
+ continue;
1524
+ }
1525
+ }
1526
+ run++;
1527
+ }
1528
+ if (run > 0) result.push({ __VT_PH__: run } as PrivateStkTableColumn<PrivateRowDT>);
1529
+ }
1530
+
1531
+ // 右扩展区不渲染,直接拼接可视区与右侧固定列
1532
+ result.push(...viewport, ...suffix);
1533
+ return result;
1534
+ }
1535
+ /** th title */
1536
+ function getHeaderTitle(col: StkTableColumn<DT>): string {
1537
+ const colKey = colKeyGen.value(col);
1538
+ // hide title
1539
+ if (props.hideHeaderTitle === true || (Array.isArray(props.hideHeaderTitle) && props.hideHeaderTitle.includes(colKey))) {
1540
+ return '';
1541
+ }
1542
+ return col.title || '';
1543
+ }
1544
+
1545
+ function getTRProps(row: PrivateRowDT | null | undefined, index: number) {
1546
+ const rowIndex = getAbsoluteRowIndex(index);
1547
+ const rowKey = rowKeyGen(row);
1548
+
1549
+ const classList = [props.rowClassName(row, rowIndex), row?.__EXP__ ? 'expanded' : '', row?.__EXP_R__ ? 'expanded-row' : ''];
1550
+ // area selection row highlight
1551
+ // if (areaSelectionConfig.value.enabled) {
1552
+ // classList.push(...getAreaSelectionRowClass(rowIndex));
1553
+ // }
1554
+ if (currentRowKey.value === rowKey || row === currentRow.value) {
1555
+ classList.push('active');
1556
+ }
1557
+ if (props.showTrHoverClass && (rowKey === currentHoverRowKey.value || row === currentHoverRow)) {
1558
+ classList.push('hover');
1559
+ }
1560
+
1561
+ const result: Record<string, number | string | null> = {
1562
+ id: stkTableId + '-' + rowKey,
1563
+ 'data-row-key': rowKey,
1564
+ 'data-row-i': rowIndex,
1565
+ class: classList.filter(Boolean).join(' '),
1566
+ style: null,
1567
+ };
1568
+
1569
+ const needRowHeight = row?.__EXP_R__ && props.virtual && props.expandConfig?.height;
1570
+
1571
+ if (needRowHeight) {
1572
+ result.style = `--row-height: ${props.expandConfig?.height}px`;
1573
+ }
1574
+ return result;
1575
+ }
1576
+
1577
+ function getTHProps(col: PrivateStkTableColumn<DT>) {
1578
+ const colKey = colKeyGen.value(col);
1579
+ const sortState = getColumnSortState(colKey);
1580
+ const isSorted = !!sortState && sortState.order !== null;
1581
+
1582
+ return {
1583
+ 'data-col-key': colKey,
1584
+ draggable: Boolean(isHeaderDraggable(col)),
1585
+ rowspan: col.__R_SP__,
1586
+ colspan: col.__C_SP__,
1587
+ style: cellStyleMap.value[TagType.TH].get(colKey),
1588
+ title: getHeaderTitle(col),
1589
+ // class 用预拼接字符串(而非数组),降低每格 vnode diff 与 GC 开销
1590
+ class: [
1591
+ col.sorter ? 'sortable' : '',
1592
+ isSorted ? 'sorter-' + sortState?.order : '',
1593
+ col.headerClassName,
1594
+ fixedColClassMap.value.get(colKey),
1595
+ col.headerAlign === 'left' ? 'text-l' : col.headerAlign === 'right' ? 'text-r' : col.headerAlign === 'center' ? 'text-c' : '',
1596
+ ]
1597
+ .filter(Boolean)
1598
+ .join(' '),
1599
+ };
1600
+ }
1601
+
1602
+ function getTFProps(col: StkTableColumn<DT>) {
1603
+ const colKey = colKeyGen.value(col);
1604
+ return {
1605
+ 'data-col-key': colKey,
1606
+ style: cellStyleMap.value[TagType.TF].get(colKey),
1607
+ class: [
1608
+ col.className,
1609
+ fixedColClassMap.value.get(colKey),
1610
+ col.type === 'seq' ? 'seq-column' : '',
1611
+ col.align === 'center' ? 'text-c' : col.align === 'right' ? 'text-r' : '',
1612
+ ]
1613
+ .filter(Boolean)
1614
+ .join(' '),
1615
+ };
1616
+ }
1617
+
1618
+ function getTDProps(row: PrivateRowDT | null | undefined, col: StkTableColumn<PrivateRowDT>, rowIndex: number, colIndex: number) {
1619
+ const colKey = colKeyGen.value(col);
1620
+ if (!row) {
1621
+ return {
1622
+ style: cellStyleMap.value[TagType.TD].get(colKey),
1623
+ };
1624
+ }
1625
+
1626
+ const cellKey = cellKeyGen(row, col);
1627
+ const classList = [col.className, fixedColClassMap.value.get(colKey)];
1628
+
1629
+ if (col.align === 'center') {
1630
+ classList.push('text-c');
1631
+ } else if (col.align === 'right') {
1632
+ classList.push('text-r');
1633
+ }
1634
+ if (col.mergeCells) {
1635
+ if (hoverMergedCells.value.has(cellKey)) {
1636
+ classList.push('cell-hover');
1637
+ }
1638
+ if (activeMergedCells.value.has(cellKey)) {
1639
+ classList.push('cell-active');
1640
+ }
1641
+ }
1642
+
1643
+ if (props.cellActive && currentSelectedCellKey.value === cellKey) {
1644
+ classList.push('active');
1645
+ }
1646
+ // // area selection style
1647
+ // if (areaSelectionConfig.value.enabled) {
1648
+ // const absRowIndex = getAbsoluteRowIndex(rowIndex);
1649
+ // classList.push(...getAreaSelectionClasses(cellKey, absRowIndex, colKey));
1650
+ // }
1651
+ if (col.type === 'seq') {
1652
+ classList.push('seq-column');
1653
+ } else if (col.type === 'expand' && (row.__EXP__ ? colKeyGen.value(row.__EXP__) === colKey : false)) {
1654
+ classList.push('expanded');
1655
+ } else if (row.__T_EXP__ && col.type === 'tree-node') {
1656
+ classList.push('tree-expanded');
1657
+ } else if (col.type === 'dragRow') {
1658
+ classList.push('drag-row-cell');
1659
+ }
1660
+
1661
+ return {
1662
+ 'data-col-key': colKey,
1663
+ style: cellStyleMap.value[TagType.TD].get(colKey),
1664
+ class: classList.filter(Boolean).join(' '),
1665
+ ...mergeCellsWrapper(row, col, rowIndex, (col as PrivateStkTableColumn<DT>).__LF_S__ ?? 0),
1666
+ };
1667
+ }
1668
+
1669
+ function onRowClick(e: MouseEvent) {
1670
+ const rowIndex = getClosestTrIndex(e.target as HTMLElement);
1671
+ const row = dataSourceCopy.value[rowIndex];
1672
+ if (!row) return;
1673
+ emits('row-click', e, row, { rowIndex });
1674
+ if (rowActiveProp.value.disabled?.(row)) return;
1675
+ const isCurrentRow = props.rowKey ? currentRowKey.value === rowKeyGen(row) : currentRow.value === row;
1676
+ if (isCurrentRow) {
1677
+ if (!rowActiveProp.value.revokable) {
1678
+ return;
1679
+ }
1680
+ setCurrentRow(void 0, { silent: true });
1681
+ } else {
1682
+ setCurrentRow(row, { silent: true });
1683
+ }
1684
+ emits('current-change', e, row, { select: !isCurrentRow });
1685
+ }
1686
+
1687
+ function onRowDblclick(e: MouseEvent) {
1688
+ const rowIndex = getClosestTrIndex(e.target as HTMLElement);
1689
+ const row = dataSourceCopy.value[rowIndex];
1690
+ if (!row) return;
1691
+ emits('row-dblclick', e, row, { rowIndex });
1692
+ }
1693
+
1694
+ function onHeaderMenu(e: MouseEvent) {
1695
+ emits('header-row-menu', e);
1696
+ }
1697
+
1698
+ function onRowMenu(e: MouseEvent) {
1699
+ const rowIndex = getClosestTrIndex(e.target as HTMLElement);
1700
+ const row = dataSourceCopy.value[rowIndex];
1701
+ if (!row) return;
1702
+ emits('row-menu', e, row, { rowIndex });
1703
+ }
1704
+
1705
+ function triangleClick(e: MouseEvent, row: DT, col: StkTableColumn<DT>) {
1706
+ if (col.type === 'expand') {
1707
+ toggleExpandRow(row, col);
1708
+ } else if (col.type === 'tree-node') {
1709
+ toggleTreeNode(row, col);
1710
+ }
1711
+ }
1712
+
1713
+ function onCellClick(e: MouseEvent) {
1714
+ const rowIndex = getClosestTrIndex(e.target as HTMLElement);
1715
+ const row = dataSourceCopy.value[rowIndex];
1716
+ if (!row) return;
1717
+ const colKey = getClosestColKey(e.target as HTMLElement);
1718
+ const col = tableHeaderLast.value.find(item => colKeyGen.value(item) === colKey);
1719
+ if (!col) return;
1720
+ // Delegated triangle/fold icon click
1721
+ if ((e.target as HTMLElement)?.closest('.stk-fold-icon')) {
1722
+ triangleClick(e, row, col);
1723
+ return;
1724
+ }
1725
+ if (props.cellActive) {
1726
+ const cellKey = cellKeyGen(row, col);
1727
+ const result = { row, col, select: false, rowIndex };
1728
+ if (props.selectedCellRevokable && currentSelectedCellKey.value === cellKey) {
1729
+ currentSelectedCellKey.value = void 0;
1730
+ } else {
1731
+ currentSelectedCellKey.value = cellKey;
1732
+ result.select = true;
1733
+ }
1734
+ emits('cell-selected', e, result);
1735
+ }
1736
+ emits('cell-click', e, row, col, { rowIndex });
1737
+ }
1738
+
1739
+ function getCellEventData(e: MouseEvent) {
1740
+ const rowIndex = getClosestTrIndex(e.target as HTMLElement) || 0;
1741
+ const row = dataSourceCopy.value[rowIndex];
1742
+ const colKey = getClosestColKey(e.target as HTMLElement);
1743
+ const col = tableHeaderLast.value.find(item => colKeyGen.value(item) === colKey) as any;
1744
+ return { row, col, rowIndex };
1745
+ }
1746
+
1747
+ /** th click */
1748
+ function onHeaderCellClick(e: MouseEvent, col: StkTableColumn<DT>) {
1749
+ onColumnSort(col);
1750
+ emits('header-cell-click', e, col);
1751
+ }
1752
+
1753
+ /**
1754
+ * Delegated mouseover on tbody: emits cell-mouseover, and simulates cell-mouseenter
1755
+ * by checking if relatedTarget is outside the current td.
1756
+ */
1757
+ function onCellMouseOver(e: MouseEvent) {
1758
+ const td = getClosestTd(e.target as HTMLElement);
1759
+ if (!td) return;
1760
+ const { row, col } = getCellEventData(e);
1761
+ emits('cell-mouseover', e, row, col);
1762
+ // Simulate cell-mouseenter: relatedTarget is outside this td
1763
+ const related = e.relatedTarget as Node | null;
1764
+ if (!related || !td.contains(related)) {
1765
+ emits('cell-mouseenter', e, row, col);
1766
+ }
1767
+ }
1768
+
1769
+ /**
1770
+ * Delegated mouseout on tbody: handles both cell-mouseleave and tr-mouseleave.
1771
+ * - cell-mouseleave: relatedTarget is outside the current td.
1772
+ * - tr-mouseleave: relatedTarget is outside the current tr (simulates the old onTrMouseLeave).
1773
+ */
1774
+ function onTbodyMouseOut(e: MouseEvent) {
1775
+ const target = e.target as HTMLElement;
1776
+ const related = e.relatedTarget as Node | null;
1777
+
1778
+ // Cell mouseleave
1779
+ const td = getClosestTd(target);
1780
+ if (td && (!related || !td.contains(related))) {
1781
+ const { row, col } = getCellEventData(e);
1782
+ emits('cell-mouseleave', e, row, col);
1783
+ }
1784
+
1785
+ // Tr mouseleave
1786
+ const tr = getClosestTr(target);
1787
+ if (tr && (!related || !tr.contains(related))) {
1788
+ currentHoverRow = null;
1789
+ if (props.showTrHoverClass) {
1790
+ currentHoverRowKey.value = null;
1791
+ }
1792
+ if (props.rowHover) {
1793
+ updateHoverMergedCells(void 0);
1794
+ }
1795
+ }
1796
+ }
1797
+
1798
+ /** Delegated drop on tbody: extracts rowIndex from the closest tr and calls onTrDrop. */
1799
+ function onBodyDrop(e: DragEvent) {
1800
+ const trIndex = getClosestTrIndex(e.target as HTMLElement);
1801
+ if (trIndex < 0) return;
1802
+ onTrDrop(e, getAbsoluteRowIndex(trIndex));
1803
+ }
1804
+
1805
+ function onCellMouseDown(e: MouseEvent) {
1806
+ const { row, col, rowIndex } = getCellEventData(e);
1807
+ emits('cell-mousedown', e, row, col, { rowIndex });
1808
+
1809
+ if (areaSelectionConfig.value.enabled) {
1810
+ onSelectionMouseDown(e);
1811
+ }
1812
+ }
1813
+
1814
+ // isWheeling: true when wheel event is triggered, auto reset to false after delay
1815
+ const [isWheeling, setIsWheeling] = useWheeling();
1816
+
1817
+ /**
1818
+ * proxy scroll, prevent white screen
1819
+ * @param e
1820
+ */
1821
+ function onTableWheel(e: WheelEvent) {
1822
+ if (props.smoothScroll) return;
1823
+ // if is resizing, not allow scroll
1824
+ if (isColResizing.value) {
1825
+ e.stopPropagation();
1826
+ return;
1827
+ }
1828
+ const dom = tableContainerRef.value as HTMLElement;
1829
+
1830
+ const { deltaY, deltaX, shiftKey } = e;
1831
+
1832
+ if (virtual_on.value && deltaY && !shiftKey) {
1833
+ const { containerHeight, scrollTop, scrollHeight } = virtualScroll.value;
1834
+ // overflow: hidden mode: manually control scroll, preventDefault to block parent scroll
1835
+ const canScrollDown = scrollTop < scrollHeight - containerHeight - 1;
1836
+ const canScrollUp = scrollTop > 1;
1837
+
1838
+ if ((deltaY > 0 && canScrollDown) || (deltaY < 0 && canScrollUp)) {
1839
+ // Table can still scroll → mark wheeling & prevent parent
1840
+ setIsWheeling(true);
1841
+ e.preventDefault();
1842
+ } else if (isWheeling()) {
1843
+ // At boundary but user is still actively wheeling → briefly prevent parent for smooth transition
1844
+ e.preventDefault();
1845
+ }
1846
+ // At boundary + isWheeling expired → no preventDefault → browser scrolls parent naturally
1847
+
1848
+ if (isExperimentalScrollY.value) {
1849
+ rafUpdateVirtualScrollYForWheel(scrollTop + deltaY);
1850
+ updateCustomScrollbar();
1851
+ } else {
1852
+ dom.scrollTop += deltaY;
1853
+ }
1854
+ }
1855
+ if (virtualX_on.value) {
1856
+ const { containerWidth, scrollLeft, scrollWidth } = virtualScrollX.value;
1857
+ let distance = deltaX;
1858
+ if (shiftKey && deltaY) {
1859
+ distance = deltaY;
1860
+ }
1861
+ const canScrollRight = scrollLeft < scrollWidth - containerWidth - 1;
1862
+ const canScrollLeft = scrollLeft > 1;
1863
+
1864
+ if ((distance > 0 && canScrollRight) || (distance < 0 && canScrollLeft)) {
1865
+ setIsWheeling(true);
1866
+ e.preventDefault();
1867
+ } else if (isWheeling()) {
1868
+ e.preventDefault();
1869
+ }
1870
+ dom.scrollLeft += distance;
1871
+ }
1872
+ }
1873
+
1874
+ /** Prevent re-entrant requestAnimationFrame in onTableScroll */
1875
+ let scrollRAFScheduled = false;
1876
+
1877
+ /**
1878
+ * @param e scrollEvent
1879
+ */
1880
+ function onTableScroll(e: Event) {
1881
+ if (!e?.target || scrollRAFScheduled) return;
1882
+ scrollRAFScheduled = true;
1883
+ requestAnimationFrame(() => {
1884
+ scrollRAFScheduled = false;
1885
+ const { scrollTop, scrollLeft } = e.target as HTMLElement;
1886
+ const { scrollTop: vScrollTop } = virtualScroll.value;
1887
+ const { scrollLeft: vScrollLeft } = virtualScrollX.value;
1888
+ // 在 experimental.scrollY 模式下,纵向滚动通过 transform 模拟,DOM scrollTop 始终为 0,
1889
+ // 不能用来判断纵向滚动,否则横向滚动时会误触发纵向滚动重置
1890
+ const isYScroll = isExperimentalScrollY.value ? false : scrollTop !== vScrollTop;
1891
+ const isXScroll = scrollLeft !== vScrollLeft;
1892
+
1893
+ if (isYScroll) {
1894
+ updateVirtualScrollY(scrollTop);
1895
+ }
1896
+
1897
+ if (isXScroll) {
1898
+ if (virtualX_on.value) {
1899
+ updateVirtualScrollX(scrollLeft);
1900
+ } else {
1901
+ // en: Record the scroll position. Used to determine isXScroll
1902
+ virtualScrollX.value.scrollLeft = scrollLeft;
1903
+ }
1904
+ updateFixedShadow(virtualScrollX);
1905
+ }
1906
+
1907
+ if (isYScroll) {
1908
+ const { startIndex, endIndex } = virtualScroll.value;
1909
+ emits('scroll', e, { startIndex, endIndex });
1910
+ }
1911
+ if (isXScroll) {
1912
+ emits('scroll-x', e);
1913
+ }
1914
+
1915
+ updateCustomScrollbar();
1916
+ });
1917
+ }
1918
+
1919
+ /** tr hover */
1920
+ function onTrMouseOver(e: MouseEvent) {
1921
+ const tr = getClosestTr(e.target as HTMLElement);
1922
+ if (!tr) return;
1923
+ const rowIndex = Number(tr.dataset.rowI);
1924
+ const row = dataSourceCopy.value[rowIndex];
1925
+ if (currentHoverRow === row) return;
1926
+ currentHoverRow = row;
1927
+ const rowKey = tr.dataset.rowKey;
1928
+ if (props.showTrHoverClass) {
1929
+ currentHoverRowKey.value = rowKey || null;
1930
+ }
1931
+ if (props.rowHover) {
1932
+ updateHoverMergedCells(rowKey);
1933
+ }
1934
+ }
1935
+
1936
+ /**
1937
+ * 选中一行
1938
+ *
1939
+ * en: Select a row
1940
+ * @param {string} rowKeyOrRow selected rowKey, undefined to unselect
1941
+ * @param {boolean} option.silent if set true not emit `current-change`. default:false
1942
+ * @param {boolean} option.deep if set true, deep search in children. default:false
1943
+ */
1944
+ function setCurrentRow(rowKeyOrRow: string | undefined | DT, option: { silent?: boolean; deep?: boolean } = { silent: false, deep: false }) {
1945
+ const select = rowKeyOrRow !== void 0;
1946
+ const currentRowTemp = currentRow.value;
1947
+ if (!select) {
1948
+ currentRow.value = void 0;
1949
+ currentRowKey.value = void 0;
1950
+ updateActiveMergedCells(true);
1951
+ } else if (typeof rowKeyOrRow === 'string') {
1952
+ const findRowByKey = (data: DT[], key: string): DT | null => {
1953
+ for (let i = 0; i < data.length; i++) {
1954
+ const item = data[i];
1955
+ if (rowKeyGen(item) === key) {
1956
+ return item;
1957
+ }
1958
+ if (option.deep && item.children?.length) {
1959
+ const found = findRowByKey(item.children, key);
1960
+ if (found) {
1961
+ return found;
1962
+ }
1963
+ }
1964
+ }
1965
+ return null;
1966
+ };
1967
+
1968
+ currentRowKey.value = rowKeyOrRow;
1969
+ updateActiveMergedCells(false, currentRowKey.value);
1970
+ const row = findRowByKey(dataSourceCopy.value || [], rowKeyOrRow);
1971
+ if (!row) {
1972
+ console.warn('setCurrentRow failed.rowKey:', rowKeyOrRow);
1973
+ return;
1974
+ }
1975
+ currentRow.value = row;
1976
+ } else {
1977
+ currentRow.value = rowKeyOrRow;
1978
+ currentRowKey.value = rowKeyGen(rowKeyOrRow);
1979
+ updateActiveMergedCells(false, currentRowKey.value);
1980
+ }
1981
+ if (!option.silent) {
1982
+ emits('current-change', /** no Event */ null, select ? currentRow.value : currentRowTemp, { select });
1983
+ }
1984
+ }
1985
+
1986
+ /**
1987
+ * set highlight active cell (props.cellActive=true)
1988
+ * @param row row if undefined, clear highlight
1989
+ * @param col column
1990
+ * @param option.silent if emit current-change. default:false(not emit `current-change`)
1991
+ */
1992
+ function setSelectedCell(row?: DT, col?: StkTableColumn<DT>, option = { silent: false }) {
1993
+ if (!dataSourceCopy.value.length) return;
1994
+ const select = row !== void 0 && col !== void 0;
1995
+ currentSelectedCellKey.value = select ? cellKeyGen(row, col) : void 0;
1996
+ if (!option.silent) {
1997
+ emits('cell-selected', /** no Event */ null, { row, col, select });
1998
+ }
1999
+ }
2000
+
2001
+ /** smooth 滚动动画时长(ms) */
2002
+ const SMOOTH_SCROLL_DURATION = 300;
2003
+ let smoothScrollRafId = 0;
2004
+ /** 动画代际序号,取消后旧动画的 rAF 回调不再生效 */
2005
+ let smoothScrollSeq = 0;
2006
+ /** 当前动画的手势监听清理函数(监听直接挂 cancelSmoothScroll,无额外包装) */
2007
+ let removeGestureListeners: (() => void) | null = null;
2008
+
2009
+ /** 取消进行中的平滑滚动动画 */
2010
+ function cancelSmoothScroll() {
2011
+ smoothScrollSeq++;
2012
+ if (smoothScrollRafId) {
2013
+ cancelAnimationFrame(smoothScrollRafId);
2014
+ smoothScrollRafId = 0;
2015
+ }
2016
+ if (removeGestureListeners) {
2017
+ removeGestureListeners();
2018
+ removeGestureListeners = null;
2019
+ }
2020
+ }
2021
+
2022
+ /**
2023
+ * 解析滚动轴目标为绝对像素坐标
2024
+ * @param target 数字坐标或 { index, key, px } 目标,undefined 表示不改变该轴
2025
+ * @param resolveByIndex 按 index 解析基准偏移,失败返回 null
2026
+ * @param resolveByKey 按 key 解析基准偏移,失败返回 null
2027
+ * @returns null 表示该轴不滚动
2028
+ */
2029
+ function resolveScrollAxis(
2030
+ target: number | ScrollAxisTarget | undefined,
2031
+ resolveByIndex: (index: number) => number | null,
2032
+ resolveByKey: (key: string | number) => number | null,
2033
+ ): number | null {
2034
+ if (target === undefined) return null;
2035
+ if (typeof target === 'number') return target;
2036
+ let base: number | null;
2037
+ if (target.index !== undefined) {
2038
+ // index 优先于 key
2039
+ base = resolveByIndex(target.index);
2040
+ } else if (target.key !== undefined) {
2041
+ base = resolveByKey(target.key);
2042
+ } else {
2043
+ // 仅传 px:基准为 0
2044
+ base = 0;
2045
+ }
2046
+ if (base === null) return null;
2047
+ return base + (target.px ?? 0);
2048
+ }
2049
+
2050
+ /** top 轴基准偏移:目标行之前所有行的累计高度 */
2051
+ function resolveTopByIndex(index: number): number | null {
2052
+ if (index < 0 || index >= dataSourceCopy.value.length) return null;
2053
+ return getRowsHeight(index);
2054
+ }
2055
+ function resolveTopByKey(key: string | number): number | null {
2056
+ const index = dataSourceCopy.value.findIndex(row => rowKeyGen(row) === key);
2057
+ return index === -1 ? null : getRowsHeight(index);
2058
+ }
2059
+
2060
+ /** left 轴基准偏移:目标列之前所有列的宽度之和(与区域选择内部 getColPosition 同算法) */
2061
+ function getColLeft(colIndex: number): number {
2062
+ const cols = tableHeaderLast.value;
2063
+ let left = 0;
2064
+ for (let i = 0; i < colIndex; i++) {
2065
+ left += getCalculatedColWidth(cols[i]);
2066
+ }
2067
+ return left;
2068
+ }
2069
+
2070
+ /** top 轴最大可滚动距离:基于理论内容高度(所有行高之和),不依赖 DOM 测量 */
2071
+ function getMaxScrollTop(): number {
2072
+ return Math.max(0, getRowsHeight(dataSourceCopy.value.length) - virtualScroll.value.containerHeight);
2073
+ }
2074
+
2075
+ function resolveLeftByIndex(index: number): number | null {
2076
+ if (index < 0 || index >= tableHeaderLast.value.length) return null;
2077
+ return getColLeft(index);
2078
+ }
2079
+ function resolveLeftByKey(key: string | number): number | null {
2080
+ const index = tableHeaderLast.value.findIndex(col => col.dataIndex === key);
2081
+ return index === -1 ? null : getColLeft(index);
2082
+ }
2083
+
2084
+ /** 执行滚动(数字重载与 options 重载共用) */
2085
+ function applyScrollPosition(top: number | null, left: number | null) {
2086
+ if (top !== null) {
2087
+ if (isExperimentalScrollY.value) {
2088
+ updateVirtualScrollY(top);
2089
+ updateCustomScrollbar();
2090
+ } else {
2091
+ tableContainerRef.value!.scrollTop = top;
2092
+ }
2093
+ }
2094
+ if (left !== null) tableContainerRef.value!.scrollLeft = left;
2095
+ }
2096
+
2097
+ /** 平滑滚动到目标位置(rAF 动画,ease-out);每帧走与用户滚动相同的路径 */
2098
+ function smoothScrollTo(top: number | null, left: number | null) {
2099
+ const container = tableContainerRef.value;
2100
+ if (!container || (top === null && left === null)) return;
2101
+ const startTop = isExperimentalScrollY.value ? virtualScroll.value.scrollTop : container.scrollTop;
2102
+ const startLeft = container.scrollLeft;
2103
+ const deltaTop = top === null ? 0 : top - startTop;
2104
+ const deltaLeft = left === null ? 0 : left - startLeft;
2105
+ if (!deltaTop && !deltaLeft) return;
2106
+
2107
+ const startTime = Date.now();
2108
+ const ease = (t: number) => 1 - Math.pow(1 - t, 3);
2109
+ // 用户手势介入时取消动画
2110
+ container.addEventListener('wheel', cancelSmoothScroll, { passive: true });
2111
+ container.addEventListener('touchstart', cancelSmoothScroll, { passive: true });
2112
+ removeGestureListeners = () => {
2113
+ container.removeEventListener('wheel', cancelSmoothScroll);
2114
+ container.removeEventListener('touchstart', cancelSmoothScroll);
2115
+ };
2116
+
2117
+ const seq = smoothScrollSeq;
2118
+ const step = () => {
2119
+ // 动画已被取消(新调用或用户手势),旧回调直接退出
2120
+ if (seq !== smoothScrollSeq) return;
2121
+ const t = Math.min(1, (Date.now() - startTime) / SMOOTH_SCROLL_DURATION);
2122
+ const eased = ease(t);
2123
+ applyScrollPosition(top === null ? null : startTop + deltaTop * eased, left === null ? null : startLeft + deltaLeft * eased);
2124
+ if (t < 1) {
2125
+ smoothScrollRafId = requestAnimationFrame(step);
2126
+ } else {
2127
+ smoothScrollRafId = 0;
2128
+ removeGestureListeners!();
2129
+ removeGestureListeners = null;
2130
+ }
2131
+ };
2132
+ smoothScrollRafId = requestAnimationFrame(step);
2133
+ }
2134
+
2135
+ /**
2136
+ * set scroll bar position
2137
+ * - 数字重载(向后兼容):`scrollTo(top, left)`,null 表示不改变该轴,省略默认 0
2138
+ * - options 重载:`scrollTo({ top, left, behavior })`,每轴可传像素数字或 `{ index, key, px }` 目标,
2139
+ * 省略的轴不改变位置;目标无法解析(index 越界 / key 不存在)时该轴静默跳过
2140
+ * @param options null to not change
2141
+ * @param leftArg null to not change
2142
+ */
2143
+ function scrollTo(top?: number | null, left?: number | null): void;
2144
+ function scrollTo(options: ScrollToOptions): void;
2145
+ function scrollTo(options?: ScrollToOptions | number | null, leftArg?: number | null): void {
2146
+ if (!tableContainerRef.value) return;
2147
+ cancelSmoothScroll();
2148
+ if (options === undefined || options === null || typeof options === 'number') {
2149
+ // 数字重载:保持既有行为
2150
+ applyScrollPosition(options === undefined ? 0 : options, leftArg === undefined ? 0 : leftArg);
2151
+ return;
2152
+ }
2153
+ let top: number | null = resolveScrollAxis(options.top, resolveTopByIndex, resolveTopByKey);
2154
+ let left: number | null = resolveScrollAxis(options.left, resolveLeftByIndex, resolveLeftByKey);
2155
+ if (top === null && left === null) return;
2156
+ // 钳制到合法滚动范围(基于理论内容尺寸,与滚动条语义一致)
2157
+ if (top !== null) top = Math.min(Math.max(top, 0), getMaxScrollTop());
2158
+ if (left !== null)
2159
+ left = Math.min(Math.max(left, 0), Math.max(0, getColLeft(tableHeaderLast.value.length) - virtualScrollX.value.containerWidth));
2160
+ if (options.behavior === 'smooth') {
2161
+ smoothScrollTo(top, left);
2162
+ } else {
2163
+ applyScrollPosition(top, left);
2164
+ }
2165
+ }
2166
+ // 编译期断言:实现签名与公共类型 ScrollToFn 保持一致(type-tests 依赖此类型)
2167
+ void (scrollTo satisfies ScrollToFn);
2168
+
2169
+ /** get current table data */
2170
+ function getTableData() {
2171
+ return toRaw(dataSourceCopy.value);
2172
+ }
2173
+
2174
+ /**
2175
+ * 清空 mergeCells 结果缓存并强制重算合并结果。
2176
+ *
2177
+ * 缓存以「绝对行索引 + 叶子列索引」为键、行引用同一性校验命中,
2178
+ * 仅在 dataSource/列配置变化时自动清空。业务代码原地修改行字段
2179
+ * (行对象引用不变,常见于响应式行对象)且 mergeCells 结果依赖这些字段时,
2180
+ * 缓存会命中旧结果,需调用本方法显式失效。
2181
+ */
2182
+ function clearMergeCellsCache() {
2183
+ mergeCellsCache.clear();
2184
+ // 重建 dataSourceCopy(新数组引用):触发缓存清空 watch 与渲染更新,
2185
+ // 使 hiddenCellMap/hoverRowMap 及渲染出的 rowspan/colspan 全部重算
2186
+ initDataSource();
2187
+ updateMaxRowSpan();
2188
+ }
2189
+
2190
+ defineExpose({
2191
+ /**
2192
+ * 重新计算虚拟列表宽高
2193
+ *
2194
+ * en: calc virtual scroll x & y info
2195
+ * @see {@link initVirtualScroll}
2196
+ */
2197
+ initVirtualScroll,
2198
+ /**
2199
+ * 重新计算虚拟列表宽度
2200
+ *
2201
+ * en: calc virtual scroll x
2202
+ * @see {@link initVirtualScrollX}
2203
+ */
2204
+ initVirtualScrollX,
2205
+ /**
2206
+ * 重新计算虚拟列表高度
2207
+ *
2208
+ * en: calc virtual scroll y
2209
+ * @see {@link initVirtualScrollY}
2210
+ */
2211
+ initVirtualScrollY,
2212
+ /**
2213
+ * 清空 mergeCells 结果缓存并强制重算合并结果
2214
+ *
2215
+ * en: Clear mergeCells result cache and force recomputation
2216
+ * @see {@link clearMergeCellsCache}
2217
+ */
2218
+ clearMergeCellsCache,
2219
+ /**
2220
+ * 选中一行
2221
+ *
2222
+ * en:select a row
2223
+ * @see {@link setCurrentRow}
2224
+ */
2225
+ setCurrentRow,
2226
+ /**
2227
+ * 取消选中单元格
2228
+ *
2229
+ * en: set highlight active cell (props.cellActive=true)
2230
+ * @see {@link setSelectedCell}
2231
+ */
2232
+ setSelectedCell,
2233
+ /**
2234
+ * 设置高亮单元格
2235
+ *
2236
+ * en: Set highlight cell
2237
+ * @see {@link setHighlightDimCell}
2238
+ */
2239
+ setHighlightDimCell,
2240
+ /**
2241
+ * 设置高亮行
2242
+ *
2243
+ * en: Set highlight row
2244
+ * @see {@link setHighlightDimRow}
2245
+ */
2246
+ setHighlightDimRow,
2247
+ /**
2248
+ * 表格排序列colKey
2249
+ *
2250
+ * en: Table sort column colKey
2251
+ */
2252
+ sortCol,
2253
+ /**
2254
+ * 排序状态数组
2255
+ *
2256
+ * en: Multi-column sort states array
2257
+ */
2258
+ sortStates,
2259
+ /**
2260
+ * 表格排序列顺序
2261
+ *
2262
+ * en: get current sort info
2263
+ * @see {@link getSortColumns}
2264
+ */
2265
+ getSortColumns,
2266
+ /**
2267
+ * 设置表头排序状态
2268
+ *
2269
+ * en: Set the sort status of the table header
2270
+ * @see {@link setSorter}
2271
+ */
2272
+ setSorter,
2273
+ /**
2274
+ * 重置sorter状态
2275
+ *
2276
+ * en: Reset the sorter status
2277
+ * @see {@link resetSorter}
2278
+ */
2279
+ resetSorter,
2280
+ /**
2281
+ * 滚动至
2282
+ * - 数字参数:scrollTo(top, left),null 表示不改变该轴
2283
+ * - 对象参数:scrollTo({ top, left, behavior }),top/left 可传像素数字或 { index, key, px } 目标
2284
+ *
2285
+ * en: Scroll to position or target row/column
2286
+ * @see {@link scrollTo}
2287
+ */
2288
+ scrollTo,
2289
+ /**
2290
+ * 获取表格数据
2291
+ *
2292
+ * en: Get table data
2293
+ * @see {@link getTableData}
2294
+ */
2295
+ getTableData,
2296
+ getRowIndex,
2297
+ getColumnIndex,
2298
+ /**
2299
+ * 设置展开的行
2300
+ *
2301
+ * en: Set expanded rows
2302
+ * @see {@link setRowExpand}
2303
+ */
2304
+ setRowExpand,
2305
+ /**
2306
+ * 不定行高时,如果行高有变化,则调用此方法更新行高。
2307
+ *
2308
+ * en: When the row height is not fixed, call this method to update the row height if the row height changes.
2309
+ * @see {@link setAutoHeight}
2310
+ */
2311
+ setAutoHeight,
2312
+ /**
2313
+ * 清除所有行高
2314
+ *
2315
+ * en: Clear all row heights
2316
+ * @see {@link clearAllAutoHeight}
2317
+ */
2318
+ clearAllAutoHeight,
2319
+ /**
2320
+ * 设置树节点展开状态
2321
+ *
2322
+ * en: Set tree node expand state
2323
+ * @see {@link setTreeExpand}
2324
+ */
2325
+ setTreeExpand,
2326
+ /**
2327
+ * 获取拖选选中的单元格信息
2328
+ *
2329
+ * en: Get selected cells info (areaSelection=true)
2330
+ * @see {@link getSelectedArea}
2331
+ */
2332
+ getSelectedArea,
2333
+ /**
2334
+ * 设置拖选选区
2335
+ *
2336
+ * en: Set cell selection range (areaSelection=true)
2337
+ * @see {@link setAreaSelection}
2338
+ */
2339
+ setAreaSelection,
2340
+ /**
2341
+ * 清空拖选选区
2342
+ *
2343
+ * en: Clear cell selection range (areaSelection=true)
2344
+ * @see {@link clearSelectedArea}
2345
+ */
2346
+ clearSelectedArea,
2347
+ /**
2348
+ * 复制选区内容到剪贴板
2349
+ *
2350
+ * en: Copy selected area to clipboard (areaSelection=true)
2351
+ * @see {@link copySelectedArea}
2352
+ */
2353
+ copySelectedArea,
2354
+ /**
2355
+ * 设置筛选状态(Beta)
2356
+ *
2357
+ * en: Set filter status(Beta)
2358
+ * @see {@link setFilter}
2359
+ */
2360
+ setFilter,
2361
+ });
2362
+ </script>