@jetlinks-web/components 3.2.1 → 3.2.3
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/es/EditTable/CellRender.js +58 -27
- package/es/EditTable/EditTable.js +9 -4
- package/es/EditTable/FormItem.js +94 -80
- package/es/Search/hooks/index.js +1 -0
- package/es/VirtualTable/Table.js +205 -196
- package/es/VirtualTable/useVirtualScroll.js +112 -55
- package/lib/EditTable/CellRender.js +58 -27
- package/lib/EditTable/EditTable.js +9 -4
- package/lib/EditTable/FormItem.js +94 -80
- package/lib/Search/hooks/index.js +1 -0
- package/lib/VirtualTable/Table.js +204 -195
- package/lib/VirtualTable/useVirtualScroll.js +111 -54
- package/package.json +3 -3
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { computed,
|
|
1
|
+
import { computed, watch, isRef, shallowRef } from 'vue';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 高性能虚拟滚动 Hook
|
|
4
|
+
* 优化点:
|
|
5
|
+
* 1. 使用累积高度缓存,避免重复计算
|
|
6
|
+
* 2. 使用二分查找快速定位索引
|
|
7
|
+
* 3. 使用 shallowRef 减少响应式开销
|
|
8
|
+
* 4. 滚动节流减少计算频率
|
|
4
9
|
*/
|
|
5
10
|
export function useVirtualScroll(options) {
|
|
6
11
|
var itemCount = options.itemCount,
|
|
@@ -11,6 +16,9 @@ export function useVirtualScroll(options) {
|
|
|
11
16
|
overscan = _options$overscan === void 0 ? 5 : _options$overscan,
|
|
12
17
|
_options$threshold = options.threshold,
|
|
13
18
|
threshold = _options$threshold === void 0 ? 100 : _options$threshold;
|
|
19
|
+
// 固定行高模式(性能更高)
|
|
20
|
+
var isFixedHeight = typeof itemHeight === 'number';
|
|
21
|
+
var fixedItemHeight = isFixedHeight ? itemHeight : 54;
|
|
14
22
|
// 容器高度(支持响应式)
|
|
15
23
|
var containerHeight = computed(function () {
|
|
16
24
|
if (isRef(containerHeightOption)) {
|
|
@@ -18,23 +26,30 @@ export function useVirtualScroll(options) {
|
|
|
18
26
|
}
|
|
19
27
|
return containerHeightOption;
|
|
20
28
|
});
|
|
21
|
-
// 滚动位置
|
|
22
|
-
var scrollTop =
|
|
23
|
-
//
|
|
24
|
-
var itemHeights =
|
|
29
|
+
// 滚动位置 - 使用 shallowRef 减少响应式开销
|
|
30
|
+
var scrollTop = shallowRef(0);
|
|
31
|
+
// 动态行高缓存(仅在非固定高度模式使用)
|
|
32
|
+
var itemHeights = shallowRef(new Map());
|
|
33
|
+
// 累积高度缓存 - 用于二分查找
|
|
34
|
+
var accumulatedHeights = shallowRef([]);
|
|
35
|
+
var heightsCacheValid = false;
|
|
25
36
|
/**
|
|
26
37
|
* 获取指定索引的行高
|
|
27
38
|
*/
|
|
28
39
|
var getItemHeight = function getItemHeight(index) {
|
|
40
|
+
if (isFixedHeight) {
|
|
41
|
+
return fixedItemHeight;
|
|
42
|
+
}
|
|
29
43
|
// 优先使用缓存的高度
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
var cached = itemHeights.value.get(index);
|
|
45
|
+
if (cached !== undefined) {
|
|
46
|
+
return cached;
|
|
32
47
|
}
|
|
33
|
-
//
|
|
48
|
+
// 使用配置的高度函数
|
|
34
49
|
if (typeof itemHeight === 'function') {
|
|
35
50
|
return itemHeight(index);
|
|
36
51
|
}
|
|
37
|
-
return
|
|
52
|
+
return fixedItemHeight;
|
|
38
53
|
};
|
|
39
54
|
/**
|
|
40
55
|
* 设置行高(用于动态测量)
|
|
@@ -42,65 +57,101 @@ export function useVirtualScroll(options) {
|
|
|
42
57
|
var setItemHeight = function setItemHeight(index, height) {
|
|
43
58
|
if (height > 0 && itemHeights.value.get(index) !== height) {
|
|
44
59
|
itemHeights.value.set(index, height);
|
|
60
|
+
heightsCacheValid = false; // 使缓存失效
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* 重建累积高度缓存
|
|
65
|
+
*/
|
|
66
|
+
var rebuildAccumulatedHeights = function rebuildAccumulatedHeights() {
|
|
67
|
+
if (heightsCacheValid) return;
|
|
68
|
+
var count = itemCount.value;
|
|
69
|
+
var heights = new Array(count + 1);
|
|
70
|
+
heights[0] = 0;
|
|
71
|
+
if (isFixedHeight) {
|
|
72
|
+
// 固定高度模式,直接计算
|
|
73
|
+
for (var i = 0; i < count; i++) {
|
|
74
|
+
heights[i + 1] = heights[i] + fixedItemHeight;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// 动态高度模式
|
|
78
|
+
for (var _i = 0; _i < count; _i++) {
|
|
79
|
+
heights[_i + 1] = heights[_i] + getItemHeight(_i);
|
|
80
|
+
}
|
|
45
81
|
}
|
|
82
|
+
accumulatedHeights.value = heights;
|
|
83
|
+
heightsCacheValid = true;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* 二分查找:找到第一个累积高度大于 target 的索引
|
|
87
|
+
*/
|
|
88
|
+
var binarySearchIndex = function binarySearchIndex(target) {
|
|
89
|
+
rebuildAccumulatedHeights();
|
|
90
|
+
var heights = accumulatedHeights.value;
|
|
91
|
+
if (heights.length === 0) return 0;
|
|
92
|
+
var left = 0;
|
|
93
|
+
var right = heights.length - 1;
|
|
94
|
+
while (left < right) {
|
|
95
|
+
var mid = Math.floor((left + right) / 2);
|
|
96
|
+
if (heights[mid] <= target) {
|
|
97
|
+
left = mid + 1;
|
|
98
|
+
} else {
|
|
99
|
+
right = mid;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return Math.max(0, left - 1);
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* 获取累积高度
|
|
106
|
+
*/
|
|
107
|
+
var getAccumulatedHeight = function getAccumulatedHeight(index) {
|
|
108
|
+
rebuildAccumulatedHeights();
|
|
109
|
+
var heights = accumulatedHeights.value;
|
|
110
|
+
if (index < 0) return 0;
|
|
111
|
+
if (index >= heights.length) return heights[heights.length - 1] || 0;
|
|
112
|
+
return heights[index];
|
|
46
113
|
};
|
|
47
114
|
/**
|
|
48
115
|
* 计算总高度
|
|
49
116
|
*/
|
|
50
117
|
var totalHeight = computed(function () {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
total += getItemHeight(i);
|
|
118
|
+
if (isFixedHeight) {
|
|
119
|
+
return itemCount.value * fixedItemHeight;
|
|
54
120
|
}
|
|
55
|
-
|
|
121
|
+
rebuildAccumulatedHeights();
|
|
122
|
+
var heights = accumulatedHeights.value;
|
|
123
|
+
return heights.length > 0 ? heights[heights.length - 1] : 0;
|
|
56
124
|
});
|
|
57
125
|
/**
|
|
58
|
-
* 计算可视范围的起始索引
|
|
126
|
+
* 计算可视范围的起始索引 - 使用二分查找 O(log n)
|
|
59
127
|
*/
|
|
60
128
|
var startIndex = computed(function () {
|
|
61
|
-
|
|
129
|
+
var count = itemCount.value;
|
|
130
|
+
if (count < threshold) {
|
|
62
131
|
return 0;
|
|
63
132
|
}
|
|
64
|
-
var
|
|
65
|
-
|
|
66
|
-
var height = getItemHeight(i);
|
|
67
|
-
if (accumulated + height > scrollTop.value) {
|
|
68
|
-
return Math.max(0, i - overscan);
|
|
69
|
-
}
|
|
70
|
-
accumulated += height;
|
|
71
|
-
}
|
|
72
|
-
return 0;
|
|
133
|
+
var index = binarySearchIndex(scrollTop.value);
|
|
134
|
+
return Math.max(0, index - overscan);
|
|
73
135
|
});
|
|
74
136
|
/**
|
|
75
137
|
* 计算可视范围的结束索引
|
|
76
138
|
*/
|
|
77
139
|
var endIndex = computed(function () {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
var start = startIndex.value;
|
|
82
|
-
var accumulated = 0;
|
|
83
|
-
// 从起始索引开始累加
|
|
84
|
-
for (var i = 0; i < start; i++) {
|
|
85
|
-
accumulated += getItemHeight(i);
|
|
86
|
-
}
|
|
87
|
-
for (var _i = start; _i < itemCount.value; _i++) {
|
|
88
|
-
accumulated += getItemHeight(_i);
|
|
89
|
-
if (accumulated > scrollTop.value + containerHeight.value) {
|
|
90
|
-
return Math.min(itemCount.value, _i + 1 + overscan);
|
|
91
|
-
}
|
|
140
|
+
var count = itemCount.value;
|
|
141
|
+
if (count < threshold) {
|
|
142
|
+
return count;
|
|
92
143
|
}
|
|
93
|
-
|
|
144
|
+
var targetHeight = scrollTop.value + containerHeight.value;
|
|
145
|
+
var index = binarySearchIndex(targetHeight);
|
|
146
|
+
return Math.min(count, index + 1 + overscan);
|
|
94
147
|
});
|
|
95
148
|
/**
|
|
96
149
|
* 顶部偏移量
|
|
97
150
|
*/
|
|
98
151
|
var offsetY = computed(function () {
|
|
99
|
-
var
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
return offset;
|
|
152
|
+
var start = startIndex.value;
|
|
153
|
+
if (start === 0) return 0;
|
|
154
|
+
return getAccumulatedHeight(start);
|
|
104
155
|
});
|
|
105
156
|
/**
|
|
106
157
|
* 可见行数
|
|
@@ -108,20 +159,29 @@ export function useVirtualScroll(options) {
|
|
|
108
159
|
var visibleCount = computed(function () {
|
|
109
160
|
return endIndex.value - startIndex.value;
|
|
110
161
|
});
|
|
162
|
+
// 滚动节流
|
|
163
|
+
var scrollRAF = null;
|
|
111
164
|
/**
|
|
112
|
-
*
|
|
165
|
+
* 处理滚动事件(带节流)
|
|
113
166
|
*/
|
|
114
167
|
var handleScroll = function handleScroll(e) {
|
|
115
168
|
var target = e.target;
|
|
116
|
-
if (target)
|
|
117
|
-
|
|
169
|
+
if (!target) return;
|
|
170
|
+
// 使用 RAF 节流
|
|
171
|
+
if (scrollRAF !== null) {
|
|
172
|
+
cancelAnimationFrame(scrollRAF);
|
|
118
173
|
}
|
|
174
|
+
scrollRAF = requestAnimationFrame(function () {
|
|
175
|
+
scrollTop.value = target.scrollTop;
|
|
176
|
+
scrollRAF = null;
|
|
177
|
+
});
|
|
119
178
|
};
|
|
120
179
|
/**
|
|
121
180
|
* 滚动到指定位置
|
|
122
181
|
*/
|
|
123
182
|
var scrollTo = function scrollTo(offset) {
|
|
124
|
-
|
|
183
|
+
var maxScroll = Math.max(0, totalHeight.value - containerHeight.value);
|
|
184
|
+
scrollTop.value = Math.max(0, Math.min(offset, maxScroll));
|
|
125
185
|
};
|
|
126
186
|
/**
|
|
127
187
|
* 滚动到指定索引
|
|
@@ -130,14 +190,11 @@ export function useVirtualScroll(options) {
|
|
|
130
190
|
if (index < 0 || index >= itemCount.value) {
|
|
131
191
|
return;
|
|
132
192
|
}
|
|
133
|
-
|
|
134
|
-
for (var i = 0; i < index; i++) {
|
|
135
|
-
offset += getItemHeight(i);
|
|
136
|
-
}
|
|
137
|
-
scrollTo(offset);
|
|
193
|
+
scrollTo(getAccumulatedHeight(index));
|
|
138
194
|
};
|
|
139
|
-
//
|
|
195
|
+
// 数据变化时使缓存失效并重置滚动位置
|
|
140
196
|
watch(itemCount, function () {
|
|
197
|
+
heightsCacheValid = false;
|
|
141
198
|
var maxScroll = Math.max(0, totalHeight.value - containerHeight.value);
|
|
142
199
|
if (scrollTop.value > maxScroll) {
|
|
143
200
|
scrollTop.value = maxScroll;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) {
|
|
2
|
+
return lhs;
|
|
3
|
+
}
|
|
4
|
+
else {
|
|
5
|
+
return rhsFn();
|
|
6
|
+
} }
|
|
1
7
|
function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) {
|
|
2
8
|
const op = ops[i];
|
|
3
9
|
const fn = ops[i + 1];
|
|
@@ -15,10 +21,15 @@ function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]
|
|
|
15
21
|
}
|
|
16
22
|
} return value; }
|
|
17
23
|
/* Analyzed bindings: {} */
|
|
18
|
-
import { defineComponent, h, shallowRef, } from 'vue';
|
|
24
|
+
import { defineComponent, h, shallowRef, markRaw } from 'vue';
|
|
19
25
|
/**
|
|
20
|
-
* CellRender -
|
|
21
|
-
*
|
|
26
|
+
* CellRender - 高性能单元格渲染组件
|
|
27
|
+
*
|
|
28
|
+
* 优化点:
|
|
29
|
+
* 1. 使用 shallowRef 避免深度响应式
|
|
30
|
+
* 2. 缓存渲染结果,避免不必要的重渲染
|
|
31
|
+
* 3. 使用 markRaw 标记渲染结果,避免被 Vue 代理
|
|
32
|
+
* 4. 智能对比 record 的 key 变化
|
|
22
33
|
*/
|
|
23
34
|
const __sfc_main__ = defineComponent({
|
|
24
35
|
name: "CellRender",
|
|
@@ -35,43 +46,63 @@ const __sfc_main__ = defineComponent({
|
|
|
35
46
|
index: {
|
|
36
47
|
type: Number,
|
|
37
48
|
required: true
|
|
49
|
+
},
|
|
50
|
+
// 可选:指定用于对比的 key 字段
|
|
51
|
+
rowKey: {
|
|
52
|
+
type: String,
|
|
53
|
+
default: 'id'
|
|
38
54
|
}
|
|
39
55
|
},
|
|
40
56
|
setup(props) {
|
|
41
57
|
// 使用 shallowRef 避免深度响应式
|
|
42
58
|
const cachedResult = shallowRef(null);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
// 缓存上次渲染的关键信息
|
|
60
|
+
let lastValue = Symbol('initial');
|
|
61
|
+
let lastRecordKey = Symbol('initial');
|
|
62
|
+
let lastIndex = -1;
|
|
63
|
+
// 获取 record 的唯一标识
|
|
64
|
+
const getRecordKey = (record) => {
|
|
65
|
+
return _nullishCoalesce(_nullishCoalesce(_optionalChain([record, 'optionalAccess', _ => _[props.rowKey]]), () => (_optionalChain([record, 'optionalAccess', _2 => _2.key]))), () => (_optionalChain([record, 'optionalAccess', _3 => _3.__dataIndex])));
|
|
66
|
+
};
|
|
67
|
+
// 检查是否需要重新渲染
|
|
68
|
+
const shouldRerender = () => {
|
|
69
|
+
const currentRecordKey = getRecordKey(props.record);
|
|
70
|
+
// 快速检查:index 和 key 都没变,value 也没变
|
|
71
|
+
if (lastIndex === props.index &&
|
|
72
|
+
lastRecordKey === currentRecordKey &&
|
|
73
|
+
lastValue === props.value) {
|
|
74
|
+
return false;
|
|
55
75
|
}
|
|
56
|
-
return
|
|
76
|
+
return true;
|
|
57
77
|
};
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
// 执行渲染
|
|
79
|
+
const doRender = () => {
|
|
80
|
+
if (!shouldRerender() && cachedResult.value !== null) {
|
|
81
|
+
return cachedResult.value;
|
|
82
|
+
}
|
|
83
|
+
// 更新缓存的对比值
|
|
84
|
+
lastValue = props.value;
|
|
85
|
+
lastRecordKey = getRecordKey(props.record);
|
|
86
|
+
lastIndex = props.index;
|
|
87
|
+
try {
|
|
88
|
+
const result = props.renderFn(props.value, props.record, props.index);
|
|
89
|
+
// 使用 markRaw 避免渲染结果被代理
|
|
90
|
+
cachedResult.value = result != null ? markRaw({ node: result }) : null;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (import.meta.env.DEV) {
|
|
67
94
|
console.error('[CellRender] Render error:', error);
|
|
68
|
-
cachedResult.value = null;
|
|
69
95
|
}
|
|
96
|
+
cachedResult.value = null;
|
|
70
97
|
}
|
|
71
98
|
return cachedResult.value;
|
|
72
99
|
};
|
|
73
100
|
return () => {
|
|
74
|
-
|
|
101
|
+
const result = doRender();
|
|
102
|
+
return h('div', {
|
|
103
|
+
class: 'cell-render-wrapper',
|
|
104
|
+
style: { display: 'contents' } // 避免额外的 DOM 层级影响布局
|
|
105
|
+
}, _nullishCoalesce(_optionalChain([result, 'optionalAccess', _4 => _4.node]), () => (null)));
|
|
75
106
|
};
|
|
76
107
|
}
|
|
77
108
|
});
|
|
@@ -454,21 +454,26 @@ const __sfc_main__ = _defineComponent({
|
|
|
454
454
|
]),
|
|
455
455
|
_createElementVNode("div", _hoisted_2, [
|
|
456
456
|
_createElementVNode("div", _hoisted_3, [
|
|
457
|
-
_createVNode(_unref(VirtualTable), _mergeProps(
|
|
457
|
+
_createVNode(_unref(VirtualTable), _mergeProps({
|
|
458
|
+
ref_key: "virtualTableRef",
|
|
459
|
+
ref: virtualTableRef
|
|
460
|
+
}, props, {
|
|
458
461
|
"data-source": bodyDataSource.value,
|
|
459
462
|
columns: newColumns.value,
|
|
460
463
|
scroll: scroll.value,
|
|
461
464
|
pagination: false,
|
|
462
465
|
virtual: {
|
|
463
466
|
itemHeight: props.cellHeight,
|
|
464
|
-
overscan:
|
|
467
|
+
overscan: 5,
|
|
465
468
|
threshold: props.height / props.cellHeight
|
|
466
469
|
}
|
|
467
470
|
}), {
|
|
468
|
-
bodyCell: _withCtx(({ column, record }) => [
|
|
471
|
+
bodyCell: _withCtx(({ column, record, index }) => [
|
|
469
472
|
_renderSlot(_ctx.$slots, column.dataIndex, {
|
|
470
473
|
column: column,
|
|
471
|
-
record: record
|
|
474
|
+
record: record,
|
|
475
|
+
index: record.__dataIndex,
|
|
476
|
+
visibleIndex: index
|
|
472
477
|
})
|
|
473
478
|
]),
|
|
474
479
|
_: 3 /* FORWARDED */
|