stk-table-vue 1.2.1 → 1.2.2
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/lib/{Dropdown-Bq4DxXYJ.js → Dropdown-D40QlY91.js} +2 -2
- package/lib/{StkTable-yOz9wkCz.js → StkTable-g_qwf6BE.js} +180 -124
- package/lib/src/StkTable/useSorter.d.ts +2 -1
- package/lib/src/StkTable/useVirtualScroll.d.ts +11 -2
- package/lib/stk-table-vue.js +3 -3
- package/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/StkTable/StkTable.vue +19 -7
- package/src/StkTable/useFixedStyle.ts +2 -2
- package/src/StkTable/useMergeCells.ts +149 -111
- package/src/StkTable/useSorter.ts +9 -1
- package/src/StkTable/useVirtualScroll.ts +38 -11
- package/lib/src/StkTable/features/useAreaSelection.bak.d.ts +0 -25
- package/lib/test/StkTable.browser.test.d.ts +0 -1
- package/lib/test/autoHeightStripe.repro.test.d.ts +0 -1
- package/lib/test/autoRowHeightVirtualScroll.test.d.ts +0 -1
- package/lib/test/defaultSort.test.d.ts +0 -1
- package/lib/test/insertToOrderedArray.test.d.ts +0 -1
- package/lib/test/perf/scrollPerf.test.d.ts +0 -1
- package/lib/test/rowMergeVirtualScroll.repro.test.d.ts +0 -1
- package/lib/test/scrollTo.test.d.ts +0 -1
- package/lib/test/setSorter.test.d.ts +0 -1
- package/lib/test/setTreeExpandParents.test.d.ts +0 -1
- package/lib/test/trReuse.repro.test.d.ts +0 -1
- package/lib/test/utils/DragResize.d.ts +0 -28
- package/lib/test/utils/h.d.ts +0 -10
- package/lib/test/virtualXMerge.test.d.ts +0 -1
- package/src/StkTable/features/useAreaSelection.bak.ts +0 -951
|
@@ -882,6 +882,7 @@ const [sortStates, sortCol, onColumnSort, setSorter, resetSorter, getSortColumns
|
|
|
882
882
|
tableHeaderLast,
|
|
883
883
|
dataSourceCopy,
|
|
884
884
|
initDataSource,
|
|
885
|
+
onDataSourceChange,
|
|
885
886
|
);
|
|
886
887
|
|
|
887
888
|
const [isSRBRActive] = useScrollRowByRow(props, tableContainerRef);
|
|
@@ -936,6 +937,7 @@ const [
|
|
|
936
937
|
getMaxRowSpanValue,
|
|
937
938
|
scrollbarOptions,
|
|
938
939
|
isExperimentalScrollY,
|
|
940
|
+
isRelativeMode,
|
|
939
941
|
mergeCellsCache,
|
|
940
942
|
);
|
|
941
943
|
|
|
@@ -1219,6 +1221,8 @@ onMounted(() => {
|
|
|
1219
1221
|
});
|
|
1220
1222
|
|
|
1221
1223
|
async function onDataSourceChange() {
|
|
1224
|
+
// 数据顺序/行数变化后重算 rowspan 映射(排序后 mergeCells 的 rowIndex 会变化)
|
|
1225
|
+
updateMaxRowSpan();
|
|
1222
1226
|
await nextTick();
|
|
1223
1227
|
initVirtualScrollY();
|
|
1224
1228
|
updateCustomScrollbar();
|
|
@@ -1250,6 +1254,9 @@ function setFilter(
|
|
|
1250
1254
|
filterStatus.value = status;
|
|
1251
1255
|
if (!option?.remote) {
|
|
1252
1256
|
initDataSource();
|
|
1257
|
+
// #80:筛选改变 dataSourceCopy 后重算虚拟滚动窗口,
|
|
1258
|
+
// 否则残留 startIndex/endIndex/scrollHeight 会导致只渲染 1 行或空白视口
|
|
1259
|
+
onDataSourceChange();
|
|
1253
1260
|
}
|
|
1254
1261
|
if (!option?.silent) {
|
|
1255
1262
|
emits('filter-change', status);
|
|
@@ -1525,14 +1532,16 @@ function getTHProps(col: PrivateStkTableColumn<DT>) {
|
|
|
1525
1532
|
colspan: col.__C_SP__,
|
|
1526
1533
|
style: cellStyleMap.value[TagType.TH].get(colKey),
|
|
1527
1534
|
title: getHeaderTitle(col),
|
|
1535
|
+
// class 用预拼接字符串(而非数组),降低每格 vnode diff 与 GC 开销
|
|
1528
1536
|
class: [
|
|
1529
1537
|
col.sorter ? 'sortable' : '',
|
|
1530
|
-
isSorted
|
|
1538
|
+
isSorted ? 'sorter-' + sortState?.order : '',
|
|
1531
1539
|
col.headerClassName,
|
|
1532
1540
|
fixedColClassMap.value.get(colKey),
|
|
1533
|
-
col.headerAlign
|
|
1534
|
-
|
|
1535
|
-
|
|
1541
|
+
col.headerAlign === 'left' ? 'text-l' : col.headerAlign === 'right' ? 'text-r' : col.headerAlign === 'center' ? 'text-c' : '',
|
|
1542
|
+
]
|
|
1543
|
+
.filter(Boolean)
|
|
1544
|
+
.join(' '),
|
|
1536
1545
|
};
|
|
1537
1546
|
}
|
|
1538
1547
|
|
|
@@ -1546,7 +1555,9 @@ function getTFProps(col: StkTableColumn<DT>) {
|
|
|
1546
1555
|
fixedColClassMap.value.get(colKey),
|
|
1547
1556
|
col.type === 'seq' ? 'seq-column' : '',
|
|
1548
1557
|
col.align === 'center' ? 'text-c' : col.align === 'right' ? 'text-r' : '',
|
|
1549
|
-
]
|
|
1558
|
+
]
|
|
1559
|
+
.filter(Boolean)
|
|
1560
|
+
.join(' '),
|
|
1550
1561
|
};
|
|
1551
1562
|
}
|
|
1552
1563
|
|
|
@@ -1596,7 +1607,7 @@ function getTDProps(row: PrivateRowDT | null | undefined, col: StkTableColumn<Pr
|
|
|
1596
1607
|
return {
|
|
1597
1608
|
'data-col-key': colKey,
|
|
1598
1609
|
style: cellStyleMap.value[TagType.TD].get(colKey),
|
|
1599
|
-
class: classList,
|
|
1610
|
+
class: classList.filter(Boolean).join(' '),
|
|
1600
1611
|
...mergeCellsWrapper(row, col, rowIndex, (col as PrivateStkTableColumn<DT>).__LF_S__ ?? 0),
|
|
1601
1612
|
};
|
|
1602
1613
|
}
|
|
@@ -2090,7 +2101,8 @@ function scrollTo(options?: ScrollToOptions | number | null, leftArg?: number |
|
|
|
2090
2101
|
if (top === null && left === null) return;
|
|
2091
2102
|
// 钳制到合法滚动范围(基于理论内容尺寸,与滚动条语义一致)
|
|
2092
2103
|
if (top !== null) top = Math.min(Math.max(top, 0), getMaxScrollTop());
|
|
2093
|
-
if (left !== null)
|
|
2104
|
+
if (left !== null)
|
|
2105
|
+
left = Math.min(Math.max(left, 0), Math.max(0, getColLeft(tableHeaderLast.value.length) - virtualScrollX.value.containerWidth));
|
|
2094
2106
|
if (options.behavior === 'smooth') {
|
|
2095
2107
|
smoothScrollTo(top, left);
|
|
2096
2108
|
} else {
|
|
@@ -34,8 +34,6 @@ export function useFixedStyle<DT extends Record<string, any>>(
|
|
|
34
34
|
|
|
35
35
|
const { headerRowHeight, rowHeight } = props;
|
|
36
36
|
const isFixedLeft = fixed === 'left';
|
|
37
|
-
const { scrollLeft, scrollWidth, offsetLeft, containerWidth } = virtualScrollX.value;
|
|
38
|
-
const scrollRight = scrollWidth - containerWidth - scrollLeft;
|
|
39
37
|
|
|
40
38
|
let style = '';
|
|
41
39
|
|
|
@@ -60,6 +58,8 @@ export function useFixedStyle<DT extends Record<string, any>>(
|
|
|
60
58
|
style += `right:${lr};`;
|
|
61
59
|
}
|
|
62
60
|
} else {
|
|
61
|
+
const { scrollLeft, scrollWidth, offsetLeft, containerWidth } = virtualScrollX.value;
|
|
62
|
+
const scrollRight = scrollWidth - containerWidth - scrollLeft;
|
|
63
63
|
if (isFixedLeft) {
|
|
64
64
|
style += `left:${scrollLeft - (virtualX_on.value ? offsetLeft : 0)}px;`;
|
|
65
65
|
} else {
|
|
@@ -48,6 +48,131 @@ export function useMergeCells(
|
|
|
48
48
|
|
|
49
49
|
/** column index cache */
|
|
50
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
|
+
);
|
|
51
176
|
|
|
52
177
|
/**
|
|
53
178
|
* 数据/列真正变化时才清空 mergeCells 结果缓存。
|
|
@@ -65,9 +190,6 @@ export function useMergeCells(
|
|
|
65
190
|
buildHiddenCellMap();
|
|
66
191
|
});
|
|
67
192
|
|
|
68
|
-
/** 是否存在合并列(仅依赖列配置,跨滚动帧缓存,不随数据/滚动重算) */
|
|
69
|
-
const hasMergeColumn = computed(() => tableHeaderLast.value.some(col => !!col.mergeCells));
|
|
70
|
-
|
|
71
193
|
/**
|
|
72
194
|
* Pre-build hiddenCellMap and hoverRowMap before rendering to avoid
|
|
73
195
|
* a two-render cycle (null → populated) that causes visual flicker.
|
|
@@ -204,21 +326,34 @@ export function useMergeCells(
|
|
|
204
326
|
// 渲染传入的是切片内相对索引,统一换算为绝对行索引后再调用 mergeCells,
|
|
205
327
|
// 保证与 buildHiddenCellMap / aboveViewportColumnMap 的入参及缓存键一致
|
|
206
328
|
const absRowIndex = virtualScroll.value.startIndex + rowIndex;
|
|
207
|
-
const { colspan, rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, colIndex);
|
|
208
|
-
|
|
209
|
-
if (colspan === 1 && rowspan === 1) return;
|
|
210
|
-
|
|
211
|
-
const rowKey = rowKeyGen(row);
|
|
212
329
|
const colKey = colKeyGen.value(col);
|
|
213
|
-
const mergedCellKey = pureCellKeyGen(rowKey, colKey);
|
|
214
330
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
331
|
+
// 返回值记忆化:命中时跳过 getMergeCellsResult、rowspan 修正循环与对象分配。
|
|
332
|
+
// hideCells 为幂等写入(buildHiddenCellMap 已在渲染前按相同输入构建),命中时跳过安全。
|
|
333
|
+
let rowCache = wrapperCache.get(row);
|
|
334
|
+
if (!rowCache) {
|
|
335
|
+
rowCache = new Map();
|
|
336
|
+
wrapperCache.set(row, rowCache);
|
|
219
337
|
}
|
|
338
|
+
const hit = rowCache.get(colKey);
|
|
339
|
+
if (hit && hit.absRowIndex === absRowIndex && hit.gen === wrapperCacheGen) return hit.result;
|
|
220
340
|
|
|
221
|
-
|
|
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;
|
|
222
357
|
}
|
|
223
358
|
|
|
224
359
|
/**
|
|
@@ -383,103 +518,6 @@ export function useMergeCells(
|
|
|
383
518
|
return map;
|
|
384
519
|
});
|
|
385
520
|
|
|
386
|
-
/** 空合并段列表(start 为绝对行索引)。禁止合并时返回共享空数组 */
|
|
387
|
-
const EMPTY_BLOCKS: { start: number; count: number }[] = [];
|
|
388
|
-
|
|
389
|
-
/** 空下方占位段列表。无下方合并行时返回共享空数组 */
|
|
390
|
-
const EMPTY_SEGMENTS: { count: number }[] = [];
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* 视口上方连续「无 td」空行段(行数 >= 2 才成段;单行保留独立 tr)。
|
|
394
|
-
* 空行判定与渲染保持一致:aboveViewportColumnMap 中列结果为空且非展开行。
|
|
395
|
-
* 供模板合并渲染(StkTable aboveRenderParts)与 rowspan 属性修正共用。
|
|
396
|
-
*/
|
|
397
|
-
const aboveEmptyBlocks = computed<{ start: number; count: number }[]>(() => {
|
|
398
|
-
if (!canMergeEmptyRows.value) return EMPTY_BLOCKS;
|
|
399
|
-
const data = virtual_dataSourcePart.value;
|
|
400
|
-
const { startIndex, viewportStartIndex } = virtualScroll.value;
|
|
401
|
-
const aboveCount = Math.min(data.length, Math.max(0, viewportStartIndex - startIndex));
|
|
402
|
-
if (aboveCount <= 0) return EMPTY_BLOCKS;
|
|
403
|
-
const colMap = aboveViewportColumnMap.value;
|
|
404
|
-
|
|
405
|
-
const isEmptyRow = (row: PrivateRowDT): boolean => {
|
|
406
|
-
// 展开行有独立行高且渲染内容,不能参与合并
|
|
407
|
-
if (!row || row.__EXP_R__) return false;
|
|
408
|
-
const cols = colMap.get(rowKeyGen(row));
|
|
409
|
-
// 仅当映射中明确为空列结果时才算空行;缺失(异常)时保留原渲染
|
|
410
|
-
return cols !== void 0 && cols.length === 0;
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
const blocks: { start: number; count: number }[] = [];
|
|
414
|
-
let runStart = -1;
|
|
415
|
-
const flushRun = (endExclusive: number) => {
|
|
416
|
-
if (endExclusive - runStart >= 2) {
|
|
417
|
-
blocks.push({ start: startIndex + runStart, count: endExclusive - runStart });
|
|
418
|
-
}
|
|
419
|
-
runStart = -1;
|
|
420
|
-
};
|
|
421
|
-
for (let i = 0; i < aboveCount; i++) {
|
|
422
|
-
if (isEmptyRow(data[i])) {
|
|
423
|
-
if (runStart < 0) runStart = i;
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
if (runStart >= 0) flushRun(i);
|
|
427
|
-
}
|
|
428
|
-
if (runStart >= 0) flushRun(aboveCount);
|
|
429
|
-
return blocks.length ? blocks : EMPTY_BLOCKS;
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* 视口下方占位段:把 [viewportEndIndex+1, endIndex] 按跨界 rowspan 的
|
|
434
|
-
* 去重结束行切分为多段,每段渲染一个占位 tr。
|
|
435
|
-
*
|
|
436
|
-
* 单个占位 tr 无法表达多个 rowspan 的不同逻辑结束位置(不规律合并下
|
|
437
|
-
* 多个单元格会塌缩到同一 tr 底边,滚动时高度跳动);按结束行切段后,
|
|
438
|
-
* 每个单元格的修正 rowspan 恰好止于包含其逻辑结束行的段。
|
|
439
|
-
* 超长 rowspan 场景只有一个结束行(= 区域末端),仍只产生 1 段,不增加 DOM。
|
|
440
|
-
*/
|
|
441
|
-
const belowPhSegments = computed<{ count: number }[]>(() => {
|
|
442
|
-
const belowCount = belowViewportRowCount.value;
|
|
443
|
-
if (belowCount <= 0) return EMPTY_SEGMENTS;
|
|
444
|
-
const data = virtual_dataSourcePart.value;
|
|
445
|
-
const { startIndex, viewportEndIndex, endIndex } = virtualScroll.value;
|
|
446
|
-
// 下方区域末端钳制到窗口最后一行(endIndex 可能超出数据长度)
|
|
447
|
-
const regionEnd = Math.min(endIndex, startIndex + data.length - 1);
|
|
448
|
-
const columns = virtualX_columnPart.value;
|
|
449
|
-
|
|
450
|
-
// 跨界 rowspan 的锚点只可能在视口上方保留行与视口行内,收集其去重结束行
|
|
451
|
-
const endsSet = new Set<number>();
|
|
452
|
-
const anchorRowEnd = Math.min(data.length - 1, viewportEndIndex - startIndex);
|
|
453
|
-
for (let rowOffset = 0; rowOffset <= anchorRowEnd; rowOffset++) {
|
|
454
|
-
const row = data[rowOffset];
|
|
455
|
-
if (!row) continue;
|
|
456
|
-
const absRowIndex = startIndex + rowOffset;
|
|
457
|
-
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
458
|
-
const col = columns[colIdx];
|
|
459
|
-
if (!col.mergeCells) continue;
|
|
460
|
-
const leafIndex = col.__LF_S__ ?? colIdx;
|
|
461
|
-
const { rowspan } = mergeCellsCache.getMergeCellsResult(row, col, absRowIndex, leafIndex);
|
|
462
|
-
if (rowspan <= 1) continue;
|
|
463
|
-
const spanEnd = absRowIndex + rowspan - 1;
|
|
464
|
-
if (spanEnd > viewportEndIndex) endsSet.add(Math.min(spanEnd, regionEnd));
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// 兜底:未收集到跨界结束行时保持单段(与合并前行为一致)
|
|
469
|
-
if (!endsSet.size) return [{ count: belowCount }];
|
|
470
|
-
|
|
471
|
-
const ends = Array.from(endsSet).sort((a, b) => a - b);
|
|
472
|
-
const segments: { count: number }[] = [];
|
|
473
|
-
let prev = viewportEndIndex;
|
|
474
|
-
for (let i = 0; i < ends.length; i++) {
|
|
475
|
-
const count = ends[i] - prev;
|
|
476
|
-
if (count > 0) segments.push({ count });
|
|
477
|
-
prev = ends[i];
|
|
478
|
-
}
|
|
479
|
-
if (regionEnd > prev) segments.push({ count: regionEnd - prev });
|
|
480
|
-
return segments;
|
|
481
|
-
});
|
|
482
|
-
|
|
483
521
|
function updateActiveMergedCells(clear?: boolean, rowKey?: UniqKey) {
|
|
484
522
|
if (!rowActiveProp.value.enabled) return;
|
|
485
523
|
if (clear) {
|
|
@@ -13,11 +13,12 @@ const SORT_SWITCH_ORDER: Order[] = [null, 'desc', 'asc'] as const;
|
|
|
13
13
|
* 排序 Hook
|
|
14
14
|
* 管理表格排序状态和相关操作
|
|
15
15
|
* @param props 表格 props
|
|
16
|
-
* @param colKeyGen 列 key 生成函数
|
|
16
|
+
* @param colKeyGen 列 key 生成函数
|
|
17
17
|
* @param tableHeaderLast 表头最后一行(叶子节点)
|
|
18
18
|
* @param dataSourceCopy 数据源副本 ref
|
|
19
19
|
* @param initDataSource 初始化数据源函数
|
|
20
20
|
* @param emits 事件发射函数
|
|
21
|
+
* @param onDataSourceChange 数据源变化后的刷新回调(重算虚拟滚动等,见 #80)
|
|
21
22
|
* @returns 排序相关状态和方法
|
|
22
23
|
*/
|
|
23
24
|
export function useSorter<DT extends Record<string, any>>(
|
|
@@ -27,6 +28,7 @@ export function useSorter<DT extends Record<string, any>>(
|
|
|
27
28
|
tableHeaderLast: Ref<StkTableColumn<DT>[]>,
|
|
28
29
|
dataSourceCopy: Ref<DT[]>,
|
|
29
30
|
initDataSource: (data?: DT[], option?: { forceSort?: boolean }) => void,
|
|
31
|
+
onDataSourceChange: () => void,
|
|
30
32
|
) {
|
|
31
33
|
/** 多列排序状态数组 */
|
|
32
34
|
const sortStates = ref<SortState<DT>[]>([]);
|
|
@@ -182,6 +184,8 @@ export function useSorter<DT extends Record<string, any>>(
|
|
|
182
184
|
|
|
183
185
|
if (!props.sortRemote) {
|
|
184
186
|
initDataSource();
|
|
187
|
+
// #80:排序后数据窗口需重算,否则残留 startIndex/endIndex 会“吃掉”数据
|
|
188
|
+
onDataSourceChange();
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
emits('sort-change', col, order, toRaw(dataSourceCopy.value), sortConfig);
|
|
@@ -220,6 +224,8 @@ export function useSorter<DT extends Record<string, any>>(
|
|
|
220
224
|
if (newOption.sort && dataSourceCopy.value?.length) {
|
|
221
225
|
if (!props.sortRemote || newOption.force) {
|
|
222
226
|
initDataSource(props.dataSource, { forceSort: newOption.force });
|
|
227
|
+
// #80:与 onColumnSort 一致,排序后重算虚拟滚动窗口
|
|
228
|
+
onDataSourceChange();
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
231
|
|
|
@@ -243,6 +249,8 @@ export function useSorter<DT extends Record<string, any>>(
|
|
|
243
249
|
function resetSorter() {
|
|
244
250
|
sortStates.value = [];
|
|
245
251
|
initDataSource();
|
|
252
|
+
// #80:重置后同样重算虚拟滚动窗口
|
|
253
|
+
onDataSourceChange();
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
/**
|
|
@@ -21,7 +21,11 @@ export type VirtualScrollStore = {
|
|
|
21
21
|
rowHeight: number;
|
|
22
22
|
/** 表格定位上边距 */
|
|
23
23
|
offsetTop: number;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* 纵向滚动条位置,用于判断是横向滚动还是纵向。
|
|
26
|
+
* 静默字段:非 relative 模式下写入不触发 triggerRef(滚动窗口未变的帧零重渲染),
|
|
27
|
+
* 不得在响应式上下文(computed/模板/watch)中依赖此字段。
|
|
28
|
+
*/
|
|
25
29
|
scrollTop: number;
|
|
26
30
|
/** 总滚动高度 */
|
|
27
31
|
scrollHeight: number;
|
|
@@ -43,7 +47,10 @@ export type VirtualScrollXStore = {
|
|
|
43
47
|
endIndex: number;
|
|
44
48
|
/** 表格定位左边距 */
|
|
45
49
|
offsetLeft: number;
|
|
46
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* 横向滚动位置,用于判断是横向滚动还是纵向。
|
|
52
|
+
* 静默字段:非 relative 模式下写入不触发 triggerRef,不得在响应式上下文中依赖此字段。
|
|
53
|
+
*/
|
|
47
54
|
scrollLeft: number;
|
|
48
55
|
};
|
|
49
56
|
|
|
@@ -91,6 +98,10 @@ function useColWidthCache<T extends { fixed?: StkTableColumn<PrivateRowDT>['fixe
|
|
|
91
98
|
/** vue2 优化滚动回收延时 */
|
|
92
99
|
const VUE2_SCROLL_TIMEOUT_MS = 200;
|
|
93
100
|
|
|
101
|
+
/** 静默字段常量(模块级复用,避免每次滚动帧分配数组);relative 模式不静默(见 assignVs 注释) */
|
|
102
|
+
const SILENT_Y_KEYS: ['scrollTop'] = ['scrollTop'];
|
|
103
|
+
const SILENT_X_KEYS: ['scrollLeft'] = ['scrollLeft'];
|
|
104
|
+
|
|
94
105
|
/**
|
|
95
106
|
* 合并单元格可视范围修正的最大迭代次数。
|
|
96
107
|
* 合法(不重叠)的合并配置最多 2 轮即收敛(1 轮扩展 + 1 轮验证);
|
|
@@ -115,6 +126,8 @@ export function useVirtualScroll(
|
|
|
115
126
|
getMaxRowSpanValue: () => number,
|
|
116
127
|
scrollbarOptions: Ref<Required<ScrollbarOptions>>,
|
|
117
128
|
isExperimentalScrollY: Ref<boolean | undefined>,
|
|
129
|
+
/** relative 固定模式的样式(useFixedStyle)以响应式方式依赖 scrollTop/scrollLeft,该模式下保留滚动位置的响应式触发 */
|
|
130
|
+
isRelativeMode: Ref<boolean>,
|
|
118
131
|
/** mergeCells 结果共享缓存(与 useMergeCells 共用,避免重复调用用户回调) */
|
|
119
132
|
mergeCellsCache: MergeCellsCache,
|
|
120
133
|
) {
|
|
@@ -538,15 +551,20 @@ export function useVirtualScroll(
|
|
|
538
551
|
* shallowRef 内部属性变更不会触发响应式,必须手动 triggerRef;但无条件触发会让
|
|
539
552
|
* 「值未变」的调用也强制整表重渲染(每帧滚动多付 1~5ms 渲染成本)。
|
|
540
553
|
* 本函数恢复 Vue 深响应式 ref 的 hasChanged 语义,滚动重算回到 0.02ms 级。
|
|
554
|
+
*
|
|
555
|
+
* silentKeys:写入 store 但不参与 triggerRef 判定的字段。滚动位置(scrollTop/scrollLeft)
|
|
556
|
+
* 每帧必变且模板不直接消费,静默写入使「滚动窗口(startIndex/endIndex)未变」的帧
|
|
557
|
+
* 完全跳过组件重渲染(消费方均在滚动处理函数中即时读取 .value)。
|
|
558
|
+
* relative 固定模式例外:useFixedStyle 的样式计算响应式依赖滚动位置,该模式下不静默。
|
|
541
559
|
*/
|
|
542
|
-
function assignVs<T extends object>(ref: ShallowRef<T>, patch: Partial<T
|
|
560
|
+
function assignVs<T extends object>(ref: ShallowRef<T>, patch: Partial<T>, silentKeys?: (keyof T)[]) {
|
|
543
561
|
const store = ref.value;
|
|
544
562
|
let changed = false;
|
|
545
563
|
for (const key in patch) {
|
|
546
564
|
const k = key as keyof T;
|
|
547
565
|
if (store[k] !== patch[k]) {
|
|
548
566
|
store[k] = patch[k] as T[keyof T];
|
|
549
|
-
changed = true;
|
|
567
|
+
if (!silentKeys?.includes(k)) changed = true;
|
|
550
568
|
}
|
|
551
569
|
}
|
|
552
570
|
if (changed) triggerRef(ref);
|
|
@@ -736,16 +754,21 @@ export function useVirtualScroll(
|
|
|
736
754
|
}
|
|
737
755
|
vsValue.scrollTop = sTop;
|
|
738
756
|
|
|
739
|
-
|
|
757
|
+
// relative 模式固定表头样式响应式依赖 scrollTop,保留触发;其余模式静默写入
|
|
758
|
+
assignVs(virtualScroll, vsValue, isRelativeMode.value ? undefined : SILENT_Y_KEYS);
|
|
740
759
|
|
|
741
760
|
if (!virtual_on.value) {
|
|
742
761
|
// github #34 init
|
|
762
|
+
// endIndex 重置为最后一行而非 0:virtual_on=false 时虽不参与切片(全量渲染),
|
|
763
|
+
// 但若后续数据变多而未被重算(如旧版筛选/排序路径,见 #80),残留 endIndex=0
|
|
764
|
+
// 会使 slice(startIndex, 0+1) 只渲染 1 行;指向末行可让残留窗口覆盖全量数据。
|
|
765
|
+
const lastIdx = Math.max(0, dataLength - 1);
|
|
743
766
|
assignVs(virtualScroll, {
|
|
744
767
|
startIndex: 0,
|
|
745
|
-
endIndex:
|
|
768
|
+
endIndex: lastIdx,
|
|
746
769
|
offsetTop: 0,
|
|
747
770
|
viewportStartIndex: 0,
|
|
748
|
-
viewportEndIndex:
|
|
771
|
+
viewportEndIndex: lastIdx,
|
|
749
772
|
});
|
|
750
773
|
return;
|
|
751
774
|
}
|
|
@@ -864,8 +887,9 @@ export function useVirtualScroll(
|
|
|
864
887
|
endIndex = Math.min(endIndex, dataLength);
|
|
865
888
|
|
|
866
889
|
if (startIndex >= endIndex) {
|
|
867
|
-
// fallback
|
|
868
|
-
startIndex
|
|
890
|
+
// fallback(#80:scrollTop 残留超出新数据长度时 endIndex 被钳到 dataLength,
|
|
891
|
+
// 需回退到末页窗口;钳非负防 pageSize 并常时产生负 startIndex → 负 offsetTop)
|
|
892
|
+
startIndex = Math.max(0, endIndex - pageSize);
|
|
869
893
|
}
|
|
870
894
|
|
|
871
895
|
if (vue2ScrollYTimeout) {
|
|
@@ -958,13 +982,16 @@ export function useVirtualScroll(
|
|
|
958
982
|
window.clearTimeout(vue2ScrollXTimeout);
|
|
959
983
|
}
|
|
960
984
|
|
|
985
|
+
// relative 模式固定列样式响应式依赖 scrollLeft,保留触发;其余模式静默写入
|
|
986
|
+
const silentXKeys = isRelativeMode.value ? undefined : SILENT_X_KEYS;
|
|
987
|
+
|
|
961
988
|
// <= 等于是因为初始化时要赋值
|
|
962
989
|
if (!props.optimizeVue2Scroll || sLeft <= scrollLeft) {
|
|
963
990
|
// 向左滚动
|
|
964
|
-
assignVs(virtualScrollX, { startIndex, endIndex, offsetLeft, scrollLeft: sLeft });
|
|
991
|
+
assignVs(virtualScrollX, { startIndex, endIndex, offsetLeft, scrollLeft: sLeft }, silentXKeys);
|
|
965
992
|
} else {
|
|
966
993
|
// vue2 向右滚动优化
|
|
967
|
-
assignVs(virtualScrollX, { endIndex, scrollLeft: sLeft });
|
|
994
|
+
assignVs(virtualScrollX, { endIndex, scrollLeft: sLeft }, silentXKeys);
|
|
968
995
|
vue2ScrollXTimeout = window.setTimeout(() => {
|
|
969
996
|
assignVs(virtualScrollX, { startIndex, offsetLeft });
|
|
970
997
|
}, VUE2_SCROLL_TIMEOUT_MS);
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Ref, ShallowRef } from 'vue';
|
|
2
|
-
import { AreaSelectionConfig, AreaSelectionRange, CellKeyGen, ColKeyGen, StkTableColumn, UniqKey, AreaSelectionSetterRange, AreaSelectionSetterOption } from '../types';
|
|
3
|
-
import { VirtualScrollStore, VirtualScrollXStore } from '../useVirtualScroll';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 单元格区域选择功能
|
|
7
|
-
* 支持鼠标拖拽选择、键盘导航、复制粘贴等功能
|
|
8
|
-
* en: Cell area selection feature with mouse drag, keyboard navigation, copy-paste, etc.
|
|
9
|
-
*/
|
|
10
|
-
export declare function useAreaSelection<DT extends Record<string, any>>(props: any, emits: any, tableContainerRef: Ref<HTMLDivElement | undefined>, dataSourceCopy: ShallowRef<DT[]>, tableHeaderLast: ShallowRef<StkTableColumn<DT>[]>, colKeyGen: ColKeyGen, cellKeyGen: CellKeyGen, scrollTo: (top: number | null, left: number | null) => void, virtualScroll: Ref<VirtualScrollStore>, virtualScrollX: Ref<VirtualScrollXStore>, getRowIndex: (row: DT) => number, getColumnIndex: (col: StkTableColumn<DT>) => number): {
|
|
11
|
-
config: import('vue').ComputedRef<AreaSelectionConfig>;
|
|
12
|
-
isSelecting: Ref<boolean, boolean>;
|
|
13
|
-
getClass: (cellKey: string, absoluteRowIndex: number, colKey: UniqKey) => string[];
|
|
14
|
-
getRowClass: (absoluteRowIndex: number) => string[];
|
|
15
|
-
get: () => {
|
|
16
|
-
rows: DT[];
|
|
17
|
-
cols: StkTableColumn<DT>[];
|
|
18
|
-
ranges: AreaSelectionRange[];
|
|
19
|
-
};
|
|
20
|
-
set: (ranges?: AreaSelectionSetterRange<DT>, option?: AreaSelectionSetterOption) => AreaSelectionRange[];
|
|
21
|
-
clear: () => void;
|
|
22
|
-
copy: () => string;
|
|
23
|
-
onMD: (e: MouseEvent) => void;
|
|
24
|
-
};
|
|
25
|
-
export declare const useAreaSelectionName = "useAreaSelection";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export default class DragResize {
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
* @param {HTMLElement} el
|
|
5
|
-
*/
|
|
6
|
-
constructor(el: HTMLElement);
|
|
7
|
-
buttonSize: {
|
|
8
|
-
width: number;
|
|
9
|
-
height: number;
|
|
10
|
-
};
|
|
11
|
-
/** @type {ResizeObserver} */
|
|
12
|
-
resizeObserver: ResizeObserver;
|
|
13
|
-
/** @type {DOMRect} */
|
|
14
|
-
targetDOMRect: DOMRect;
|
|
15
|
-
/** @type {{left:number,top:number}} */
|
|
16
|
-
resizeButtonStyle: {
|
|
17
|
-
left: number;
|
|
18
|
-
top: number;
|
|
19
|
-
};
|
|
20
|
-
el: HTMLElement;
|
|
21
|
-
initData(): void;
|
|
22
|
-
createResizeButton(): void;
|
|
23
|
-
resizeButton: HTMLElement | undefined;
|
|
24
|
-
/** 监听元素大小改变 */
|
|
25
|
-
onResize(): void;
|
|
26
|
-
addButtonHoverStyle(): void;
|
|
27
|
-
addEvent(): void;
|
|
28
|
-
}
|
package/lib/test/utils/h.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createElement function
|
|
3
|
-
* h(tag[, text[,children]])
|
|
4
|
-
* h(tag[, attrs[,children]])
|
|
5
|
-
* h(tag[, children])
|
|
6
|
-
* @param {String} tag 标签名称,支持tag#id.class emmet写法,暂支持id ,class
|
|
7
|
-
* @param {Object | String | Number | Array<HTMLElement>} attrs 传Object为属性,传String为textContent,传数组为children
|
|
8
|
-
* @param {Array<HTMLElement>} children
|
|
9
|
-
*/
|
|
10
|
-
export default function h(tag: string, attrs: Object | string | number | Array<HTMLElement>, children: Array<HTMLElement>): HTMLElement;
|