stk-table-vue 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -9
- package/lib/{Dropdown-gzfyZN4Y.js → Dropdown-DknXtT1A.js} +2 -2
- package/lib/{StkTable-CoLDxW4i.js → StkTable-6OMgH-gT.js} +223 -24
- package/lib/src/StkTable/StkTable.vue.d.ts +3 -0
- package/lib/src/StkTable/useTree.d.ts +27 -2
- package/lib/stk-table-vue.js +3 -3
- package/lib/style.css +1 -1
- package/lib/test/setTreeExpandParents.test.d.ts +1 -0
- package/lib/test/virtualXMerge.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/StkTable/useRowExpand.ts +2 -2
- package/src/StkTable/useTree.ts +107 -6
- package/src/StkTable/useVirtualScroll.ts +141 -21
package/README.md
CHANGED
|
@@ -12,13 +12,26 @@
|
|
|
12
12
|
</p>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
Stk Table Vue(Sticky Table) is a high-performance virtual
|
|
16
|
-
|
|
17
|
-
Smooth performance with tens of thousands of rows
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
Stk Table Vue(Sticky Table) is a high-performance virtual scrolling table component based on Vue 3 / Vue 2.7.
|
|
16
|
+
|
|
17
|
+
Smooth performance with tens of thousands of rows. Designed for real-time data display with built-in highlighting and animation effects.
|
|
18
|
+
|
|
19
|
+
- Vue 3 & Vue 2.7 support
|
|
20
|
+
- Virtual scrolling (X & Y axis)
|
|
21
|
+
- CSS `sticky` based fixed columns & fixed header
|
|
22
|
+
- Multi-level header with horizontal virtual scroll
|
|
23
|
+
- Tree table & expandable rows
|
|
24
|
+
- Area selection with keyboard navigation
|
|
25
|
+
- Cell highlighting with Animation API
|
|
26
|
+
- Built-in custom cells: Filter, Editable, Checkbox, Number, Change
|
|
27
|
+
- Column resize, header drag, row drag
|
|
28
|
+
- Merge cells (virtual mode supported)
|
|
29
|
+
- Custom scrollbar
|
|
30
|
+
- Sortable & filterable
|
|
31
|
+
- Theme support (dark / light)
|
|
32
|
+
- CSS variables for easy customization
|
|
33
|
+
- Based on native `<table>` element
|
|
34
|
+
- Zero third-party runtime dependencies
|
|
22
35
|
|
|
23
36
|
|
|
24
37
|
## Documentation
|
|
@@ -172,5 +185,5 @@ const dataSource = [
|
|
|
172
185
|
Compare performance with other vue table [vue-table-compare](https://github.com/ja-plus/vue-table-compare)
|
|
173
186
|
|
|
174
187
|
|
|
175
|
-
##
|
|
176
|
-
|
|
188
|
+
## License
|
|
189
|
+
[MIT](LICENSE)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* name: stk-table-vue
|
|
3
|
-
* version: v1.0
|
|
3
|
+
* version: v1.1.0
|
|
4
4
|
* description: High performance realtime virtual table for vue3 and vue2.7
|
|
5
5
|
* author: japlus
|
|
6
6
|
* homepage: https://ja-plus.github.io/stk-table-vue/
|
|
7
7
|
* license: MIT
|
|
8
8
|
*/
|
|
9
|
-
import { t as StkTable_default } from "./StkTable-
|
|
9
|
+
import { t as StkTable_default } from "./StkTable-6OMgH-gT.js";
|
|
10
10
|
import { createElementBlock, createElementVNode, createVNode, defineComponent, h, nextTick, normalizeClass, normalizeStyle, onMounted, onUnmounted, openBlock, reactive, ref, withModifiers } from "vue";
|
|
11
11
|
//#region src/StkTable/custom-cells/FilterCell/Dropdown/index.vue?vue&type=script&setup=true&lang.ts
|
|
12
12
|
var DROPDOWN_DEFAULT_WIDTH = 300;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* name: stk-table-vue
|
|
3
|
-
* version: v1.0
|
|
3
|
+
* version: v1.1.0
|
|
4
4
|
* description: High performance realtime virtual table for vue3 and vue2.7
|
|
5
5
|
* author: japlus
|
|
6
6
|
* homepage: https://ja-plus.github.io/stk-table-vue/
|
|
@@ -2170,7 +2170,7 @@ function useRowExpand(emits, dataSourceCopy, rowKeyGen, onDataSourceChange) {
|
|
|
2170
2170
|
*/
|
|
2171
2171
|
function setRowExpand(rowKeyOrRow, expand, data) {
|
|
2172
2172
|
let rowKey;
|
|
2173
|
-
if (typeof rowKeyOrRow === "string") rowKey = rowKeyOrRow;
|
|
2173
|
+
if (typeof rowKeyOrRow === "string" || typeof rowKeyOrRow === "number") rowKey = rowKeyOrRow;
|
|
2174
2174
|
else rowKey = rowKeyGen(rowKeyOrRow);
|
|
2175
2175
|
const tempData = dataSourceCopy.value.slice();
|
|
2176
2176
|
const index = tempData.findIndex((it) => rowKeyGen(it) === rowKey);
|
|
@@ -2929,6 +2929,9 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
|
|
|
2929
2929
|
* @param row rowKey or row
|
|
2930
2930
|
* @param option
|
|
2931
2931
|
* @param option.expand expand or collapse
|
|
2932
|
+
* @param option.all expand all descendants
|
|
2933
|
+
* @param option.level expand to the nth level
|
|
2934
|
+
* @param option.parents expand/collapse all ancestors of the given row, the target row itself is also expanded if it has children
|
|
2932
2935
|
* @param option.silent if set true, not emit `toggle-tree-expand`, default:false
|
|
2933
2936
|
*/
|
|
2934
2937
|
function privateSetTreeExpand(row, option) {
|
|
@@ -2937,7 +2940,7 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
|
|
|
2937
2940
|
for (let i = 0; i < rowKeyOrRowArr.length; i++) {
|
|
2938
2941
|
const rowKeyOrRow = rowKeyOrRowArr[i];
|
|
2939
2942
|
let rowKey;
|
|
2940
|
-
if (typeof rowKeyOrRow === "string") rowKey = rowKeyOrRow;
|
|
2943
|
+
if (typeof rowKeyOrRow === "string" || typeof rowKeyOrRow === "number") rowKey = rowKeyOrRow;
|
|
2941
2944
|
else rowKey = rowKeyGen(rowKeyOrRow);
|
|
2942
2945
|
const index = tempData.findIndex((it) => rowKeyGen(it) === rowKey);
|
|
2943
2946
|
if (index === -1) {
|
|
@@ -2946,12 +2949,22 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
|
|
|
2946
2949
|
}
|
|
2947
2950
|
const row = tempData[index];
|
|
2948
2951
|
const level = row.__T_LV__ || 0;
|
|
2952
|
+
const wasExpanded = Boolean(row.__T_EXP__);
|
|
2949
2953
|
let expanded = option === null || option === void 0 ? void 0 : option.expand;
|
|
2950
2954
|
if (expanded === void 0) expanded = !row.__T_EXP__;
|
|
2951
|
-
if (
|
|
2955
|
+
if (option.all || option.level !== void 0) {
|
|
2956
|
+
const targetLevel = option.all ? Infinity : option.level || 0;
|
|
2957
|
+
setDescendantsToLevel(row, level + 1, targetLevel, expanded);
|
|
2958
|
+
}
|
|
2959
|
+
if (expanded) if (wasExpanded) {
|
|
2960
|
+
const deleteCount = foldNode(index, tempData, level);
|
|
2952
2961
|
const children = expandNode(row, level);
|
|
2953
|
-
tempData.splice(index + 1,
|
|
2962
|
+
tempData.splice(index + 1, deleteCount, ...children);
|
|
2954
2963
|
} else {
|
|
2964
|
+
const children = expandNode(row, level);
|
|
2965
|
+
tempData.splice(index + 1, 0, ...children);
|
|
2966
|
+
}
|
|
2967
|
+
else {
|
|
2955
2968
|
const deleteCount = foldNode(index, tempData, level);
|
|
2956
2969
|
tempData.splice(index + 1, deleteCount);
|
|
2957
2970
|
}
|
|
@@ -2966,11 +2979,55 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
|
|
|
2966
2979
|
onDataSourceChange();
|
|
2967
2980
|
}
|
|
2968
2981
|
function setTreeExpand(row, option) {
|
|
2982
|
+
if (option === null || option === void 0 ? void 0 : option.parents) {
|
|
2983
|
+
var _target$children;
|
|
2984
|
+
const rowKeyOrRow = Array.isArray(row) ? row[0] : row;
|
|
2985
|
+
const rowKey = typeof rowKeyOrRow === "string" || typeof rowKeyOrRow === "number" ? rowKeyOrRow : rowKeyGen(rowKeyOrRow);
|
|
2986
|
+
const path = findPath(props.dataSource || [], rowKey);
|
|
2987
|
+
if (!path) {
|
|
2988
|
+
console.warn("treeExpandRow failed.rowKey:", rowKey);
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
const expanded = (option === null || option === void 0 ? void 0 : option.expand) !== false;
|
|
2992
|
+
const target = path[path.length - 1];
|
|
2993
|
+
const keys = path.slice(0, -1).map((it) => rowKeyGen(it));
|
|
2994
|
+
if (expanded && ((_target$children = target.children) === null || _target$children === void 0 ? void 0 : _target$children.length)) keys.push(rowKeyGen(target));
|
|
2995
|
+
if (!keys.length) return;
|
|
2996
|
+
if (!expanded) keys.reverse();
|
|
2997
|
+
privateSetTreeExpand(keys, {
|
|
2998
|
+
expand: expanded,
|
|
2999
|
+
isClick: false
|
|
3000
|
+
});
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
2969
3003
|
privateSetTreeExpand(row, {
|
|
2970
3004
|
...option,
|
|
2971
3005
|
isClick: false
|
|
2972
3006
|
});
|
|
2973
3007
|
}
|
|
3008
|
+
/**
|
|
3009
|
+
* 在原始树形数据中查找目标节点,返回从根节点到目标节点的完整路径(含目标节点自身)
|
|
3010
|
+
* en: Find target node in raw tree data, return the full path from root to target node (target included)
|
|
3011
|
+
* @returns full path including target, or null if target not found
|
|
3012
|
+
*/
|
|
3013
|
+
function findPath(data, targetKey) {
|
|
3014
|
+
const path = [];
|
|
3015
|
+
function dfs(list) {
|
|
3016
|
+
for (const item of list) {
|
|
3017
|
+
if (rowKeyGen(item) === targetKey) {
|
|
3018
|
+
path.push(item);
|
|
3019
|
+
return true;
|
|
3020
|
+
}
|
|
3021
|
+
if (item.children) {
|
|
3022
|
+
path.push(item);
|
|
3023
|
+
if (dfs(item.children)) return true;
|
|
3024
|
+
path.pop();
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
return false;
|
|
3028
|
+
}
|
|
3029
|
+
return dfs(data) ? path : null;
|
|
3030
|
+
}
|
|
2974
3031
|
function setNodeExpanded(row, expanded, level, parent) {
|
|
2975
3032
|
row.__T_EXP__ = expanded;
|
|
2976
3033
|
if (level !== void 0) row.__T_LV__ = level;
|
|
@@ -3006,6 +3063,17 @@ function useTree(props, dataSourceCopy, rowKeyGen, emits, onDataSourceChange) {
|
|
|
3006
3063
|
isFirstLoad = false;
|
|
3007
3064
|
return result;
|
|
3008
3065
|
}
|
|
3066
|
+
/**
|
|
3067
|
+
* 递归设置目标节点后代到指定层级的展开/折叠状态
|
|
3068
|
+
* en: Recursively set expand/collapse state for descendants up to the target level
|
|
3069
|
+
*/
|
|
3070
|
+
function setDescendantsToLevel(row, currentLevel, targetLevel, expanded) {
|
|
3071
|
+
if (!row.children || currentLevel > targetLevel) return;
|
|
3072
|
+
for (const child of row.children) {
|
|
3073
|
+
setNodeExpanded(child, expanded, currentLevel, row);
|
|
3074
|
+
setDescendantsToLevel(child, currentLevel + 1, targetLevel, expanded);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3009
3077
|
function expandNode(row, level) {
|
|
3010
3078
|
let result = [];
|
|
3011
3079
|
row.children && row.children.forEach((child) => {
|
|
@@ -3081,6 +3149,12 @@ function useColWidthCache(getColWidth) {
|
|
|
3081
3149
|
/** vue2 优化滚动回收延时 */
|
|
3082
3150
|
var VUE2_SCROLL_TIMEOUT_MS = 200;
|
|
3083
3151
|
/**
|
|
3152
|
+
* 合并单元格可视范围修正的最大迭代次数。
|
|
3153
|
+
* 合法(不重叠)的合并配置最多 2 轮即收敛(1 轮扩展 + 1 轮验证);
|
|
3154
|
+
* 此上限仅针对重叠合并区域等病态配置兜底,防止修正循环过长。
|
|
3155
|
+
*/
|
|
3156
|
+
var MAX_MERGE_RANGE_EXPAND_ITERATIONS = 8;
|
|
3157
|
+
/**
|
|
3084
3158
|
* virtual scroll
|
|
3085
3159
|
* @returns
|
|
3086
3160
|
*/
|
|
@@ -3139,20 +3213,145 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
3139
3213
|
/** 是否多级表头 */
|
|
3140
3214
|
const isMultiLevelHeader = computed(() => tableHeaders.value.length > 1);
|
|
3141
3215
|
/**
|
|
3216
|
+
* 需要参与可视范围修正的合并列集合。仅依赖 tableHeaderLast(columns 变化),
|
|
3217
|
+
* 缓存后避免在数据更新、纵向滚动换窗等高频调用 buildColMergeRange 时重复收集。
|
|
3218
|
+
* 无需修正时返回 null。
|
|
3219
|
+
*/
|
|
3220
|
+
const virtualX_mergeColsInfo = computed(() => {
|
|
3221
|
+
const headers = tableHeaderLast.value;
|
|
3222
|
+
const headerLength = headers.length;
|
|
3223
|
+
let maxColIndex = headerLength;
|
|
3224
|
+
const mergeCols = [];
|
|
3225
|
+
for (let i = 0; i < headerLength; i++) {
|
|
3226
|
+
const col = headers[i];
|
|
3227
|
+
if (maxColIndex === headerLength && col.fixed === "right") maxColIndex = i;
|
|
3228
|
+
if (col.mergeCells && !col.fixed) mergeCols.push({
|
|
3229
|
+
col,
|
|
3230
|
+
index: i
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
if (!mergeCols.length || !maxColIndex) return null;
|
|
3234
|
+
return {
|
|
3235
|
+
headerLength,
|
|
3236
|
+
maxColIndex,
|
|
3237
|
+
mergeCols
|
|
3238
|
+
};
|
|
3239
|
+
});
|
|
3240
|
+
/**
|
|
3241
|
+
* 收集行集合中的列合并(colspan)区间
|
|
3242
|
+
* @param rows 行数据
|
|
3243
|
+
* @param rowIndexBase mergeCells 入参 rowIndex 的起始值
|
|
3244
|
+
*/
|
|
3245
|
+
function buildColMergeRange(rows, rowIndexBase) {
|
|
3246
|
+
const mergeColsInfo = virtualX_mergeColsInfo.value;
|
|
3247
|
+
if (!mergeColsInfo) return null;
|
|
3248
|
+
const { headerLength, maxColIndex, mergeCols } = mergeColsInfo;
|
|
3249
|
+
const leftReach = new Int32Array(headerLength).fill(-1);
|
|
3250
|
+
const rightEnd = new Int32Array(headerLength);
|
|
3251
|
+
for (let r = 0; r < rows.length; r++) {
|
|
3252
|
+
const row = rows[r];
|
|
3253
|
+
for (let m = 0; m < mergeCols.length; m++) {
|
|
3254
|
+
const { col, index } = mergeCols[m];
|
|
3255
|
+
if (index >= maxColIndex) break;
|
|
3256
|
+
const { colspan = 1 } = col.mergeCells({
|
|
3257
|
+
row,
|
|
3258
|
+
col,
|
|
3259
|
+
rowIndex: rowIndexBase + r,
|
|
3260
|
+
colIndex: index
|
|
3261
|
+
}) || {};
|
|
3262
|
+
if (colspan > 1) {
|
|
3263
|
+
const end = Math.min(index + colspan, maxColIndex);
|
|
3264
|
+
for (let c = index; c < end; c++) {
|
|
3265
|
+
if (leftReach[c] === -1 || index < leftReach[c]) leftReach[c] = index;
|
|
3266
|
+
if (end > rightEnd[c]) rightEnd[c] = end;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
return {
|
|
3272
|
+
leftReach,
|
|
3273
|
+
rightEnd
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* 全量数据的列合并覆盖信息(懒计算)。
|
|
3278
|
+
* 仅在 Y 虚拟滚动未开启(可视行即全量数据)且被消费时才遍历全量数据;
|
|
3279
|
+
* 依赖 dataSourceCopy 与 columns,数据/列变化时自动重算。
|
|
3280
|
+
* virtual + virtualX 模式下消费方走可视行分支,本 computed 直接返回 null,
|
|
3281
|
+
* 避免 O(rows × mergeCols) 的全量 mergeCells 调用浪费。
|
|
3282
|
+
*/
|
|
3283
|
+
const virtualX_colMergeRangeFull = computed(() => {
|
|
3284
|
+
if (!virtualX_on.value || virtual_on.value) return null;
|
|
3285
|
+
return buildColMergeRange(dataSourceCopy.value, 0);
|
|
3286
|
+
});
|
|
3287
|
+
/**
|
|
3288
|
+
* virtual-x 列合并覆盖信息。
|
|
3289
|
+
* Y 虚拟滚动开启时仅计算可视行(rowIndex 与渲染时 mergeCells 入参保持一致);
|
|
3290
|
+
* 否则使用全量数据懒计算缓存(此时可视行即全量数据)。
|
|
3291
|
+
*/
|
|
3292
|
+
const virtualX_colMergeRange = computed(() => {
|
|
3293
|
+
if (!virtualX_on.value) return null;
|
|
3294
|
+
if (virtual_on.value) return buildColMergeRange(virtual_dataSourcePart.value, 0);
|
|
3295
|
+
return virtualX_colMergeRangeFull.value;
|
|
3296
|
+
});
|
|
3297
|
+
/**
|
|
3298
|
+
* 经列合并修正后的可视列范围。
|
|
3299
|
+
* 合并单元格的锚点列可能已滚出可视区(但合并区域仍覆盖可视列),
|
|
3300
|
+
* 也可能合并区域超出可视区右边界,需要将范围扩展到能完整渲染合并单元格。
|
|
3301
|
+
*/
|
|
3302
|
+
const virtualX_colRange = computed(() => {
|
|
3303
|
+
let { startIndex, endIndex } = virtualScrollX.value;
|
|
3304
|
+
const mergeRange = virtualX_colMergeRange.value;
|
|
3305
|
+
if (mergeRange) {
|
|
3306
|
+
const { leftReach, rightEnd } = mergeRange;
|
|
3307
|
+
const len = leftReach.length;
|
|
3308
|
+
for (let k = 0; k < MAX_MERGE_RANGE_EXPAND_ITERATIONS; k++) {
|
|
3309
|
+
let newStart = startIndex;
|
|
3310
|
+
let newEnd = endIndex;
|
|
3311
|
+
const loopEnd = Math.min(endIndex, len);
|
|
3312
|
+
for (let i = Math.max(0, startIndex); i < loopEnd; i++) {
|
|
3313
|
+
const reach = leftReach[i];
|
|
3314
|
+
if (reach > -1 && reach < newStart) newStart = reach;
|
|
3315
|
+
if (rightEnd[i] > newEnd) newEnd = rightEnd[i];
|
|
3316
|
+
}
|
|
3317
|
+
if (newStart === startIndex && newEnd === endIndex) break;
|
|
3318
|
+
startIndex = newStart;
|
|
3319
|
+
endIndex = newEnd;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return {
|
|
3323
|
+
startIndex,
|
|
3324
|
+
endIndex
|
|
3325
|
+
};
|
|
3326
|
+
});
|
|
3327
|
+
/**
|
|
3142
3328
|
* 多级表头横向虚拟滚动参数:以顶层列组为单位计算开始/结束位置。
|
|
3143
3329
|
* - 只有整个顶层组完全滚出视口时才移除(避免 colSpan 变化导致抖动)。
|
|
3144
3330
|
* - 单级表头时退化为与 tbody 相同的参数。
|
|
3331
|
+
* - 范围基于列合并修正后的 virtualX_colRange,保证合并单元格完整渲染。
|
|
3145
3332
|
*/
|
|
3146
3333
|
const theadVirtualX = computed(() => {
|
|
3147
|
-
if (!virtualX_on.value
|
|
3148
|
-
startIndex
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3334
|
+
if (!virtualX_on.value) {
|
|
3335
|
+
const { startIndex, endIndex, offsetLeft } = virtualScrollX.value;
|
|
3336
|
+
return {
|
|
3337
|
+
startIndex,
|
|
3338
|
+
endIndex,
|
|
3339
|
+
offsetLeft
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
const { startIndex, endIndex } = virtualX_colRange.value;
|
|
3343
|
+
if (!isMultiLevelHeader.value) {
|
|
3344
|
+
const { nonFixedCols } = getColWidthCache(tableHeaderLast.value);
|
|
3345
|
+
const found = binarySearch(nonFixedCols, (mid) => nonFixedCols[mid].index < startIndex ? -1 : 1);
|
|
3346
|
+
return {
|
|
3347
|
+
startIndex,
|
|
3348
|
+
endIndex,
|
|
3349
|
+
offsetLeft: found > 0 ? nonFixedCols[found - 1].cumWidth : 0
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3153
3352
|
const topLevelCols = tableHeaders.value[0];
|
|
3154
3353
|
const totalLeafCount = tableHeaderLast.value.length;
|
|
3155
|
-
let theadStartIndex =
|
|
3354
|
+
let theadStartIndex = totalLeafCount;
|
|
3156
3355
|
let theadEndIndex = totalLeafCount;
|
|
3157
3356
|
let theadOffsetLeft = 0;
|
|
3158
3357
|
let cumLeft = 0;
|
|
@@ -3160,21 +3359,21 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
3160
3359
|
for (let i = 0, len = topLevelCols.length; i < len; i++) {
|
|
3161
3360
|
const col = topLevelCols[i];
|
|
3162
3361
|
if (col.fixed === "left" || col.fixed === "right") continue;
|
|
3362
|
+
const groupStart = col.__LF_S__ ?? 0;
|
|
3363
|
+
const groupEnd = col.__LF_E__ ?? groupStart + 1;
|
|
3163
3364
|
const groupWidth = col.__W__ || getCalculatedColWidth(col);
|
|
3164
|
-
|
|
3165
|
-
if (!foundStart && groupRight > scrollLeft) {
|
|
3365
|
+
if (!foundStart && groupEnd > startIndex) {
|
|
3166
3366
|
foundStart = true;
|
|
3167
|
-
theadStartIndex =
|
|
3367
|
+
theadStartIndex = groupStart;
|
|
3168
3368
|
theadOffsetLeft = cumLeft;
|
|
3169
3369
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
theadStartIndex = totalLeafCount;
|
|
3176
|
-
theadOffsetLeft = cumLeft;
|
|
3370
|
+
if (foundStart) {
|
|
3371
|
+
if (groupStart >= endIndex) break;
|
|
3372
|
+
theadEndIndex = groupEnd;
|
|
3373
|
+
}
|
|
3374
|
+
cumLeft += groupWidth;
|
|
3177
3375
|
}
|
|
3376
|
+
if (!foundStart) theadOffsetLeft = cumLeft;
|
|
3178
3377
|
return {
|
|
3179
3378
|
startIndex: theadStartIndex,
|
|
3180
3379
|
endIndex: theadEndIndex,
|
|
@@ -3184,7 +3383,7 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
3184
3383
|
const virtualX_columnPart = computed(() => {
|
|
3185
3384
|
const tableHeaderLastValue = tableHeaderLast.value;
|
|
3186
3385
|
if (virtualX_on.value) {
|
|
3187
|
-
const { startIndex, endIndex } =
|
|
3386
|
+
const { startIndex, endIndex } = virtualX_colRange.value;
|
|
3188
3387
|
const maxIndex = tableHeaderLastValue.length;
|
|
3189
3388
|
const validEndIndex = Math.min(endIndex, maxIndex);
|
|
3190
3389
|
const validStartIndex = Math.min(startIndex, maxIndex);
|
|
@@ -3252,7 +3451,7 @@ function useVirtualScroll(props, tableContainerRef, trRef, dataSourceCopy, table
|
|
|
3252
3451
|
});
|
|
3253
3452
|
const virtualX_offsetRight = computed(() => {
|
|
3254
3453
|
if (!virtualX_on.value) return 0;
|
|
3255
|
-
const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex :
|
|
3454
|
+
const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex : virtualX_colRange.value.endIndex;
|
|
3256
3455
|
let width = 0;
|
|
3257
3456
|
const tableHeaderLastValue = tableHeaderLast.value;
|
|
3258
3457
|
for (let i = endIndex; i < tableHeaderLastValue.length; i++) {
|
|
@@ -381,6 +381,9 @@ declare const _default: __VLS_WithTemplateSlots<import('vue').DefineComponent<im
|
|
|
381
381
|
children?: (PrivateRowDT & /*elided*/ any)[];
|
|
382
382
|
}))[], option?: {
|
|
383
383
|
expand?: boolean;
|
|
384
|
+
all?: boolean;
|
|
385
|
+
level?: number;
|
|
386
|
+
parents?: boolean;
|
|
384
387
|
}) => void;
|
|
385
388
|
/**
|
|
386
389
|
* 获取拖选选中的单元格信息
|
|
@@ -4,7 +4,32 @@ import { PrivateRowDT, RowKeyGen, UniqKey } from './types';
|
|
|
4
4
|
type DT = PrivateRowDT & {
|
|
5
5
|
children?: DT[];
|
|
6
6
|
};
|
|
7
|
-
|
|
7
|
+
type SetTreeExpandOption = {
|
|
8
|
+
/**
|
|
9
|
+
* 是否展开
|
|
10
|
+
* en: Whether to expand
|
|
11
|
+
* @default false
|
|
12
|
+
*/
|
|
8
13
|
expand?: boolean;
|
|
9
|
-
|
|
14
|
+
/**
|
|
15
|
+
* 是否展开所有子节点
|
|
16
|
+
* en: Whether to expand all child nodes
|
|
17
|
+
* @default false
|
|
18
|
+
* @version 1.0.4
|
|
19
|
+
*/
|
|
20
|
+
all?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* 展开到第几层
|
|
23
|
+
* en: Expand to the nth level
|
|
24
|
+
* @version 1.0.4
|
|
25
|
+
*/
|
|
26
|
+
level?: number;
|
|
27
|
+
/**
|
|
28
|
+
* 将传入 row 视为目标子节点,展开/收起其所有父节点;展开时若目标行自身有子节点则一并展开
|
|
29
|
+
* en: Treat the given row as a target child, expand/collapse all its ancestors. The target row itself is also expanded when expanding if it has children
|
|
30
|
+
* @version 1.1.0
|
|
31
|
+
*/
|
|
32
|
+
parents?: boolean;
|
|
33
|
+
};
|
|
34
|
+
export declare function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen: RowKeyGen, emits: any, onDataSourceChange: () => void): readonly [(row: DT, col: any) => void, (row: (UniqKey | DT) | (UniqKey | DT)[], option?: SetTreeExpandOption) => void, (data: DT[]) => DT[]];
|
|
10
35
|
export {};
|
package/lib/stk-table-vue.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* name: stk-table-vue
|
|
3
|
-
* version: v1.0
|
|
3
|
+
* version: v1.1.0
|
|
4
4
|
* description: High performance realtime virtual table for vue3 and vue2.7
|
|
5
5
|
* author: japlus
|
|
6
6
|
* homepage: https://ja-plus.github.io/stk-table-vue/
|
|
7
7
|
* license: MIT
|
|
8
8
|
*/
|
|
9
|
-
import { a as insertToOrderedArray, i as binarySearch, n as registerFeature, o as strCompare, r as useAreaSelection, s as tableSort, t as StkTable_default } from "./StkTable-
|
|
9
|
+
import { a as insertToOrderedArray, i as binarySearch, n as registerFeature, o as strCompare, r as useAreaSelection, s as tableSort, t as StkTable_default } from "./StkTable-6OMgH-gT.js";
|
|
10
10
|
import { Fragment, computed, createApp, createBlock, createElementBlock, createElementVNode, createTextVNode, defineComponent, getCurrentInstance, h, markRaw, nextTick, normalizeClass, openBlock, ref, renderSlot, resolveDynamicComponent, toDisplayString, watch, withModifiers } from "vue";
|
|
11
11
|
//#region src/StkTable/custom-cells/FilterCell/Dropdown/index.ts
|
|
12
12
|
var DropdownIns = null;
|
|
@@ -15,7 +15,7 @@ async function getDropdownIns() {
|
|
|
15
15
|
const div = document.createElement("div");
|
|
16
16
|
div.classList.add("stk-filter-dropdown-wrapper");
|
|
17
17
|
document.body.appendChild(div);
|
|
18
|
-
DropdownIns = createApp(await import("./Dropdown-
|
|
18
|
+
DropdownIns = createApp(await import("./Dropdown-DknXtT1A.js").then((module) => module.default)).mount(div);
|
|
19
19
|
}
|
|
20
20
|
return DropdownIns;
|
|
21
21
|
}
|
package/lib/style.css
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ShallowRef
|
|
1
|
+
import { ShallowRef } from 'vue';
|
|
2
2
|
import { EXPANDED_ROW_KEY_PREFIX } from './const';
|
|
3
3
|
import { PrivateRowDT, RowKeyGen, StkTableColumn, UniqKey } from './types';
|
|
4
4
|
type DT = PrivateRowDT;
|
|
@@ -24,7 +24,7 @@ export function useRowExpand(emits: any, dataSourceCopy: ShallowRef<DT[]>, rowKe
|
|
|
24
24
|
*/
|
|
25
25
|
function setRowExpand(rowKeyOrRow: string | undefined | DT, expand?: boolean | null, data?: { col?: StkTableColumn<DT>; silent?: boolean }) {
|
|
26
26
|
let rowKey: UniqKey;
|
|
27
|
-
if (typeof rowKeyOrRow === 'string') {
|
|
27
|
+
if (typeof rowKeyOrRow === 'string' || typeof rowKeyOrRow === 'number') {
|
|
28
28
|
rowKey = rowKeyOrRow;
|
|
29
29
|
} else {
|
|
30
30
|
rowKey = rowKeyGen(rowKeyOrRow);
|
package/src/StkTable/useTree.ts
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
-
import { ShallowRef
|
|
1
|
+
import { ShallowRef } from 'vue';
|
|
2
2
|
import { PrivateRowDT, RowKeyGen, TreeConfig, UniqKey } from './types';
|
|
3
3
|
|
|
4
4
|
type DT = PrivateRowDT & { children?: DT[] };
|
|
5
5
|
|
|
6
|
+
type SetTreeExpandOption = {
|
|
7
|
+
/**
|
|
8
|
+
* 是否展开
|
|
9
|
+
* en: Whether to expand
|
|
10
|
+
* @default false
|
|
11
|
+
*/
|
|
12
|
+
expand?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 是否展开所有子节点
|
|
15
|
+
* en: Whether to expand all child nodes
|
|
16
|
+
* @default false
|
|
17
|
+
* @version 1.0.4
|
|
18
|
+
*/
|
|
19
|
+
all?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* 展开到第几层
|
|
22
|
+
* en: Expand to the nth level
|
|
23
|
+
* @version 1.0.4
|
|
24
|
+
*/
|
|
25
|
+
level?: number;
|
|
26
|
+
/**
|
|
27
|
+
* 将传入 row 视为目标子节点,展开/收起其所有父节点;展开时若目标行自身有子节点则一并展开
|
|
28
|
+
* en: Treat the given row as a target child, expand/collapse all its ancestors. The target row itself is also expanded when expanding if it has children
|
|
29
|
+
* @version 1.1.0
|
|
30
|
+
*/
|
|
31
|
+
parents?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
6
34
|
export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen: RowKeyGen, emits: any, onDataSourceChange: () => void) {
|
|
7
35
|
const { defaultExpandAll, defaultExpandKeys, defaultExpandLevel }: TreeConfig = props.treeConfig;
|
|
8
36
|
/** It used to check if it is first load. To execute defaultExpandXXX */
|
|
@@ -18,16 +46,19 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
|
|
|
18
46
|
* @param row rowKey or row
|
|
19
47
|
* @param option
|
|
20
48
|
* @param option.expand expand or collapse
|
|
49
|
+
* @param option.all expand all descendants
|
|
50
|
+
* @param option.level expand to the nth level
|
|
51
|
+
* @param option.parents expand/collapse all ancestors of the given row, the target row itself is also expanded if it has children
|
|
21
52
|
* @param option.silent if set true, not emit `toggle-tree-expand`, default:false
|
|
22
53
|
*/
|
|
23
|
-
function privateSetTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option:
|
|
54
|
+
function privateSetTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option: SetTreeExpandOption & { col?: any; isClick: boolean }) {
|
|
24
55
|
const rowKeyOrRowArr: (UniqKey | DT)[] = Array.isArray(row) ? row : [row];
|
|
25
56
|
|
|
26
57
|
const tempData = dataSourceCopy.value.slice();
|
|
27
58
|
for (let i = 0; i < rowKeyOrRowArr.length; i++) {
|
|
28
59
|
const rowKeyOrRow = rowKeyOrRowArr[i];
|
|
29
60
|
let rowKey: UniqKey;
|
|
30
|
-
if (typeof rowKeyOrRow === 'string') {
|
|
61
|
+
if (typeof rowKeyOrRow === 'string' || typeof rowKeyOrRow === 'number') {
|
|
31
62
|
rowKey = rowKeyOrRow;
|
|
32
63
|
} else {
|
|
33
64
|
rowKey = rowKeyGen(rowKeyOrRow);
|
|
@@ -40,13 +71,26 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
|
|
|
40
71
|
|
|
41
72
|
const row = tempData[index];
|
|
42
73
|
const level = row.__T_LV__ || 0;
|
|
74
|
+
const wasExpanded = Boolean(row.__T_EXP__);
|
|
43
75
|
let expanded = option?.expand;
|
|
44
76
|
if (expanded === void 0) {
|
|
45
77
|
expanded = !row.__T_EXP__;
|
|
46
78
|
}
|
|
79
|
+
if (option.all || option.level !== void 0) {
|
|
80
|
+
const targetLevel = option.all ? Infinity : option.level || 0;
|
|
81
|
+
setDescendantsToLevel(row, level + 1, targetLevel, expanded);
|
|
82
|
+
}
|
|
47
83
|
if (expanded) {
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
if (wasExpanded) {
|
|
85
|
+
// already expanded, rebuild the flattened subtree so newly expanded
|
|
86
|
+
// descendants are inserted into the visible data source
|
|
87
|
+
const deleteCount = foldNode(index, tempData, level);
|
|
88
|
+
const children = expandNode(row, level);
|
|
89
|
+
tempData.splice(index + 1, deleteCount, ...children);
|
|
90
|
+
} else {
|
|
91
|
+
const children = expandNode(row, level);
|
|
92
|
+
tempData.splice(index + 1, 0, ...children);
|
|
93
|
+
}
|
|
50
94
|
} else {
|
|
51
95
|
// delete all child nodes from i
|
|
52
96
|
const deleteCount = foldNode(index, tempData, level);
|
|
@@ -64,10 +108,55 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
|
|
|
64
108
|
onDataSourceChange();
|
|
65
109
|
}
|
|
66
110
|
|
|
67
|
-
function setTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option?:
|
|
111
|
+
function setTreeExpand(row: (UniqKey | DT) | (UniqKey | DT)[], option?: SetTreeExpandOption) {
|
|
112
|
+
if (option?.parents) {
|
|
113
|
+
const rowKeyOrRow = Array.isArray(row) ? row[0] : row;
|
|
114
|
+
const rowKey = typeof rowKeyOrRow === 'string' || typeof rowKeyOrRow === 'number' ? rowKeyOrRow : rowKeyGen(rowKeyOrRow);
|
|
115
|
+
const path = findPath(props.dataSource || [], rowKey);
|
|
116
|
+
if (!path) {
|
|
117
|
+
console.warn('treeExpandRow failed.rowKey:', rowKey);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const expanded = option?.expand !== false;
|
|
121
|
+
const target = path[path.length - 1];
|
|
122
|
+
const keys = path.slice(0, -1).map(it => rowKeyGen(it));
|
|
123
|
+
// 展开时若目标行自身有子节点则一并展开;收起时仅处理父节点,目标行自身状态不变
|
|
124
|
+
// en: when expanding, also expand the target row itself if it has children; when collapsing, only ancestors are handled
|
|
125
|
+
if (expanded && target.children?.length) keys.push(rowKeyGen(target));
|
|
126
|
+
if (!keys.length) return;
|
|
127
|
+
// 展开时从根到目标逐级展开;收起时逆序处理,避免先折叠根节点导致其余节点从可见数据中移除而查找失败
|
|
128
|
+
// en: expand from root to target; collapse in reverse order, otherwise collapsing the root first removes the rest nodes from visible data
|
|
129
|
+
if (!expanded) keys.reverse();
|
|
130
|
+
privateSetTreeExpand(keys, { expand: expanded, isClick: false });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
68
133
|
privateSetTreeExpand(row, { ...option, isClick: false });
|
|
69
134
|
}
|
|
70
135
|
|
|
136
|
+
/**
|
|
137
|
+
* 在原始树形数据中查找目标节点,返回从根节点到目标节点的完整路径(含目标节点自身)
|
|
138
|
+
* en: Find target node in raw tree data, return the full path from root to target node (target included)
|
|
139
|
+
* @returns full path including target, or null if target not found
|
|
140
|
+
*/
|
|
141
|
+
function findPath(data: DT[], targetKey: UniqKey): DT[] | null {
|
|
142
|
+
const path: DT[] = [];
|
|
143
|
+
function dfs(list: DT[]): boolean {
|
|
144
|
+
for (const item of list) {
|
|
145
|
+
if (rowKeyGen(item) === targetKey) {
|
|
146
|
+
path.push(item);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (item.children) {
|
|
150
|
+
path.push(item);
|
|
151
|
+
if (dfs(item.children)) return true;
|
|
152
|
+
path.pop();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return dfs(data) ? path : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
71
160
|
function setNodeExpanded(row: DT, expanded: boolean, level?: number, parent?: DT) {
|
|
72
161
|
row.__T_EXP__ = expanded;
|
|
73
162
|
if (level !== void 0) {
|
|
@@ -119,6 +208,18 @@ export function useTree(props: any, dataSourceCopy: ShallowRef<DT[]>, rowKeyGen:
|
|
|
119
208
|
return result;
|
|
120
209
|
}
|
|
121
210
|
|
|
211
|
+
/**
|
|
212
|
+
* 递归设置目标节点后代到指定层级的展开/折叠状态
|
|
213
|
+
* en: Recursively set expand/collapse state for descendants up to the target level
|
|
214
|
+
*/
|
|
215
|
+
function setDescendantsToLevel(row: DT, currentLevel: number, targetLevel: number, expanded: boolean) {
|
|
216
|
+
if (!row.children || currentLevel > targetLevel) return;
|
|
217
|
+
for (const child of row.children) {
|
|
218
|
+
setNodeExpanded(child, expanded, currentLevel, row);
|
|
219
|
+
setDescendantsToLevel(child, currentLevel + 1, targetLevel, expanded);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
122
223
|
function expandNode(row: DT, level: number) {
|
|
123
224
|
let result: DT[] = [];
|
|
124
225
|
row.children &&
|
|
@@ -85,6 +85,13 @@ function useColWidthCache<T extends { fixed?: StkTableColumn<PrivateRowDT>['fixe
|
|
|
85
85
|
/** vue2 优化滚动回收延时 */
|
|
86
86
|
const VUE2_SCROLL_TIMEOUT_MS = 200;
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 合并单元格可视范围修正的最大迭代次数。
|
|
90
|
+
* 合法(不重叠)的合并配置最多 2 轮即收敛(1 轮扩展 + 1 轮验证);
|
|
91
|
+
* 此上限仅针对重叠合并区域等病态配置兜底,防止修正循环过长。
|
|
92
|
+
*/
|
|
93
|
+
const MAX_MERGE_RANGE_EXPAND_ITERATIONS = 8;
|
|
94
|
+
|
|
88
95
|
/**
|
|
89
96
|
* virtual scroll
|
|
90
97
|
* @returns
|
|
@@ -170,24 +177,138 @@ export function useVirtualScroll(
|
|
|
170
177
|
/** 是否多级表头 */
|
|
171
178
|
const isMultiLevelHeader = computed(() => tableHeaders.value.length > 1);
|
|
172
179
|
|
|
180
|
+
/**
|
|
181
|
+
* 需要参与可视范围修正的合并列集合。仅依赖 tableHeaderLast(columns 变化),
|
|
182
|
+
* 缓存后避免在数据更新、纵向滚动换窗等高频调用 buildColMergeRange 时重复收集。
|
|
183
|
+
* 无需修正时返回 null。
|
|
184
|
+
*/
|
|
185
|
+
const virtualX_mergeColsInfo = computed(() => {
|
|
186
|
+
const headers = tableHeaderLast.value;
|
|
187
|
+
const headerLength = headers.length;
|
|
188
|
+
// 右固定列始终渲染在可视区末尾,不参与主可视区合并修正
|
|
189
|
+
let maxColIndex = headerLength;
|
|
190
|
+
const mergeCols: { col: PrivateStkTableColumn<PrivateRowDT>; index: number }[] = [];
|
|
191
|
+
for (let i = 0; i < headerLength; i++) {
|
|
192
|
+
const col = headers[i];
|
|
193
|
+
if (maxColIndex === headerLength && col.fixed === 'right') maxColIndex = i;
|
|
194
|
+
// 固定列始终渲染,不需要修正可视范围
|
|
195
|
+
if (col.mergeCells && !col.fixed) mergeCols.push({ col, index: i });
|
|
196
|
+
}
|
|
197
|
+
if (!mergeCols.length || !maxColIndex) return null;
|
|
198
|
+
return { headerLength, maxColIndex, mergeCols };
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 收集行集合中的列合并(colspan)区间
|
|
203
|
+
* @param rows 行数据
|
|
204
|
+
* @param rowIndexBase mergeCells 入参 rowIndex 的起始值
|
|
205
|
+
*/
|
|
206
|
+
function buildColMergeRange(rows: PrivateRowDT[], rowIndexBase: number) {
|
|
207
|
+
const mergeColsInfo = virtualX_mergeColsInfo.value;
|
|
208
|
+
if (!mergeColsInfo) return null;
|
|
209
|
+
const { headerLength, maxColIndex, mergeCols } = mergeColsInfo;
|
|
210
|
+
|
|
211
|
+
const leftReach = new Int32Array(headerLength).fill(-1);
|
|
212
|
+
const rightEnd = new Int32Array(headerLength);
|
|
213
|
+
for (let r = 0; r < rows.length; r++) {
|
|
214
|
+
const row = rows[r];
|
|
215
|
+
for (let m = 0; m < mergeCols.length; m++) {
|
|
216
|
+
const { col, index } = mergeCols[m];
|
|
217
|
+
if (index >= maxColIndex) break;
|
|
218
|
+
const { colspan = 1 } = col.mergeCells!({ row, col, rowIndex: rowIndexBase + r, colIndex: index }) || {};
|
|
219
|
+
if (colspan > 1) {
|
|
220
|
+
const end = Math.min(index + colspan, maxColIndex);
|
|
221
|
+
for (let c = index; c < end; c++) {
|
|
222
|
+
if (leftReach[c] === -1 || index < leftReach[c]) leftReach[c] = index;
|
|
223
|
+
if (end > rightEnd[c]) rightEnd[c] = end;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { leftReach, rightEnd };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 全量数据的列合并覆盖信息(懒计算)。
|
|
233
|
+
* 仅在 Y 虚拟滚动未开启(可视行即全量数据)且被消费时才遍历全量数据;
|
|
234
|
+
* 依赖 dataSourceCopy 与 columns,数据/列变化时自动重算。
|
|
235
|
+
* virtual + virtualX 模式下消费方走可视行分支,本 computed 直接返回 null,
|
|
236
|
+
* 避免 O(rows × mergeCols) 的全量 mergeCells 调用浪费。
|
|
237
|
+
*/
|
|
238
|
+
const virtualX_colMergeRangeFull = computed(() => {
|
|
239
|
+
if (!virtualX_on.value || virtual_on.value) return null;
|
|
240
|
+
return buildColMergeRange(dataSourceCopy.value, 0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* virtual-x 列合并覆盖信息。
|
|
245
|
+
* Y 虚拟滚动开启时仅计算可视行(rowIndex 与渲染时 mergeCells 入参保持一致);
|
|
246
|
+
* 否则使用全量数据懒计算缓存(此时可视行即全量数据)。
|
|
247
|
+
*/
|
|
248
|
+
const virtualX_colMergeRange = computed(() => {
|
|
249
|
+
if (!virtualX_on.value) return null;
|
|
250
|
+
if (virtual_on.value) {
|
|
251
|
+
return buildColMergeRange(virtual_dataSourcePart.value, 0);
|
|
252
|
+
}
|
|
253
|
+
return virtualX_colMergeRangeFull.value;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 经列合并修正后的可视列范围。
|
|
258
|
+
* 合并单元格的锚点列可能已滚出可视区(但合并区域仍覆盖可视列),
|
|
259
|
+
* 也可能合并区域超出可视区右边界,需要将范围扩展到能完整渲染合并单元格。
|
|
260
|
+
*/
|
|
261
|
+
const virtualX_colRange = computed(() => {
|
|
262
|
+
let { startIndex, endIndex } = virtualScrollX.value;
|
|
263
|
+
const mergeRange = virtualX_colMergeRange.value;
|
|
264
|
+
if (mergeRange) {
|
|
265
|
+
const { leftReach, rightEnd } = mergeRange;
|
|
266
|
+
const len = leftReach.length;
|
|
267
|
+
// 扩展后的范围可能引入新的合并单元格,迭代直到稳定(合法配置最多 2 轮,上限兜底病态配置)
|
|
268
|
+
for (let k = 0; k < MAX_MERGE_RANGE_EXPAND_ITERATIONS; k++) {
|
|
269
|
+
let newStart = startIndex;
|
|
270
|
+
let newEnd = endIndex;
|
|
271
|
+
const loopEnd = Math.min(endIndex, len);
|
|
272
|
+
for (let i = Math.max(0, startIndex); i < loopEnd; i++) {
|
|
273
|
+
const reach = leftReach[i];
|
|
274
|
+
if (reach > -1 && reach < newStart) newStart = reach;
|
|
275
|
+
if (rightEnd[i] > newEnd) newEnd = rightEnd[i];
|
|
276
|
+
}
|
|
277
|
+
if (newStart === startIndex && newEnd === endIndex) break;
|
|
278
|
+
startIndex = newStart;
|
|
279
|
+
endIndex = newEnd;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { startIndex, endIndex };
|
|
283
|
+
});
|
|
284
|
+
|
|
173
285
|
/**
|
|
174
286
|
* 多级表头横向虚拟滚动参数:以顶层列组为单位计算开始/结束位置。
|
|
175
287
|
* - 只有整个顶层组完全滚出视口时才移除(避免 colSpan 变化导致抖动)。
|
|
176
288
|
* - 单级表头时退化为与 tbody 相同的参数。
|
|
289
|
+
* - 范围基于列合并修正后的 virtualX_colRange,保证合并单元格完整渲染。
|
|
177
290
|
*/
|
|
178
291
|
const theadVirtualX = computed(() => {
|
|
179
|
-
if (!virtualX_on.value
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
endIndex: virtualScrollX.value.endIndex,
|
|
183
|
-
offsetLeft: virtualScrollX.value.offsetLeft,
|
|
184
|
-
};
|
|
292
|
+
if (!virtualX_on.value) {
|
|
293
|
+
const { startIndex, endIndex, offsetLeft } = virtualScrollX.value;
|
|
294
|
+
return { startIndex, endIndex, offsetLeft };
|
|
185
295
|
}
|
|
186
|
-
|
|
296
|
+
|
|
297
|
+
const { startIndex, endIndex } = virtualX_colRange.value;
|
|
298
|
+
|
|
299
|
+
if (!isMultiLevelHeader.value) {
|
|
300
|
+
// 单级表头:offsetLeft 为起始列之前所有非固定列宽度之和
|
|
301
|
+
const { nonFixedCols } = getColWidthCache(tableHeaderLast.value);
|
|
302
|
+
const found = binarySearch(nonFixedCols, mid => (nonFixedCols[mid].index < startIndex ? -1 : 1));
|
|
303
|
+
const offsetLeft = found > 0 ? nonFixedCols[found - 1].cumWidth : 0;
|
|
304
|
+
return { startIndex, endIndex, offsetLeft };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 多级表头:保留与修正后可视范围有交集的顶层列组
|
|
187
308
|
const topLevelCols = tableHeaders.value[0];
|
|
188
309
|
const totalLeafCount = tableHeaderLast.value.length;
|
|
189
310
|
|
|
190
|
-
let theadStartIndex =
|
|
311
|
+
let theadStartIndex = totalLeafCount;
|
|
191
312
|
let theadEndIndex = totalLeafCount;
|
|
192
313
|
let theadOffsetLeft = 0;
|
|
193
314
|
let cumLeft = 0;
|
|
@@ -197,25 +318,23 @@ export function useVirtualScroll(
|
|
|
197
318
|
const col = topLevelCols[i];
|
|
198
319
|
if (col.fixed === 'left' || col.fixed === 'right') continue;
|
|
199
320
|
|
|
321
|
+
const groupStart = col.__LF_S__ ?? 0;
|
|
322
|
+
const groupEnd = col.__LF_E__ ?? groupStart + 1;
|
|
200
323
|
const groupWidth = col.__W__ || getCalculatedColWidth(col);
|
|
201
|
-
const groupRight = cumLeft + groupWidth;
|
|
202
324
|
|
|
203
|
-
if (!foundStart &&
|
|
325
|
+
if (!foundStart && groupEnd > startIndex) {
|
|
204
326
|
foundStart = true;
|
|
205
|
-
theadStartIndex =
|
|
327
|
+
theadStartIndex = groupStart;
|
|
206
328
|
theadOffsetLeft = cumLeft;
|
|
207
329
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (foundStart && groupRight >= scrollLeft + containerWidth) {
|
|
212
|
-
// find end
|
|
213
|
-
break;
|
|
330
|
+
if (foundStart) {
|
|
331
|
+
if (groupStart >= endIndex) break;
|
|
332
|
+
theadEndIndex = groupEnd;
|
|
214
333
|
}
|
|
334
|
+
cumLeft += groupWidth;
|
|
215
335
|
}
|
|
216
336
|
|
|
217
337
|
if (!foundStart) {
|
|
218
|
-
theadStartIndex = totalLeafCount;
|
|
219
338
|
theadOffsetLeft = cumLeft;
|
|
220
339
|
}
|
|
221
340
|
|
|
@@ -225,7 +344,8 @@ export function useVirtualScroll(
|
|
|
225
344
|
const virtualX_columnPart = computed(() => {
|
|
226
345
|
const tableHeaderLastValue = tableHeaderLast.value;
|
|
227
346
|
if (virtualX_on.value) {
|
|
228
|
-
|
|
347
|
+
// 使用列合并修正后的可视范围
|
|
348
|
+
const { startIndex, endIndex } = virtualX_colRange.value;
|
|
229
349
|
// 将索引钳制到列数组范围内,防止列数减少时越界
|
|
230
350
|
const maxIndex = tableHeaderLastValue.length;
|
|
231
351
|
const validEndIndex = Math.min(endIndex, maxIndex);
|
|
@@ -324,8 +444,8 @@ export function useVirtualScroll(
|
|
|
324
444
|
|
|
325
445
|
const virtualX_offsetRight = computed(() => {
|
|
326
446
|
if (!virtualX_on.value) return 0;
|
|
327
|
-
// 多级表头使用 theadEndIndex
|
|
328
|
-
const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex :
|
|
447
|
+
// 多级表头使用 theadEndIndex,单级使用列合并修正后的 endIndex
|
|
448
|
+
const endIndex = isMultiLevelHeader.value ? theadVirtualX.value.endIndex : virtualX_colRange.value.endIndex;
|
|
329
449
|
let width = 0;
|
|
330
450
|
const tableHeaderLastValue = tableHeaderLast.value;
|
|
331
451
|
for (let i = endIndex; i < tableHeaderLastValue.length; i++) {
|