@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.
@@ -1,6 +1,11 @@
1
- import { computed, ref, watch, isRef } from 'vue';
1
+ import { computed, watch, isRef, shallowRef } from 'vue';
2
2
  /**
3
- * 虚拟滚动 Hook
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 = ref(0);
23
- // 动态行高缓存
24
- var itemHeights = ref(new Map());
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
- if (itemHeights.value.has(index)) {
31
- return itemHeights.value.get(index);
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 itemHeight;
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
- var total = 0;
52
- for (var i = 0; i < itemCount.value; i++) {
53
- total += getItemHeight(i);
118
+ if (isFixedHeight) {
119
+ return itemCount.value * fixedItemHeight;
54
120
  }
55
- return total;
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
- if (itemCount.value < threshold) {
129
+ var count = itemCount.value;
130
+ if (count < threshold) {
62
131
  return 0;
63
132
  }
64
- var accumulated = 0;
65
- for (var i = 0; i < itemCount.value; i++) {
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
- if (itemCount.value < threshold) {
79
- return itemCount.value;
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
- return itemCount.value;
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 offset = 0;
100
- for (var i = 0; i < startIndex.value; i++) {
101
- offset += getItemHeight(i);
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
- scrollTop.value = target.scrollTop;
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
- scrollTop.value = Math.max(0, Math.min(offset, totalHeight.value - containerHeight.value));
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
- var offset = 0;
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
- * 使用 shallowRef 和手动更新检查来避免不必要的重渲染
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
- const lastValue = shallowRef(undefined);
44
- const lastRecordId = shallowRef(undefined);
45
- // 手动检查是否需要重新渲染
46
- const shouldUpdate = () => {
47
- // 检查 value 是否变化
48
- if (props.value !== lastValue.value) {
49
- return true;
50
- }
51
- // 检查 record 的关键属性是否变化(假设有 id 或 key)
52
- const currentRecordId = _optionalChain([(props.record), 'optionalAccess', _ => _.id]) || _optionalChain([(props.record), 'optionalAccess', _2 => _2.key]);
53
- if (currentRecordId !== lastRecordId.value) {
54
- return true;
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 false;
76
+ return true;
57
77
  };
58
- // 执行渲染函数
59
- const render = () => {
60
- if (shouldUpdate() || cachedResult.value === null) {
61
- lastValue.value = props.value;
62
- lastRecordId.value = _optionalChain([(props.record), 'optionalAccess', _3 => _3.id]) || _optionalChain([(props.record), 'optionalAccess', _4 => _4.key]);
63
- try {
64
- cachedResult.value = props.renderFn(props.value, props.record, props.index);
65
- }
66
- catch (error) {
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
- return h('div', { class: 'cell-render-wrapper' }, [render()]);
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(props, {
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: 1,
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 */