stk-table-vue 1.2.4 → 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.
- package/README.md +194 -194
- package/lib/{Dropdown-D_9cB0rD.js → Dropdown-BAANgg_5.js} +2 -2
- package/lib/{StkTable-CVcmlZi9.js → StkTable-CGcxwpdb.js} +22 -6
- package/lib/src/StkTable/StkTable.vue.d.ts +18 -4
- package/lib/src/StkTable/types/index.d.ts +4 -1
- package/lib/src/StkTable/useVirtualScroll.d.ts +6 -1
- package/lib/stk-table-vue.js +3 -3
- package/lib/style.css +1 -1
- package/package.json +93 -93
- package/src/StkTable/StkTable.vue +2362 -2338
- package/src/StkTable/components/DragHandle.vue +9 -9
- package/src/StkTable/components/SortIcon.vue +6 -6
- package/src/StkTable/components/TreeNodeCell.vue +19 -19
- package/src/StkTable/components/TriangleIcon.vue +3 -3
- package/src/StkTable/const.ts +50 -50
- package/src/StkTable/custom-cells/ChangeCell/ChangeCell.less +34 -34
- package/src/StkTable/custom-cells/ChangeCell/createChangeCell.ts +90 -90
- package/src/StkTable/custom-cells/ChangeCell/index.ts +4 -4
- package/src/StkTable/custom-cells/CheckboxCell/CheckboxCell.less +14 -14
- package/src/StkTable/custom-cells/CheckboxCell/CheckboxCell.vue +56 -56
- package/src/StkTable/custom-cells/CheckboxCell/createCheckboxCell.ts +146 -146
- package/src/StkTable/custom-cells/CheckboxCell/index.ts +5 -5
- package/src/StkTable/custom-cells/FilterCell/Dropdown/index.ts +16 -16
- package/src/StkTable/custom-cells/FilterCell/Dropdown/index.vue +186 -186
- package/src/StkTable/custom-cells/FilterCell/Filter.less +118 -118
- package/src/StkTable/custom-cells/FilterCell/Filter.vue +66 -66
- package/src/StkTable/custom-cells/FilterCell/createFilterCell.ts +120 -120
- package/src/StkTable/custom-cells/FilterCell/index.ts +6 -6
- package/src/StkTable/custom-cells/FilterCell/types.ts +43 -43
- package/src/StkTable/custom-cells/NumberCell/createNumberCell.ts +43 -43
- package/src/StkTable/custom-cells/NumberCell/index.ts +2 -2
- package/src/StkTable/custom-cells/utils/formatNumber.ts +137 -137
- package/src/StkTable/features/const.ts +1 -1
- package/src/StkTable/features/index.ts +1 -1
- package/src/StkTable/features/useAreaSelection.ts +1240 -1239
- package/src/StkTable/index.ts +20 -20
- package/src/StkTable/mergeCellsCache.ts +69 -69
- package/src/StkTable/registerFeature.ts +40 -40
- package/src/StkTable/style.less +791 -791
- package/src/StkTable/types/highlightDimOptions.ts +28 -28
- package/src/StkTable/types/index.ts +503 -500
- package/src/StkTable/useAutoResize.ts +87 -87
- package/src/StkTable/useColResize.ts +202 -202
- package/src/StkTable/useFixedCol.ts +168 -168
- package/src/StkTable/useFixedStyle.ts +75 -75
- package/src/StkTable/useGetFixedColPosition.ts +63 -63
- package/src/StkTable/useHighlight.ts +270 -270
- package/src/StkTable/useIndexResolver.ts +29 -29
- package/src/StkTable/useKeyboardArrowScroll.ts +114 -114
- package/src/StkTable/useMaxRowSpan.ts +60 -60
- package/src/StkTable/useMergeCells.ts +541 -541
- package/src/StkTable/useRowExpand.ts +81 -81
- package/src/StkTable/useScrollRowByRow.ts +96 -96
- package/src/StkTable/useScrollbar.ts +177 -177
- package/src/StkTable/useSorter.ts +267 -267
- package/src/StkTable/useTableColumns.ts +142 -142
- package/src/StkTable/useThDrag.ts +94 -94
- package/src/StkTable/useTrDrag.ts +100 -100
- package/src/StkTable/useTree.ts +253 -253
- package/src/StkTable/useVirtualScroll.ts +1032 -1025
- package/src/StkTable/useWheeling.ts +23 -23
- package/src/StkTable/utils/constRefUtils.ts +29 -29
- package/src/StkTable/utils/index.ts +331 -331
- package/src/StkTable/utils/rowHeightFenwickTree.ts +74 -74
- package/src/StkTable/utils/useTriggerRef.ts +33 -33
- package/src/VirtualTree.vue +622 -622
- package/src/VirtualTreeSelect.vue +367 -367
- package/src/vite-env.d.ts +10 -10
|
@@ -1,541 +1,541 @@
|
|
|
1
|
-
import { ComputedRef, Ref, computed, ref, ShallowRef, watch } from 'vue';
|
|
2
|
-
import { MergeCellsCache } from './mergeCellsCache';
|
|
3
|
-
import { ColKeyGen, MergeCellsParam, PrivateRowDT, PrivateStkTableColumn, RowActiveOption, RowKeyGen, UniqKey } from './types';
|
|
4
|
-
import { pureCellKeyGen } from './utils';
|
|
5
|
-
import type { VirtualScrollStore } from './useVirtualScroll';
|
|
6
|
-
|
|
7
|
-
/** Above-viewport cell descriptor: real column or placeholder colspan */
|
|
8
|
-
type AboveViewportCell = PrivateStkTableColumn<PrivateRowDT> | { __VT_PH__: number };
|
|
9
|
-
|
|
10
|
-
/** 视口上方占位单元格(共享常量;模板仅读取 __VT_PH__ 属性、key 使用列下标,可安全复用同一对象) */
|
|
11
|
-
const ABOVE_VIEWPORT_PH: AboveViewportCell = { __VT_PH__: 1 };
|
|
12
|
-
|
|
13
|
-
export function useMergeCells(
|
|
14
|
-
rowActiveProp: Ref<RowActiveOption<any>>,
|
|
15
|
-
tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>,
|
|
16
|
-
rowKeyGen: RowKeyGen,
|
|
17
|
-
colKeyGen: ColKeyGen,
|
|
18
|
-
virtual_dataSourcePart: ShallowRef<any[]>,
|
|
19
|
-
virtualScroll: Ref<VirtualScrollStore>,
|
|
20
|
-
/** 视口行渲染使用的列列表(virtual-x 时为可视列子集,否则即 tableHeaderLast) */
|
|
21
|
-
virtualX_columnPart: ComputedRef<PrivateStkTableColumn<PrivateRowDT>[]>,
|
|
22
|
-
/** 全量数据(仅用于缓存失效判定) */
|
|
23
|
-
dataSourceCopy: ShallowRef<any[]>,
|
|
24
|
-
/** mergeCells 结果共享缓存(与 useVirtualScroll 共用,可跨滚动帧复用) */
|
|
25
|
-
mergeCellsCache: MergeCellsCache,
|
|
26
|
-
/** 是否允许把连续空行合并为占位 tr(virtual && !autoRowHeight) */
|
|
27
|
-
canMergeEmptyRows: Ref<boolean>,
|
|
28
|
-
/** 视口下方合并为占位 tr 的行数(渲染 rowspan 属性修正用) */
|
|
29
|
-
belowViewportRowCount: Ref<number>,
|
|
30
|
-
) {
|
|
31
|
-
/**
|
|
32
|
-
* which cell need be hidden
|
|
33
|
-
* - key: rowKey
|
|
34
|
-
* - value: colKey Set
|
|
35
|
-
*/
|
|
36
|
-
const hiddenCellMap = ref<Record<UniqKey, Set<UniqKey>> | null>(null);
|
|
37
|
-
/**
|
|
38
|
-
* hover other row and rowspan cell should be highlighted
|
|
39
|
-
* - key: rowKey
|
|
40
|
-
* - value: cellKey Set
|
|
41
|
-
*/
|
|
42
|
-
const hoverRowMap = ref<Record<UniqKey, Set<string>>>({});
|
|
43
|
-
|
|
44
|
-
/** hover current row , which rowspan cells should be highlight */
|
|
45
|
-
const hoverMergedCells = ref(new Set<string>());
|
|
46
|
-
/** click current row , which rowspan cells should be highlight */
|
|
47
|
-
const activeMergedCells = ref(new Set<string>());
|
|
48
|
-
|
|
49
|
-
/** column index cache */
|
|
50
|
-
let colIndexCache: Map<UniqKey, number> | null = null;
|
|
51
|
-
/** 空合并段列表(start 为绝对行索引)。禁止合并时返回共享空数组 */
|
|
52
|
-
const EMPTY_BLOCKS: { start: number; count: number }[] = [];
|
|
53
|
-
|
|
54
|
-
/** 空下方占位段列表。无下方合并行时返回共享空数组 */
|
|
55
|
-
const EMPTY_SEGMENTS: { count: number }[] = [];
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* mergeCellsWrapper 返回值记忆化缓存(WeakMap<行对象, Map<colKey, 结果>>)。
|
|
59
|
-
* 签名 = 绝对行索引 + 代际(wrapperCacheGen):窗口/列/视口修正(上方空行段、
|
|
60
|
-
* 下方占位段、viewportEndIndex、下方行数)任一变化即整体失效。
|
|
61
|
-
*/
|
|
62
|
-
const wrapperCache = new WeakMap<
|
|
63
|
-
object,
|
|
64
|
-
Map<UniqKey, { absRowIndex: number; gen: number; result: { colspan?: number; rowspan?: number } | undefined }>
|
|
65
|
-
>();
|
|
66
|
-
|
|
67
|
-
/** 是否存在合并列(仅依赖列配置,跨滚动帧缓存,不随数据/滚动重算) */
|
|
68
|
-
const hasMergeColumn = computed(() => tableHeaderLast.value.some(col => !!col.mergeCells));
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 视口上方连续「无 td」空行段(行数 >= 2 才成段;单行保留独立 tr)。
|
|
72
|
-
* 空行判定与渲染保持一致:aboveViewportColumnMap 中列结果为空且非展开行。
|
|
73
|
-
* 供模板合并渲染(StkTable aboveRenderParts)与 rowspan 属性修正共用。
|
|
74
|
-
*/
|
|
75
|
-
const aboveEmptyBlocks = computed<{ start: number; count: number }[]>(() => {
|
|
76
|
-
if (!canMergeEmptyRows.value) return EMPTY_BLOCKS;
|
|
77
|
-
const data = virtual_dataSourcePart.value;
|
|
78
|
-
const { startIndex, viewportStartIndex } = virtualScroll.value;
|
|
79
|
-
const aboveCount = Math.min(data.length, Math.max(0, viewportStartIndex - startIndex));
|
|
80
|
-
if (aboveCount <= 0) return EMPTY_BLOCKS;
|
|
81
|
-
const colMap = aboveViewportColumnMap.value;
|
|
82
|
-
|
|
83
|
-
const isEmptyRow = (row: PrivateRowDT): boolean => {
|
|
84
|
-
// 展开行有独立行高且渲染内容,不能参与合并
|
|
85
|
-
if (!row || row.__EXP_R__) return false;
|
|
86
|
-
const cols = colMap.get(rowKeyGen(row));
|
|
87
|
-
// 仅当映射中明确为空列结果时才算空行;缺失(异常)时保留原渲染
|
|
88
|
-
return cols !== void 0 && cols.length === 0;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const blocks: { start: number; count: number }[] = [];
|
|
92
|
-
let runStart = -1;
|
|
93
|
-
const flushRun = (endExclusive: number) => {
|
|
94
|
-
if (endExclusive - runStart >= 2) {
|
|
95
|
-
blocks.push({ start: startIndex + runStart, count: endExclusive - runStart });
|
|
96
|
-
}
|
|
97
|
-
runStart = -1;
|
|
98
|
-
};
|
|
99
|
-
for (let i = 0; i < aboveCount; i++) {
|
|
100
|
-
if (isEmptyRow(data[i])) {
|
|
101
|
-
if (runStart < 0) runStart = i;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (runStart >= 0) flushRun(i);
|
|
105
|
-
}
|
|
106
|
-
if (runStart >= 0) flushRun(aboveCount);
|
|
107
|
-
return blocks.length ? blocks : EMPTY_BLOCKS;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 视口下方占位段:把 [viewportEndIndex+1, endIndex] 按跨界 rowspan 的
|
|
112
|
-
* 去重结束行切分为多段,每段渲染一个占位 tr。
|
|
113
|
-
*
|
|
114
|
-
* 单个占位 tr 无法表达多个 rowspan 的不同逻辑结束位置(不规律合并下
|
|
115
|
-
* 多个单元格会塌缩到同一 tr 底边,滚动时高度跳动);按结束行切段后,
|
|
116
|
-
* 每个单元格的修正 rowspan 恰好止于包含其逻辑结束行的段。
|
|
117
|
-
* 超长 rowspan 场景只有一个结束行(= 区域末端),仍只产生 1 段,不增加 DOM。
|
|
118
|
-
*/
|
|
119
|
-
const belowPhSegments = computed<{ count: number }[]>(() => {
|
|
120
|
-
const belowCount = belowViewportRowCount.value;
|
|
121
|
-
if (belowCount <= 0) return EMPTY_SEGMENTS;
|
|
122
|
-
const data = virtual_dataSourcePart.value;
|
|
123
|
-
const { startIndex, viewportEndIndex, endIndex } = virtualScroll.value;
|
|
124
|
-
// 下方区域末端钳制到窗口最后一行(endIndex 可能超出数据长度)
|
|
125
|
-
const regionEnd = Math.min(endIndex, startIndex + data.length - 1);
|
|
126
|
-
const columns = virtualX_columnPart.value;
|
|
127
|
-
|
|
128
|
-
// 跨界 rowspan 的锚点只可能在视口上方保留行与视口行内,收集其去重结束行
|
|
129
|
-
const endsSet = new Set<number>();
|
|
130
|
-
const anchorRowEnd = Math.min(data.length - 1, viewportEndIndex - startIndex);
|
|
131
|
-
for (let rowOffset = 0; rowOffset <= anchorRowEnd; rowOffset++) {
|
|
132
|
-
const row = data[rowOffset];
|
|
133
|
-
if (!row) continue;
|
|
134
|
-
const absRowIndex = startIndex + rowOffset;
|
|
135
|
-
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
136
|
-
const col = columns[colIdx];
|
|
137
|
-
if (!col.mergeCells) continue;
|
|
138
|
-
const leafIndex = col.__LF_S__ ?? colIdx;
|
|
139
|
-
const { rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
|
|
140
|
-
if (rowspan <= 1) continue;
|
|
141
|
-
const spanEnd = absRowIndex + rowspan - 1;
|
|
142
|
-
if (spanEnd > viewportEndIndex) endsSet.add(Math.min(spanEnd, regionEnd));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 兜底:未收集到跨界结束行时保持单段(与合并前行为一致)
|
|
147
|
-
if (!endsSet.size) return [{ count: belowCount }];
|
|
148
|
-
|
|
149
|
-
const ends = Array.from(endsSet).sort((a, b) => a - b);
|
|
150
|
-
const segments: { count: number }[] = [];
|
|
151
|
-
let prev = viewportEndIndex;
|
|
152
|
-
for (let i = 0; i < ends.length; i++) {
|
|
153
|
-
const count = ends[i] - prev;
|
|
154
|
-
if (count > 0) segments.push({ count });
|
|
155
|
-
prev = ends[i];
|
|
156
|
-
}
|
|
157
|
-
if (regionEnd > prev) segments.push({ count: regionEnd - prev });
|
|
158
|
-
return segments;
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
let wrapperCacheGen = 0;
|
|
162
|
-
|
|
163
|
-
watch(
|
|
164
|
-
[
|
|
165
|
-
virtual_dataSourcePart,
|
|
166
|
-
tableHeaderLast,
|
|
167
|
-
aboveEmptyBlocks,
|
|
168
|
-
belowPhSegments,
|
|
169
|
-
() => virtualScroll.value.viewportEndIndex,
|
|
170
|
-
belowViewportRowCount,
|
|
171
|
-
],
|
|
172
|
-
() => {
|
|
173
|
-
wrapperCacheGen++;
|
|
174
|
-
},
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* 数据/列真正变化时才清空 mergeCells 结果缓存。
|
|
179
|
-
* 缓存键为绝对索引且命中时校验行引用同一性,滚动换窗(virtual_dataSourcePart 变化)
|
|
180
|
-
* 无需清缓存——相邻帧高度重叠,复用缓存可避免对同一单元格重复调用用户回调。
|
|
181
|
-
* 行字段被原地修改(引用不变)时不会触发本 watch,需由外部调用暴露的
|
|
182
|
-
* clearMergeCellsCache 实例方法显式失效缓存并重算。
|
|
183
|
-
*/
|
|
184
|
-
watch([dataSourceCopy, tableHeaderLast], () => {
|
|
185
|
-
mergeCellsCache.clear();
|
|
186
|
-
colIndexCache = null;
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
watch([virtual_dataSourcePart, tableHeaderLast], () => {
|
|
190
|
-
buildHiddenCellMap();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Pre-build hiddenCellMap and hoverRowMap before rendering to avoid
|
|
195
|
-
* a two-render cycle (null → populated) that causes visual flicker.
|
|
196
|
-
*
|
|
197
|
-
* Previously, the watch set hiddenCellMap.value = null, and the map was
|
|
198
|
-
* rebuilt during rendering by mergeCellsWrapper → hideCells. This meant
|
|
199
|
-
* the first render after scroll had no hidden cells (all visible), and
|
|
200
|
-
* a second render was needed to apply merges — causing flicker/jumping.
|
|
201
|
-
*/
|
|
202
|
-
function buildHiddenCellMap() {
|
|
203
|
-
/**
|
|
204
|
-
* 无合并列快速通道:mergeCells 是可选特性,多数表格没有合并单元格,
|
|
205
|
-
* 不应为此空转「窗口行数 × 列数」的扫描和每帧的 ref 重赋值。
|
|
206
|
-
* 仅在「有 → 无」切换时重置一次(清掉残留的隐藏/高亮状态),之后滚动帧直接返回。
|
|
207
|
-
*/
|
|
208
|
-
if (!hasMergeColumn.value) {
|
|
209
|
-
if (hiddenCellMap.value) {
|
|
210
|
-
hiddenCellMap.value = null;
|
|
211
|
-
hoverRowMap.value = {};
|
|
212
|
-
}
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
hiddenCellMap.value = {};
|
|
217
|
-
hoverRowMap.value = {};
|
|
218
|
-
|
|
219
|
-
const data = virtual_dataSourcePart.value;
|
|
220
|
-
/**
|
|
221
|
-
* 只遍历实际渲染的列:virtual-x 模式下即视列子集 + 固定列,
|
|
222
|
-
* 非 virtual-x 时即 tableHeaderLast。
|
|
223
|
-
* hiddenCellMap/hoverRowMap 只会被渲染出来的 td 消费(shouldHideCell、hover 高亮),
|
|
224
|
-
* 未渲染列的合并无需计算;colspan 锚点列已被可视列范围修正保证包含在列表内。
|
|
225
|
-
*/
|
|
226
|
-
const columns = virtualX_columnPart.value;
|
|
227
|
-
/**
|
|
228
|
-
* mergeCells 入参与缓存键统一使用绝对行索引(不随虚拟窗口滑动变化)。
|
|
229
|
-
* 此前这里使用切片内相对索引,而 aboveViewportColumnMap 使用绝对索引,
|
|
230
|
-
* 二者共用 mergeCellsCache 时会发生键冲突,导致取到错误行的合并结果
|
|
231
|
-
* (表现为滚动时视口上方合并单元格锚点丢失/闪烁)。
|
|
232
|
-
*/
|
|
233
|
-
const baseRowIndex = virtualScroll.value.startIndex;
|
|
234
|
-
|
|
235
|
-
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
236
|
-
const row = data[rowIndex];
|
|
237
|
-
if (!row) continue;
|
|
238
|
-
|
|
239
|
-
const absRowIndex = baseRowIndex + rowIndex;
|
|
240
|
-
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
241
|
-
const col = columns[colIdx];
|
|
242
|
-
if (!col.mergeCells) continue;
|
|
243
|
-
|
|
244
|
-
// 叶子列索引:与 mergeCellsWrapper / aboveViewportColumnMap / buildColMergeRange 的缓存键一致
|
|
245
|
-
const leafIndex = col.__LF_S__ ?? colIdx;
|
|
246
|
-
const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
|
|
247
|
-
if (colspan === 1 && rowspan === 1) continue;
|
|
248
|
-
|
|
249
|
-
const rowKey = rowKeyGen(row);
|
|
250
|
-
const colKey = colKeyGen.value(col);
|
|
251
|
-
const mergedCellKey = pureCellKeyGen(rowKey, colKey);
|
|
252
|
-
|
|
253
|
-
for (let i = rowIndex; i < rowIndex + rowspan; i++) {
|
|
254
|
-
const targetRow = data[i];
|
|
255
|
-
if (!targetRow) break;
|
|
256
|
-
hideCells(rowKeyGen(targetRow), colKey, colspan, i === rowIndex, mergedCellKey);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* abstract the logic of hiding cells
|
|
264
|
-
*/
|
|
265
|
-
function hideCells(rowKey: UniqKey, colKey: UniqKey, colspan: number, isSelfRow = false, mergeCellKey: string) {
|
|
266
|
-
const headers = tableHeaderLast.value;
|
|
267
|
-
const colKeyGenValue = colKeyGen.value;
|
|
268
|
-
|
|
269
|
-
// use columns cache to avoid repeat findIndex
|
|
270
|
-
let startIndex = colIndexCache?.get(colKey);
|
|
271
|
-
if (startIndex === void 0) {
|
|
272
|
-
startIndex = headers.findIndex(item => colKeyGenValue(item) === colKey);
|
|
273
|
-
if (startIndex < 0) return;
|
|
274
|
-
|
|
275
|
-
if (!colIndexCache) colIndexCache = new Map();
|
|
276
|
-
colIndexCache.set(colKey, startIndex);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Initialize maps if needed
|
|
280
|
-
if (!hoverRowMap.value[rowKey]) {
|
|
281
|
-
hoverRowMap.value[rowKey] = new Set();
|
|
282
|
-
}
|
|
283
|
-
if (!hiddenCellMap.value) {
|
|
284
|
-
hiddenCellMap.value = {};
|
|
285
|
-
}
|
|
286
|
-
if (!hiddenCellMap.value[rowKey]) {
|
|
287
|
-
hiddenCellMap.value[rowKey] = new Set();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const hoverSet = hoverRowMap.value[rowKey];
|
|
291
|
-
const hiddenSet = hiddenCellMap.value[rowKey];
|
|
292
|
-
const endIndex = Math.min(startIndex + colspan, headers.length);
|
|
293
|
-
|
|
294
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
295
|
-
hoverSet.add(mergeCellKey);
|
|
296
|
-
|
|
297
|
-
if (isSelfRow && i === startIndex) {
|
|
298
|
-
// self row start cell does not need to be hidden
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const nextCol = headers[i];
|
|
303
|
-
if (!nextCol) break;
|
|
304
|
-
|
|
305
|
-
const nextColKey = colKeyGenValue(nextCol);
|
|
306
|
-
hiddenSet.add(nextColKey);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* calculate colspan and rowspan
|
|
312
|
-
* @param row
|
|
313
|
-
* @param col
|
|
314
|
-
* @param rowIndex
|
|
315
|
-
* @param colIndex
|
|
316
|
-
* @returns
|
|
317
|
-
*/
|
|
318
|
-
function mergeCellsWrapper(
|
|
319
|
-
row: MergeCellsParam<any>['row'],
|
|
320
|
-
col: MergeCellsParam<any>['col'],
|
|
321
|
-
rowIndex: MergeCellsParam<any>['rowIndex'],
|
|
322
|
-
colIndex: MergeCellsParam<any>['colIndex'],
|
|
323
|
-
): { colspan?: number; rowspan?: number } | undefined {
|
|
324
|
-
if (!col.mergeCells) return;
|
|
325
|
-
|
|
326
|
-
// 渲染传入的是切片内相对索引,统一换算为绝对行索引后再调用 mergeCells,
|
|
327
|
-
// 保证与 buildHiddenCellMap / aboveViewportColumnMap 的入参及缓存键一致
|
|
328
|
-
const absRowIndex = virtualScroll.value.startIndex + rowIndex;
|
|
329
|
-
const colKey = colKeyGen.value(col);
|
|
330
|
-
|
|
331
|
-
// 返回值记忆化:命中时跳过 getMergeCellsResult、rowspan 修正循环与对象分配。
|
|
332
|
-
// hideCells 为幂等写入(buildHiddenCellMap 已在渲染前按相同输入构建),命中时跳过安全。
|
|
333
|
-
let rowCache = wrapperCache.get(row);
|
|
334
|
-
if (!rowCache) {
|
|
335
|
-
rowCache = new Map();
|
|
336
|
-
wrapperCache.set(row, rowCache);
|
|
337
|
-
}
|
|
338
|
-
const hit = rowCache.get(colKey);
|
|
339
|
-
if (hit && hit.absRowIndex === absRowIndex && hit.gen === wrapperCacheGen) return hit.result;
|
|
340
|
-
|
|
341
|
-
const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, colIndex);
|
|
342
|
-
|
|
343
|
-
let result: { colspan?: number; rowspan?: number } | undefined;
|
|
344
|
-
if (colspan === 1 && rowspan === 1) {
|
|
345
|
-
result = void 0;
|
|
346
|
-
} else {
|
|
347
|
-
const mergedCellKey = pureCellKeyGen(rowKeyGen(row), colKey);
|
|
348
|
-
for (let i = rowIndex; i < rowIndex + rowspan; i++) {
|
|
349
|
-
const targetRow = virtual_dataSourcePart.value[i];
|
|
350
|
-
if (!targetRow) break;
|
|
351
|
-
hideCells(rowKeyGen(targetRow), colKey, colspan, i === rowIndex, mergedCellKey);
|
|
352
|
-
}
|
|
353
|
-
result = { colspan, rowspan: adjustRowspanForMergedRows(absRowIndex, rowspan) };
|
|
354
|
-
}
|
|
355
|
-
rowCache.set(colKey, { absRowIndex, gen: wrapperCacheGen, result });
|
|
356
|
-
return result;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* 渲染用 rowspan 修正:合并占位 tr 代表 N 个逻辑行,但在 DOM 中只算 1 行,
|
|
361
|
-
* 跨越它们的 rowspan 必须相应减少计数,否则浏览器会让单元格多跨 tr,
|
|
362
|
-
* 挤入后续未合并的行(上方区域表现为视口内错位,下方区域表现为吞掉 offsetBottom 占位)。
|
|
363
|
-
*
|
|
364
|
-
* - 上方合并段:位于锚点行之后、视口顶部之前。must-render 的 rowspan 必然延伸到视口,
|
|
365
|
-
* 因此锚点之后的每个段都完整位于该 span 的行范围内,每段扣减 (count - 1)。
|
|
366
|
-
* - 下方合并段:span 越过 viewportEndIndex 时,下方区域按跨界 rowspan 的结束行切分为
|
|
367
|
-
* 多个占位段(belowPhSegments),coveredRows 个逻辑行折叠为 segIdx 个占位 tr,
|
|
368
|
-
* 扣减 (coveredRows - segIdx),使单元格恰好止于包含其逻辑结束行的段边界。
|
|
369
|
-
*
|
|
370
|
-
* hiddenCellMap 等逻辑仍使用未修正的逻辑 rowspan,仅渲染属性需要修正。
|
|
371
|
-
*/
|
|
372
|
-
function adjustRowspanForMergedRows(absRowIndex: number, rowspan: number): number {
|
|
373
|
-
if (rowspan <= 1) return rowspan;
|
|
374
|
-
let adjusted = rowspan;
|
|
375
|
-
const spanEnd = absRowIndex + rowspan - 1;
|
|
376
|
-
|
|
377
|
-
const blocks = aboveEmptyBlocks.value;
|
|
378
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
379
|
-
const block = blocks[i];
|
|
380
|
-
if (block.start > absRowIndex && block.start <= spanEnd) {
|
|
381
|
-
adjusted -= block.count - 1;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const belowCount = belowViewportRowCount.value;
|
|
386
|
-
const { viewportEndIndex } = virtualScroll.value;
|
|
387
|
-
if (belowCount > 0 && spanEnd > viewportEndIndex) {
|
|
388
|
-
// 下方区域末端与 belowPhSegments 保持一致(钳制到窗口最后一行)
|
|
389
|
-
const regionEnd = viewportEndIndex + belowCount;
|
|
390
|
-
const coveredRows = Math.min(spanEnd, regionEnd) - viewportEndIndex;
|
|
391
|
-
adjusted -= coveredRows - getBelowSegmentIndex(spanEnd);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return adjusted > 0 ? adjusted : 1;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* spanEnd 落在下方区域第几个占位段(1 基)。
|
|
399
|
-
* 各段按跨界 rowspan 的去重结束行切分,单元格止于包含其逻辑结束行的段,
|
|
400
|
-
* 不同结束行的单元格因此止于不同的 tr,底边位置跨滚动帧保持稳定。
|
|
401
|
-
*/
|
|
402
|
-
function getBelowSegmentIndex(spanEnd: number): number {
|
|
403
|
-
const { viewportEndIndex } = virtualScroll.value;
|
|
404
|
-
const segments = belowPhSegments.value;
|
|
405
|
-
let acc = viewportEndIndex;
|
|
406
|
-
for (let i = 0; i < segments.length; i++) {
|
|
407
|
-
acc += segments[i].count;
|
|
408
|
-
if (spanEnd <= acc) return i + 1;
|
|
409
|
-
}
|
|
410
|
-
return segments.length || 1;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const emptySet = new Set<string>();
|
|
414
|
-
function updateHoverMergedCells(rowKey: UniqKey | undefined) {
|
|
415
|
-
hoverMergedCells.value = rowKey === void 0 ? emptySet : hoverRowMap.value[rowKey] || emptySet;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* For rows above the viewport (startIndex .. viewportStartIndex-1), compute a modified
|
|
420
|
-
* column list where non-must-render cells are replaced with placeholder entries.
|
|
421
|
-
*
|
|
422
|
-
* A cell is "must-render" if its rowspan crosses into the viewport:
|
|
423
|
-
* absRowIndex + rowspan > viewportStartIndex
|
|
424
|
-
*
|
|
425
|
-
* This computed reuses the shared mergeCellsCache so mergeCells callbacks are not called
|
|
426
|
-
* twice for the same cell.
|
|
427
|
-
*/
|
|
428
|
-
const aboveViewportColumnMap = computed(() => {
|
|
429
|
-
const map = new Map<UniqKey, AboveViewportCell[]>();
|
|
430
|
-
// autoRowHeight 时不允许剔除 td(行高靠内容撑开,见 StkTable getBodyColumns),直接返回空表
|
|
431
|
-
if (!canMergeEmptyRows.value) return map;
|
|
432
|
-
const data = virtual_dataSourcePart.value;
|
|
433
|
-
const { startIndex, viewportStartIndex } = virtualScroll.value;
|
|
434
|
-
const aboveCount = viewportStartIndex - startIndex;
|
|
435
|
-
if (aboveCount <= 0 || !data.length) return map;
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* 与视口行使用完全相同的列列表(virtual-x 模式下为可视列子集 + 固定列 + spacer,
|
|
439
|
-
* 非 virtual-x 时即 tableHeaderLast),保证视口上方行与视口行的列位置对齐。
|
|
440
|
-
*/
|
|
441
|
-
const columns = virtualX_columnPart.value;
|
|
442
|
-
/** Coverage grid: rowOffset -> Set of leafColIdx covered by rowspan from earlier rows */
|
|
443
|
-
const coverage = new Map<number, Set<number>>();
|
|
444
|
-
|
|
445
|
-
for (let rowOffset = 0; rowOffset < aboveCount; rowOffset++) {
|
|
446
|
-
const absRowIndex = startIndex + rowOffset;
|
|
447
|
-
const row = data[rowOffset];
|
|
448
|
-
if (!row) continue;
|
|
449
|
-
|
|
450
|
-
const rowCols: AboveViewportCell[] = [];
|
|
451
|
-
let hasMustRender = false;
|
|
452
|
-
|
|
453
|
-
for (let i = 0; i < columns.length; i++) {
|
|
454
|
-
const col = columns[i];
|
|
455
|
-
|
|
456
|
-
// virtual-x spacer 标记原样保留,保持与视口行一致的对齐结构
|
|
457
|
-
if (col.__VT_C_SP__) {
|
|
458
|
-
rowCols.push(col);
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// 叶子列索引:缓存键与 coverage 均使用叶子索引,与其他 mergeCells 调用处一致
|
|
463
|
-
const leafIndex = col.__LF_S__ ?? i;
|
|
464
|
-
|
|
465
|
-
// Skip positions covered by rowspan from earlier rows
|
|
466
|
-
if (coverage.get(rowOffset)?.has(leafIndex)) {
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
const { rowspan, colspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
|
|
471
|
-
|
|
472
|
-
// Check if this cell must be rendered (rowspan crosses into viewport)
|
|
473
|
-
const mustRender = absRowIndex + rowspan > viewportStartIndex;
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* 仅当合并单元格自身真实渲染(跨入视口)时,才标记其对下方行的覆盖。
|
|
477
|
-
* 未跨入视口的 rowspan 不会渲染(锚点行被占位/空行替代),被其覆盖的位置
|
|
478
|
-
* 实际上没有单元格占用,下方行必须保留自己的占位,否则后续单元格会整体错位。
|
|
479
|
-
*/
|
|
480
|
-
if (mustRender && rowspan > 1) {
|
|
481
|
-
for (let r = 1; r < rowspan; r++) {
|
|
482
|
-
const futureOffset = rowOffset + r;
|
|
483
|
-
if (futureOffset >= aboveCount) break;
|
|
484
|
-
if (!coverage.has(futureOffset)) coverage.set(futureOffset, new Set());
|
|
485
|
-
for (let c = 0; c < colspan; c++) {
|
|
486
|
-
coverage.get(futureOffset)!.add(leafIndex + c);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (mustRender) {
|
|
492
|
-
rowCols.push(col);
|
|
493
|
-
hasMustRender = true;
|
|
494
|
-
} else {
|
|
495
|
-
/**
|
|
496
|
-
* 占位逐列单独生成(colspan=1),不把多列合并成一个占位 td:
|
|
497
|
-
* 被占位的列状态并不相同——有的列是普通单元格,有的列是从上方 rowspan
|
|
498
|
-
* 继承的覆盖位。合并占位会破坏与视口行一致的“每列一个单元格”结构,
|
|
499
|
-
* 导致表格 auto layout 下列宽随滚动不断变化(抖动/闪烁)。
|
|
500
|
-
*/
|
|
501
|
-
for (let c = 0; c < colspan; c++) {
|
|
502
|
-
rowCols.push(ABOVE_VIEWPORT_PH);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Skip columns covered by this cell's colspan
|
|
507
|
-
// (列合并修正已保证被覆盖列在可视列列表中紧邻锚点列)
|
|
508
|
-
i += colspan - 1;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (!hasMustRender) {
|
|
512
|
-
// For rows without any must-render cells, no placeholder needed —
|
|
513
|
-
// tbody tr already has height: var(--row-height), so an empty <tr> maintains height.
|
|
514
|
-
rowCols.length = 0;
|
|
515
|
-
}
|
|
516
|
-
map.set(rowKeyGen(row), rowCols);
|
|
517
|
-
}
|
|
518
|
-
return map;
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
function updateActiveMergedCells(clear?: boolean, rowKey?: UniqKey) {
|
|
522
|
-
if (!rowActiveProp.value.enabled) return;
|
|
523
|
-
if (clear) {
|
|
524
|
-
activeMergedCells.value = new Set();
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
activeMergedCells.value = (rowKey !== void 0 && hoverRowMap.value[rowKey]) || new Set(hoverMergedCells.value);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return [
|
|
531
|
-
hiddenCellMap,
|
|
532
|
-
mergeCellsWrapper,
|
|
533
|
-
hoverMergedCells,
|
|
534
|
-
updateHoverMergedCells,
|
|
535
|
-
activeMergedCells,
|
|
536
|
-
updateActiveMergedCells,
|
|
537
|
-
aboveViewportColumnMap,
|
|
538
|
-
aboveEmptyBlocks,
|
|
539
|
-
belowPhSegments,
|
|
540
|
-
] as const;
|
|
541
|
-
}
|
|
1
|
+
import { ComputedRef, Ref, computed, ref, ShallowRef, watch } from 'vue';
|
|
2
|
+
import { MergeCellsCache } from './mergeCellsCache';
|
|
3
|
+
import { ColKeyGen, MergeCellsParam, PrivateRowDT, PrivateStkTableColumn, RowActiveOption, RowKeyGen, UniqKey } from './types';
|
|
4
|
+
import { pureCellKeyGen } from './utils';
|
|
5
|
+
import type { VirtualScrollStore } from './useVirtualScroll';
|
|
6
|
+
|
|
7
|
+
/** Above-viewport cell descriptor: real column or placeholder colspan */
|
|
8
|
+
type AboveViewportCell = PrivateStkTableColumn<PrivateRowDT> | { __VT_PH__: number };
|
|
9
|
+
|
|
10
|
+
/** 视口上方占位单元格(共享常量;模板仅读取 __VT_PH__ 属性、key 使用列下标,可安全复用同一对象) */
|
|
11
|
+
const ABOVE_VIEWPORT_PH: AboveViewportCell = { __VT_PH__: 1 };
|
|
12
|
+
|
|
13
|
+
export function useMergeCells(
|
|
14
|
+
rowActiveProp: Ref<RowActiveOption<any>>,
|
|
15
|
+
tableHeaderLast: ShallowRef<PrivateStkTableColumn<PrivateRowDT>[]>,
|
|
16
|
+
rowKeyGen: RowKeyGen,
|
|
17
|
+
colKeyGen: ColKeyGen,
|
|
18
|
+
virtual_dataSourcePart: ShallowRef<any[]>,
|
|
19
|
+
virtualScroll: Ref<VirtualScrollStore>,
|
|
20
|
+
/** 视口行渲染使用的列列表(virtual-x 时为可视列子集,否则即 tableHeaderLast) */
|
|
21
|
+
virtualX_columnPart: ComputedRef<PrivateStkTableColumn<PrivateRowDT>[]>,
|
|
22
|
+
/** 全量数据(仅用于缓存失效判定) */
|
|
23
|
+
dataSourceCopy: ShallowRef<any[]>,
|
|
24
|
+
/** mergeCells 结果共享缓存(与 useVirtualScroll 共用,可跨滚动帧复用) */
|
|
25
|
+
mergeCellsCache: MergeCellsCache,
|
|
26
|
+
/** 是否允许把连续空行合并为占位 tr(virtual && !autoRowHeight) */
|
|
27
|
+
canMergeEmptyRows: Ref<boolean>,
|
|
28
|
+
/** 视口下方合并为占位 tr 的行数(渲染 rowspan 属性修正用) */
|
|
29
|
+
belowViewportRowCount: Ref<number>,
|
|
30
|
+
) {
|
|
31
|
+
/**
|
|
32
|
+
* which cell need be hidden
|
|
33
|
+
* - key: rowKey
|
|
34
|
+
* - value: colKey Set
|
|
35
|
+
*/
|
|
36
|
+
const hiddenCellMap = ref<Record<UniqKey, Set<UniqKey>> | null>(null);
|
|
37
|
+
/**
|
|
38
|
+
* hover other row and rowspan cell should be highlighted
|
|
39
|
+
* - key: rowKey
|
|
40
|
+
* - value: cellKey Set
|
|
41
|
+
*/
|
|
42
|
+
const hoverRowMap = ref<Record<UniqKey, Set<string>>>({});
|
|
43
|
+
|
|
44
|
+
/** hover current row , which rowspan cells should be highlight */
|
|
45
|
+
const hoverMergedCells = ref(new Set<string>());
|
|
46
|
+
/** click current row , which rowspan cells should be highlight */
|
|
47
|
+
const activeMergedCells = ref(new Set<string>());
|
|
48
|
+
|
|
49
|
+
/** column index cache */
|
|
50
|
+
let colIndexCache: Map<UniqKey, number> | null = null;
|
|
51
|
+
/** 空合并段列表(start 为绝对行索引)。禁止合并时返回共享空数组 */
|
|
52
|
+
const EMPTY_BLOCKS: { start: number; count: number }[] = [];
|
|
53
|
+
|
|
54
|
+
/** 空下方占位段列表。无下方合并行时返回共享空数组 */
|
|
55
|
+
const EMPTY_SEGMENTS: { count: number }[] = [];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* mergeCellsWrapper 返回值记忆化缓存(WeakMap<行对象, Map<colKey, 结果>>)。
|
|
59
|
+
* 签名 = 绝对行索引 + 代际(wrapperCacheGen):窗口/列/视口修正(上方空行段、
|
|
60
|
+
* 下方占位段、viewportEndIndex、下方行数)任一变化即整体失效。
|
|
61
|
+
*/
|
|
62
|
+
const wrapperCache = new WeakMap<
|
|
63
|
+
object,
|
|
64
|
+
Map<UniqKey, { absRowIndex: number; gen: number; result: { colspan?: number; rowspan?: number } | undefined }>
|
|
65
|
+
>();
|
|
66
|
+
|
|
67
|
+
/** 是否存在合并列(仅依赖列配置,跨滚动帧缓存,不随数据/滚动重算) */
|
|
68
|
+
const hasMergeColumn = computed(() => tableHeaderLast.value.some(col => !!col.mergeCells));
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 视口上方连续「无 td」空行段(行数 >= 2 才成段;单行保留独立 tr)。
|
|
72
|
+
* 空行判定与渲染保持一致:aboveViewportColumnMap 中列结果为空且非展开行。
|
|
73
|
+
* 供模板合并渲染(StkTable aboveRenderParts)与 rowspan 属性修正共用。
|
|
74
|
+
*/
|
|
75
|
+
const aboveEmptyBlocks = computed<{ start: number; count: number }[]>(() => {
|
|
76
|
+
if (!canMergeEmptyRows.value) return EMPTY_BLOCKS;
|
|
77
|
+
const data = virtual_dataSourcePart.value;
|
|
78
|
+
const { startIndex, viewportStartIndex } = virtualScroll.value;
|
|
79
|
+
const aboveCount = Math.min(data.length, Math.max(0, viewportStartIndex - startIndex));
|
|
80
|
+
if (aboveCount <= 0) return EMPTY_BLOCKS;
|
|
81
|
+
const colMap = aboveViewportColumnMap.value;
|
|
82
|
+
|
|
83
|
+
const isEmptyRow = (row: PrivateRowDT): boolean => {
|
|
84
|
+
// 展开行有独立行高且渲染内容,不能参与合并
|
|
85
|
+
if (!row || row.__EXP_R__) return false;
|
|
86
|
+
const cols = colMap.get(rowKeyGen(row));
|
|
87
|
+
// 仅当映射中明确为空列结果时才算空行;缺失(异常)时保留原渲染
|
|
88
|
+
return cols !== void 0 && cols.length === 0;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const blocks: { start: number; count: number }[] = [];
|
|
92
|
+
let runStart = -1;
|
|
93
|
+
const flushRun = (endExclusive: number) => {
|
|
94
|
+
if (endExclusive - runStart >= 2) {
|
|
95
|
+
blocks.push({ start: startIndex + runStart, count: endExclusive - runStart });
|
|
96
|
+
}
|
|
97
|
+
runStart = -1;
|
|
98
|
+
};
|
|
99
|
+
for (let i = 0; i < aboveCount; i++) {
|
|
100
|
+
if (isEmptyRow(data[i])) {
|
|
101
|
+
if (runStart < 0) runStart = i;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (runStart >= 0) flushRun(i);
|
|
105
|
+
}
|
|
106
|
+
if (runStart >= 0) flushRun(aboveCount);
|
|
107
|
+
return blocks.length ? blocks : EMPTY_BLOCKS;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 视口下方占位段:把 [viewportEndIndex+1, endIndex] 按跨界 rowspan 的
|
|
112
|
+
* 去重结束行切分为多段,每段渲染一个占位 tr。
|
|
113
|
+
*
|
|
114
|
+
* 单个占位 tr 无法表达多个 rowspan 的不同逻辑结束位置(不规律合并下
|
|
115
|
+
* 多个单元格会塌缩到同一 tr 底边,滚动时高度跳动);按结束行切段后,
|
|
116
|
+
* 每个单元格的修正 rowspan 恰好止于包含其逻辑结束行的段。
|
|
117
|
+
* 超长 rowspan 场景只有一个结束行(= 区域末端),仍只产生 1 段,不增加 DOM。
|
|
118
|
+
*/
|
|
119
|
+
const belowPhSegments = computed<{ count: number }[]>(() => {
|
|
120
|
+
const belowCount = belowViewportRowCount.value;
|
|
121
|
+
if (belowCount <= 0) return EMPTY_SEGMENTS;
|
|
122
|
+
const data = virtual_dataSourcePart.value;
|
|
123
|
+
const { startIndex, viewportEndIndex, endIndex } = virtualScroll.value;
|
|
124
|
+
// 下方区域末端钳制到窗口最后一行(endIndex 可能超出数据长度)
|
|
125
|
+
const regionEnd = Math.min(endIndex, startIndex + data.length - 1);
|
|
126
|
+
const columns = virtualX_columnPart.value;
|
|
127
|
+
|
|
128
|
+
// 跨界 rowspan 的锚点只可能在视口上方保留行与视口行内,收集其去重结束行
|
|
129
|
+
const endsSet = new Set<number>();
|
|
130
|
+
const anchorRowEnd = Math.min(data.length - 1, viewportEndIndex - startIndex);
|
|
131
|
+
for (let rowOffset = 0; rowOffset <= anchorRowEnd; rowOffset++) {
|
|
132
|
+
const row = data[rowOffset];
|
|
133
|
+
if (!row) continue;
|
|
134
|
+
const absRowIndex = startIndex + rowOffset;
|
|
135
|
+
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
136
|
+
const col = columns[colIdx];
|
|
137
|
+
if (!col.mergeCells) continue;
|
|
138
|
+
const leafIndex = col.__LF_S__ ?? colIdx;
|
|
139
|
+
const { rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
|
|
140
|
+
if (rowspan <= 1) continue;
|
|
141
|
+
const spanEnd = absRowIndex + rowspan - 1;
|
|
142
|
+
if (spanEnd > viewportEndIndex) endsSet.add(Math.min(spanEnd, regionEnd));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 兜底:未收集到跨界结束行时保持单段(与合并前行为一致)
|
|
147
|
+
if (!endsSet.size) return [{ count: belowCount }];
|
|
148
|
+
|
|
149
|
+
const ends = Array.from(endsSet).sort((a, b) => a - b);
|
|
150
|
+
const segments: { count: number }[] = [];
|
|
151
|
+
let prev = viewportEndIndex;
|
|
152
|
+
for (let i = 0; i < ends.length; i++) {
|
|
153
|
+
const count = ends[i] - prev;
|
|
154
|
+
if (count > 0) segments.push({ count });
|
|
155
|
+
prev = ends[i];
|
|
156
|
+
}
|
|
157
|
+
if (regionEnd > prev) segments.push({ count: regionEnd - prev });
|
|
158
|
+
return segments;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
let wrapperCacheGen = 0;
|
|
162
|
+
|
|
163
|
+
watch(
|
|
164
|
+
[
|
|
165
|
+
virtual_dataSourcePart,
|
|
166
|
+
tableHeaderLast,
|
|
167
|
+
aboveEmptyBlocks,
|
|
168
|
+
belowPhSegments,
|
|
169
|
+
() => virtualScroll.value.viewportEndIndex,
|
|
170
|
+
belowViewportRowCount,
|
|
171
|
+
],
|
|
172
|
+
() => {
|
|
173
|
+
wrapperCacheGen++;
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 数据/列真正变化时才清空 mergeCells 结果缓存。
|
|
179
|
+
* 缓存键为绝对索引且命中时校验行引用同一性,滚动换窗(virtual_dataSourcePart 变化)
|
|
180
|
+
* 无需清缓存——相邻帧高度重叠,复用缓存可避免对同一单元格重复调用用户回调。
|
|
181
|
+
* 行字段被原地修改(引用不变)时不会触发本 watch,需由外部调用暴露的
|
|
182
|
+
* clearMergeCellsCache 实例方法显式失效缓存并重算。
|
|
183
|
+
*/
|
|
184
|
+
watch([dataSourceCopy, tableHeaderLast], () => {
|
|
185
|
+
mergeCellsCache.clear();
|
|
186
|
+
colIndexCache = null;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
watch([virtual_dataSourcePart, tableHeaderLast], () => {
|
|
190
|
+
buildHiddenCellMap();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Pre-build hiddenCellMap and hoverRowMap before rendering to avoid
|
|
195
|
+
* a two-render cycle (null → populated) that causes visual flicker.
|
|
196
|
+
*
|
|
197
|
+
* Previously, the watch set hiddenCellMap.value = null, and the map was
|
|
198
|
+
* rebuilt during rendering by mergeCellsWrapper → hideCells. This meant
|
|
199
|
+
* the first render after scroll had no hidden cells (all visible), and
|
|
200
|
+
* a second render was needed to apply merges — causing flicker/jumping.
|
|
201
|
+
*/
|
|
202
|
+
function buildHiddenCellMap() {
|
|
203
|
+
/**
|
|
204
|
+
* 无合并列快速通道:mergeCells 是可选特性,多数表格没有合并单元格,
|
|
205
|
+
* 不应为此空转「窗口行数 × 列数」的扫描和每帧的 ref 重赋值。
|
|
206
|
+
* 仅在「有 → 无」切换时重置一次(清掉残留的隐藏/高亮状态),之后滚动帧直接返回。
|
|
207
|
+
*/
|
|
208
|
+
if (!hasMergeColumn.value) {
|
|
209
|
+
if (hiddenCellMap.value) {
|
|
210
|
+
hiddenCellMap.value = null;
|
|
211
|
+
hoverRowMap.value = {};
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
hiddenCellMap.value = {};
|
|
217
|
+
hoverRowMap.value = {};
|
|
218
|
+
|
|
219
|
+
const data = virtual_dataSourcePart.value;
|
|
220
|
+
/**
|
|
221
|
+
* 只遍历实际渲染的列:virtual-x 模式下即视列子集 + 固定列,
|
|
222
|
+
* 非 virtual-x 时即 tableHeaderLast。
|
|
223
|
+
* hiddenCellMap/hoverRowMap 只会被渲染出来的 td 消费(shouldHideCell、hover 高亮),
|
|
224
|
+
* 未渲染列的合并无需计算;colspan 锚点列已被可视列范围修正保证包含在列表内。
|
|
225
|
+
*/
|
|
226
|
+
const columns = virtualX_columnPart.value;
|
|
227
|
+
/**
|
|
228
|
+
* mergeCells 入参与缓存键统一使用绝对行索引(不随虚拟窗口滑动变化)。
|
|
229
|
+
* 此前这里使用切片内相对索引,而 aboveViewportColumnMap 使用绝对索引,
|
|
230
|
+
* 二者共用 mergeCellsCache 时会发生键冲突,导致取到错误行的合并结果
|
|
231
|
+
* (表现为滚动时视口上方合并单元格锚点丢失/闪烁)。
|
|
232
|
+
*/
|
|
233
|
+
const baseRowIndex = virtualScroll.value.startIndex;
|
|
234
|
+
|
|
235
|
+
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
|
236
|
+
const row = data[rowIndex];
|
|
237
|
+
if (!row) continue;
|
|
238
|
+
|
|
239
|
+
const absRowIndex = baseRowIndex + rowIndex;
|
|
240
|
+
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
241
|
+
const col = columns[colIdx];
|
|
242
|
+
if (!col.mergeCells) continue;
|
|
243
|
+
|
|
244
|
+
// 叶子列索引:与 mergeCellsWrapper / aboveViewportColumnMap / buildColMergeRange 的缓存键一致
|
|
245
|
+
const leafIndex = col.__LF_S__ ?? colIdx;
|
|
246
|
+
const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
|
|
247
|
+
if (colspan === 1 && rowspan === 1) continue;
|
|
248
|
+
|
|
249
|
+
const rowKey = rowKeyGen(row);
|
|
250
|
+
const colKey = colKeyGen.value(col);
|
|
251
|
+
const mergedCellKey = pureCellKeyGen(rowKey, colKey);
|
|
252
|
+
|
|
253
|
+
for (let i = rowIndex; i < rowIndex + rowspan; i++) {
|
|
254
|
+
const targetRow = data[i];
|
|
255
|
+
if (!targetRow) break;
|
|
256
|
+
hideCells(rowKeyGen(targetRow), colKey, colspan, i === rowIndex, mergedCellKey);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* abstract the logic of hiding cells
|
|
264
|
+
*/
|
|
265
|
+
function hideCells(rowKey: UniqKey, colKey: UniqKey, colspan: number, isSelfRow = false, mergeCellKey: string) {
|
|
266
|
+
const headers = tableHeaderLast.value;
|
|
267
|
+
const colKeyGenValue = colKeyGen.value;
|
|
268
|
+
|
|
269
|
+
// use columns cache to avoid repeat findIndex
|
|
270
|
+
let startIndex = colIndexCache?.get(colKey);
|
|
271
|
+
if (startIndex === void 0) {
|
|
272
|
+
startIndex = headers.findIndex(item => colKeyGenValue(item) === colKey);
|
|
273
|
+
if (startIndex < 0) return;
|
|
274
|
+
|
|
275
|
+
if (!colIndexCache) colIndexCache = new Map();
|
|
276
|
+
colIndexCache.set(colKey, startIndex);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Initialize maps if needed
|
|
280
|
+
if (!hoverRowMap.value[rowKey]) {
|
|
281
|
+
hoverRowMap.value[rowKey] = new Set();
|
|
282
|
+
}
|
|
283
|
+
if (!hiddenCellMap.value) {
|
|
284
|
+
hiddenCellMap.value = {};
|
|
285
|
+
}
|
|
286
|
+
if (!hiddenCellMap.value[rowKey]) {
|
|
287
|
+
hiddenCellMap.value[rowKey] = new Set();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const hoverSet = hoverRowMap.value[rowKey];
|
|
291
|
+
const hiddenSet = hiddenCellMap.value[rowKey];
|
|
292
|
+
const endIndex = Math.min(startIndex + colspan, headers.length);
|
|
293
|
+
|
|
294
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
295
|
+
hoverSet.add(mergeCellKey);
|
|
296
|
+
|
|
297
|
+
if (isSelfRow && i === startIndex) {
|
|
298
|
+
// self row start cell does not need to be hidden
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const nextCol = headers[i];
|
|
303
|
+
if (!nextCol) break;
|
|
304
|
+
|
|
305
|
+
const nextColKey = colKeyGenValue(nextCol);
|
|
306
|
+
hiddenSet.add(nextColKey);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* calculate colspan and rowspan
|
|
312
|
+
* @param row
|
|
313
|
+
* @param col
|
|
314
|
+
* @param rowIndex
|
|
315
|
+
* @param colIndex
|
|
316
|
+
* @returns
|
|
317
|
+
*/
|
|
318
|
+
function mergeCellsWrapper(
|
|
319
|
+
row: MergeCellsParam<any>['row'],
|
|
320
|
+
col: MergeCellsParam<any>['col'],
|
|
321
|
+
rowIndex: MergeCellsParam<any>['rowIndex'],
|
|
322
|
+
colIndex: MergeCellsParam<any>['colIndex'],
|
|
323
|
+
): { colspan?: number; rowspan?: number } | undefined {
|
|
324
|
+
if (!col.mergeCells) return;
|
|
325
|
+
|
|
326
|
+
// 渲染传入的是切片内相对索引,统一换算为绝对行索引后再调用 mergeCells,
|
|
327
|
+
// 保证与 buildHiddenCellMap / aboveViewportColumnMap 的入参及缓存键一致
|
|
328
|
+
const absRowIndex = virtualScroll.value.startIndex + rowIndex;
|
|
329
|
+
const colKey = colKeyGen.value(col);
|
|
330
|
+
|
|
331
|
+
// 返回值记忆化:命中时跳过 getMergeCellsResult、rowspan 修正循环与对象分配。
|
|
332
|
+
// hideCells 为幂等写入(buildHiddenCellMap 已在渲染前按相同输入构建),命中时跳过安全。
|
|
333
|
+
let rowCache = wrapperCache.get(row);
|
|
334
|
+
if (!rowCache) {
|
|
335
|
+
rowCache = new Map();
|
|
336
|
+
wrapperCache.set(row, rowCache);
|
|
337
|
+
}
|
|
338
|
+
const hit = rowCache.get(colKey);
|
|
339
|
+
if (hit && hit.absRowIndex === absRowIndex && hit.gen === wrapperCacheGen) return hit.result;
|
|
340
|
+
|
|
341
|
+
const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, colIndex);
|
|
342
|
+
|
|
343
|
+
let result: { colspan?: number; rowspan?: number } | undefined;
|
|
344
|
+
if (colspan === 1 && rowspan === 1) {
|
|
345
|
+
result = void 0;
|
|
346
|
+
} else {
|
|
347
|
+
const mergedCellKey = pureCellKeyGen(rowKeyGen(row), colKey);
|
|
348
|
+
for (let i = rowIndex; i < rowIndex + rowspan; i++) {
|
|
349
|
+
const targetRow = virtual_dataSourcePart.value[i];
|
|
350
|
+
if (!targetRow) break;
|
|
351
|
+
hideCells(rowKeyGen(targetRow), colKey, colspan, i === rowIndex, mergedCellKey);
|
|
352
|
+
}
|
|
353
|
+
result = { colspan, rowspan: adjustRowspanForMergedRows(absRowIndex, rowspan) };
|
|
354
|
+
}
|
|
355
|
+
rowCache.set(colKey, { absRowIndex, gen: wrapperCacheGen, result });
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 渲染用 rowspan 修正:合并占位 tr 代表 N 个逻辑行,但在 DOM 中只算 1 行,
|
|
361
|
+
* 跨越它们的 rowspan 必须相应减少计数,否则浏览器会让单元格多跨 tr,
|
|
362
|
+
* 挤入后续未合并的行(上方区域表现为视口内错位,下方区域表现为吞掉 offsetBottom 占位)。
|
|
363
|
+
*
|
|
364
|
+
* - 上方合并段:位于锚点行之后、视口顶部之前。must-render 的 rowspan 必然延伸到视口,
|
|
365
|
+
* 因此锚点之后的每个段都完整位于该 span 的行范围内,每段扣减 (count - 1)。
|
|
366
|
+
* - 下方合并段:span 越过 viewportEndIndex 时,下方区域按跨界 rowspan 的结束行切分为
|
|
367
|
+
* 多个占位段(belowPhSegments),coveredRows 个逻辑行折叠为 segIdx 个占位 tr,
|
|
368
|
+
* 扣减 (coveredRows - segIdx),使单元格恰好止于包含其逻辑结束行的段边界。
|
|
369
|
+
*
|
|
370
|
+
* hiddenCellMap 等逻辑仍使用未修正的逻辑 rowspan,仅渲染属性需要修正。
|
|
371
|
+
*/
|
|
372
|
+
function adjustRowspanForMergedRows(absRowIndex: number, rowspan: number): number {
|
|
373
|
+
if (rowspan <= 1) return rowspan;
|
|
374
|
+
let adjusted = rowspan;
|
|
375
|
+
const spanEnd = absRowIndex + rowspan - 1;
|
|
376
|
+
|
|
377
|
+
const blocks = aboveEmptyBlocks.value;
|
|
378
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
379
|
+
const block = blocks[i];
|
|
380
|
+
if (block.start > absRowIndex && block.start <= spanEnd) {
|
|
381
|
+
adjusted -= block.count - 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const belowCount = belowViewportRowCount.value;
|
|
386
|
+
const { viewportEndIndex } = virtualScroll.value;
|
|
387
|
+
if (belowCount > 0 && spanEnd > viewportEndIndex) {
|
|
388
|
+
// 下方区域末端与 belowPhSegments 保持一致(钳制到窗口最后一行)
|
|
389
|
+
const regionEnd = viewportEndIndex + belowCount;
|
|
390
|
+
const coveredRows = Math.min(spanEnd, regionEnd) - viewportEndIndex;
|
|
391
|
+
adjusted -= coveredRows - getBelowSegmentIndex(spanEnd);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return adjusted > 0 ? adjusted : 1;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* spanEnd 落在下方区域第几个占位段(1 基)。
|
|
399
|
+
* 各段按跨界 rowspan 的去重结束行切分,单元格止于包含其逻辑结束行的段,
|
|
400
|
+
* 不同结束行的单元格因此止于不同的 tr,底边位置跨滚动帧保持稳定。
|
|
401
|
+
*/
|
|
402
|
+
function getBelowSegmentIndex(spanEnd: number): number {
|
|
403
|
+
const { viewportEndIndex } = virtualScroll.value;
|
|
404
|
+
const segments = belowPhSegments.value;
|
|
405
|
+
let acc = viewportEndIndex;
|
|
406
|
+
for (let i = 0; i < segments.length; i++) {
|
|
407
|
+
acc += segments[i].count;
|
|
408
|
+
if (spanEnd <= acc) return i + 1;
|
|
409
|
+
}
|
|
410
|
+
return segments.length || 1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const emptySet = new Set<string>();
|
|
414
|
+
function updateHoverMergedCells(rowKey: UniqKey | undefined) {
|
|
415
|
+
hoverMergedCells.value = rowKey === void 0 ? emptySet : hoverRowMap.value[rowKey] || emptySet;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* For rows above the viewport (startIndex .. viewportStartIndex-1), compute a modified
|
|
420
|
+
* column list where non-must-render cells are replaced with placeholder entries.
|
|
421
|
+
*
|
|
422
|
+
* A cell is "must-render" if its rowspan crosses into the viewport:
|
|
423
|
+
* absRowIndex + rowspan > viewportStartIndex
|
|
424
|
+
*
|
|
425
|
+
* This computed reuses the shared mergeCellsCache so mergeCells callbacks are not called
|
|
426
|
+
* twice for the same cell.
|
|
427
|
+
*/
|
|
428
|
+
const aboveViewportColumnMap = computed(() => {
|
|
429
|
+
const map = new Map<UniqKey, AboveViewportCell[]>();
|
|
430
|
+
// autoRowHeight 时不允许剔除 td(行高靠内容撑开,见 StkTable getBodyColumns),直接返回空表
|
|
431
|
+
if (!canMergeEmptyRows.value) return map;
|
|
432
|
+
const data = virtual_dataSourcePart.value;
|
|
433
|
+
const { startIndex, viewportStartIndex } = virtualScroll.value;
|
|
434
|
+
const aboveCount = viewportStartIndex - startIndex;
|
|
435
|
+
if (aboveCount <= 0 || !data.length) return map;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 与视口行使用完全相同的列列表(virtual-x 模式下为可视列子集 + 固定列 + spacer,
|
|
439
|
+
* 非 virtual-x 时即 tableHeaderLast),保证视口上方行与视口行的列位置对齐。
|
|
440
|
+
*/
|
|
441
|
+
const columns = virtualX_columnPart.value;
|
|
442
|
+
/** Coverage grid: rowOffset -> Set of leafColIdx covered by rowspan from earlier rows */
|
|
443
|
+
const coverage = new Map<number, Set<number>>();
|
|
444
|
+
|
|
445
|
+
for (let rowOffset = 0; rowOffset < aboveCount; rowOffset++) {
|
|
446
|
+
const absRowIndex = startIndex + rowOffset;
|
|
447
|
+
const row = data[rowOffset];
|
|
448
|
+
if (!row) continue;
|
|
449
|
+
|
|
450
|
+
const rowCols: AboveViewportCell[] = [];
|
|
451
|
+
let hasMustRender = false;
|
|
452
|
+
|
|
453
|
+
for (let i = 0; i < columns.length; i++) {
|
|
454
|
+
const col = columns[i];
|
|
455
|
+
|
|
456
|
+
// virtual-x spacer 标记原样保留,保持与视口行一致的对齐结构
|
|
457
|
+
if (col.__VT_C_SP__) {
|
|
458
|
+
rowCols.push(col);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 叶子列索引:缓存键与 coverage 均使用叶子索引,与其他 mergeCells 调用处一致
|
|
463
|
+
const leafIndex = col.__LF_S__ ?? i;
|
|
464
|
+
|
|
465
|
+
// Skip positions covered by rowspan from earlier rows
|
|
466
|
+
if (coverage.get(rowOffset)?.has(leafIndex)) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const { rowspan, colspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
|
|
471
|
+
|
|
472
|
+
// Check if this cell must be rendered (rowspan crosses into viewport)
|
|
473
|
+
const mustRender = absRowIndex + rowspan > viewportStartIndex;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 仅当合并单元格自身真实渲染(跨入视口)时,才标记其对下方行的覆盖。
|
|
477
|
+
* 未跨入视口的 rowspan 不会渲染(锚点行被占位/空行替代),被其覆盖的位置
|
|
478
|
+
* 实际上没有单元格占用,下方行必须保留自己的占位,否则后续单元格会整体错位。
|
|
479
|
+
*/
|
|
480
|
+
if (mustRender && rowspan > 1) {
|
|
481
|
+
for (let r = 1; r < rowspan; r++) {
|
|
482
|
+
const futureOffset = rowOffset + r;
|
|
483
|
+
if (futureOffset >= aboveCount) break;
|
|
484
|
+
if (!coverage.has(futureOffset)) coverage.set(futureOffset, new Set());
|
|
485
|
+
for (let c = 0; c < colspan; c++) {
|
|
486
|
+
coverage.get(futureOffset)!.add(leafIndex + c);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (mustRender) {
|
|
492
|
+
rowCols.push(col);
|
|
493
|
+
hasMustRender = true;
|
|
494
|
+
} else {
|
|
495
|
+
/**
|
|
496
|
+
* 占位逐列单独生成(colspan=1),不把多列合并成一个占位 td:
|
|
497
|
+
* 被占位的列状态并不相同——有的列是普通单元格,有的列是从上方 rowspan
|
|
498
|
+
* 继承的覆盖位。合并占位会破坏与视口行一致的“每列一个单元格”结构,
|
|
499
|
+
* 导致表格 auto layout 下列宽随滚动不断变化(抖动/闪烁)。
|
|
500
|
+
*/
|
|
501
|
+
for (let c = 0; c < colspan; c++) {
|
|
502
|
+
rowCols.push(ABOVE_VIEWPORT_PH);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Skip columns covered by this cell's colspan
|
|
507
|
+
// (列合并修正已保证被覆盖列在可视列列表中紧邻锚点列)
|
|
508
|
+
i += colspan - 1;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!hasMustRender) {
|
|
512
|
+
// For rows without any must-render cells, no placeholder needed —
|
|
513
|
+
// tbody tr already has height: var(--row-height), so an empty <tr> maintains height.
|
|
514
|
+
rowCols.length = 0;
|
|
515
|
+
}
|
|
516
|
+
map.set(rowKeyGen(row), rowCols);
|
|
517
|
+
}
|
|
518
|
+
return map;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
function updateActiveMergedCells(clear?: boolean, rowKey?: UniqKey) {
|
|
522
|
+
if (!rowActiveProp.value.enabled) return;
|
|
523
|
+
if (clear) {
|
|
524
|
+
activeMergedCells.value = new Set();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
activeMergedCells.value = (rowKey !== void 0 && hoverRowMap.value[rowKey]) || new Set(hoverMergedCells.value);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return [
|
|
531
|
+
hiddenCellMap,
|
|
532
|
+
mergeCellsWrapper,
|
|
533
|
+
hoverMergedCells,
|
|
534
|
+
updateHoverMergedCells,
|
|
535
|
+
activeMergedCells,
|
|
536
|
+
updateActiveMergedCells,
|
|
537
|
+
aboveViewportColumnMap,
|
|
538
|
+
aboveEmptyBlocks,
|
|
539
|
+
belowPhSegments,
|
|
540
|
+
] as const;
|
|
541
|
+
}
|