@mx-sose-front/mx-sose-graph 1.1.8 → 1.1.9

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.
Files changed (52) hide show
  1. package/dist/assets/edgeWorker-b57ca007.js +2 -0
  2. package/dist/assets/edgeWorker-b57ca007.js.map +1 -0
  3. package/dist/index.d.ts +633 -30
  4. package/dist/index.esm.js +8728 -4734
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.umd.js +1 -1
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/style.css +1 -1
  9. package/package.json +1 -1
  10. package/src/components/Common/Tree.vue +451 -0
  11. package/src/components/Common/index.ts +2 -0
  12. package/src/components/DiagramListTooltip/DiagramListTooltip.vue +1 -2
  13. package/src/components/Edge/Edge.vue +172 -169
  14. package/src/components/Gantt/Gantt.vue +1544 -0
  15. package/src/components/GanttContextMenu/GanttContextMenu.vue +304 -0
  16. package/src/components/InteractionLayer.vue +343 -147
  17. package/src/components/Matrix/Matrix.vue +828 -0
  18. package/src/components/Matrix/index.ts +168 -0
  19. package/src/components/Shape/ConceptualRole.vue +2 -34
  20. package/src/components/Table/Table.vue +970 -0
  21. package/src/constants/edgeShapeKeys.ts +8 -5
  22. package/src/constants/index.ts +259 -45
  23. package/src/hooks/index.ts +2 -0
  24. package/src/hooks/useChartRowSelection.ts +456 -0
  25. package/src/hooks/useResize.ts +2 -2
  26. package/src/hooks/useVirtualScroll.ts +258 -0
  27. package/src/index.ts +1 -1
  28. package/src/render/shape-renderer.ts +62 -2
  29. package/src/statics/icons/childIcons//345/221/275/344/273/244@3x.png +0 -0
  30. package/src/statics/icons/childIcons//346/210/230/347/225/245/346/246/202/345/277/265/350/241/250@3x.png +0 -0
  31. package/src/statics/icons/childIcons//346/216/247/345/210/266@3x.png +0 -0
  32. package/src/statics/icons/createMenu/down.png +0 -0
  33. package/src/statics/icons/createMenu/remove.png +0 -0
  34. package/src/statics/icons/createMenu/up.png +0 -0
  35. package/src/store/graphStore.ts +217 -44
  36. package/src/types/index.ts +86 -4
  37. package/src/utils/batchAutoExpand.ts +9 -10
  38. package/src/utils/containers.ts +72 -17
  39. package/src/utils/contextMenuUtils.ts +7 -7
  40. package/src/utils/dateUtils.ts +160 -0
  41. package/src/utils/diagram.ts +10 -8
  42. package/src/utils/drag.ts +6 -5
  43. package/src/utils/edgeUtils.ts +344 -427
  44. package/src/utils/edgeWorker.ts +471 -0
  45. package/src/utils/hittest.ts +37 -38
  46. package/src/utils/index.ts +3 -0
  47. package/src/utils/keyboardUtils.ts +5 -5
  48. package/src/utils/packageOutline.ts +96 -0
  49. package/src/utils/rafThrottle.ts +162 -0
  50. package/src/utils/workerManager.ts +335 -0
  51. package/src/view/graph.vue +47 -33
  52. /package/src/statics/icons/childIcons//346/210/230/347/225/{245@3x.png" → 245/345/261/202@3x.png"} +0 -0
@@ -0,0 +1,456 @@
1
+ import { computed, ref, watch, type Ref } from 'vue'
2
+
3
+ import { eventBus } from '../store/eventBus'
4
+
5
+ /**
6
+ * 行数据的最小结构约束
7
+ */
8
+ interface ChartRowItem {
9
+ id: string
10
+ modelId: string
11
+ type: string
12
+ [key: string]: any
13
+ }
14
+
15
+ /**
16
+ * 移除操作的上下文
17
+ * 用于在接口成功后按原位置恢复选中状态
18
+ */
19
+ interface RemoveContext {
20
+ removedIds: string[]
21
+ selectedIndices: number[]
22
+ }
23
+
24
+ /**
25
+ * 行选择 Hook 配置项
26
+ */
27
+ interface UseChartRowSelectionOptions<T extends ChartRowItem> {
28
+ /**
29
+ * 当前表格或甘特图的行数据
30
+ */
31
+ items: Ref<T[]>
32
+
33
+ /**
34
+ * 行选中状态变化后向外抛出的事件名
35
+ */
36
+ rowSelectedEvent: string
37
+
38
+ /**
39
+ * 切换选中前触发
40
+ * 组件通常用它来关闭行内编辑器或日期选择器
41
+ */
42
+ onBeforeSelect?: () => void
43
+
44
+ /**
45
+ * 点击空白区域、清空选中时触发
46
+ */
47
+ onClearSelection?: () => void
48
+
49
+ /**
50
+ * 删除选中行并重置本地选中态后触发
51
+ */
52
+ onDeleteRows?: () => void
53
+
54
+ /**
55
+ * 移除成功并完成本地数据对齐后触发
56
+ */
57
+ onAfterRemoveSuccess?: () => void
58
+ }
59
+
60
+ /**
61
+ * 表格/甘特图行交互 Hook
62
+ *
63
+ * @description
64
+ * 抽离 Table 与 Gantt 共用的行交互逻辑,统一处理:
65
+ * 1. 单选、Ctrl 多选、Shift 连续选择
66
+ * 2. 右键菜单定位与菜单动作
67
+ * 3. 上移、下移、删除、移除
68
+ * 4. 移除成功后的按原位置回选
69
+ * 5. 工具栏依赖的选中状态通知
70
+ */
71
+ export function useChartRowSelection<T extends ChartRowItem>({
72
+ items,
73
+ rowSelectedEvent,
74
+ onBeforeSelect,
75
+ onClearSelection,
76
+ onDeleteRows,
77
+ onAfterRemoveSuccess,
78
+ }: UseChartRowSelectionOptions<T>) {
79
+ // 状态变量
80
+ const selectedRowIds = ref<Set<string>>(new Set())
81
+ const lastSelectedId = ref<string | null>(null)
82
+ const showContextMenu = ref(false)
83
+ const contextMenuPosition = ref({ x: 0, y: 0 })
84
+ const contextMenuTargetItem = ref<T | null>(null)
85
+
86
+ /**
87
+ * 缓存本次“移除”的上下文
88
+ * 后端确认移除成功后,再按原位置恢复选中
89
+ */
90
+ const pendingRemoveCtx = ref<RemoveContext | null>(null)
91
+
92
+ /**
93
+ * 处理行点击选中
94
+ * 支持单选、Ctrl 多选、Shift 连续选择
95
+ */
96
+ function selectRow(id: string, event?: MouseEvent) {
97
+ onBeforeSelect?.()
98
+
99
+ if (event?.ctrlKey || event?.metaKey) {
100
+ const nextSelected = new Set(selectedRowIds.value)
101
+ if (nextSelected.has(id)) {
102
+ nextSelected.delete(id)
103
+ } else {
104
+ nextSelected.add(id)
105
+ }
106
+ selectedRowIds.value = nextSelected
107
+ lastSelectedId.value = id
108
+ return
109
+ }
110
+
111
+ if (event?.shiftKey && lastSelectedId.value) {
112
+ const currentIndex = items.value.findIndex(item => item.id === id)
113
+ const lastIndex = items.value.findIndex(item => item.id === lastSelectedId.value)
114
+
115
+ if (currentIndex !== -1 && lastIndex !== -1) {
116
+ const start = Math.min(currentIndex, lastIndex)
117
+ const end = Math.max(currentIndex, lastIndex)
118
+ const nextSelected = new Set<string>()
119
+
120
+ for (let i = start; i <= end; i++) {
121
+ nextSelected.add(items.value[i].id)
122
+ }
123
+
124
+ selectedRowIds.value = nextSelected
125
+ }
126
+ return
127
+ }
128
+
129
+ const isSameRow = selectedRowIds.value.size === 1 && selectedRowIds.value.has(id)
130
+
131
+ selectedRowIds.value = new Set([id])
132
+ lastSelectedId.value = id
133
+
134
+ if (isSameRow) return
135
+
136
+ const selectedItem = items.value.find(item => item.id === id)
137
+ if (selectedItem) {
138
+ eventBus.emit('chart-row-click', selectedItem)
139
+ }
140
+ }
141
+
142
+ /**
143
+ * 清空选中状态
144
+ */
145
+ function clearSelection() {
146
+ selectedRowIds.value = new Set()
147
+ lastSelectedId.value = null
148
+ onClearSelection?.()
149
+ }
150
+
151
+ /**
152
+ * 向外同步当前选中状态
153
+ * 供工具栏判断上移 / 下移按钮是否可用
154
+ */
155
+ function emitRowSelected() {
156
+ if (selectedRowIds.value.size === 0) {
157
+ eventBus.emit(rowSelectedEvent, null, { isFirst: false, isLast: false })
158
+ return
159
+ }
160
+
161
+ const selectedIndices = getSelectedIndices()
162
+ let isContinuous = true
163
+
164
+ for (let i = 1; i < selectedIndices.length; i++) {
165
+ if (selectedIndices[i] - selectedIndices[i - 1] !== 1) {
166
+ isContinuous = false
167
+ break
168
+ }
169
+ }
170
+
171
+ const firstSelectedId = [...selectedRowIds.value][0]
172
+ if (!isContinuous) {
173
+ eventBus.emit(rowSelectedEvent, firstSelectedId, {
174
+ isFirst: true,
175
+ isLast: true,
176
+ })
177
+ return
178
+ }
179
+
180
+ const canMoveUp = selectedIndices.length > 0 && selectedIndices[0] > 0
181
+ const canMoveDown =
182
+ selectedIndices.length > 0 && selectedIndices[selectedIndices.length - 1] < items.value.length - 1
183
+
184
+ eventBus.emit(rowSelectedEvent, firstSelectedId, {
185
+ isFirst: !canMoveUp,
186
+ isLast: !canMoveDown,
187
+ })
188
+ }
189
+
190
+ /**
191
+ * 统一抛出行移动事件
192
+ */
193
+ function moveRows(direction: 'UP' | 'DOWN') {
194
+ if (selectedRowIds.value.size === 0) return
195
+
196
+ const selectedItems = items.value.filter(item => selectedRowIds.value.has(item.id))
197
+ eventBus.emit('gantt-move-rows', {
198
+ selectedItems,
199
+ allItems: items.value,
200
+ direction,
201
+ })
202
+ }
203
+
204
+ /**
205
+ * 上移选中行
206
+ */
207
+ function moveUp() {
208
+ moveRows('UP')
209
+ }
210
+
211
+ /**
212
+ * 下移选中行
213
+ */
214
+ function moveDown() {
215
+ moveRows('DOWN')
216
+ }
217
+
218
+ /**
219
+ * 删除选中行
220
+ */
221
+ function deleteRows() {
222
+ if (selectedRowIds.value.size === 0) return
223
+
224
+ const deletedRows = items.value.filter(item => selectedRowIds.value.has(item.id))
225
+ const deletePayload = deletedRows.map(item => ({
226
+ modelId: item.modelId,
227
+ shapeKey: item.type,
228
+ }))
229
+
230
+ eventBus.emit('chart-delete', deletePayload)
231
+ selectedRowIds.value = new Set()
232
+ lastSelectedId.value = null
233
+ onDeleteRows?.()
234
+ }
235
+
236
+ /**
237
+ * 打开右键菜单
238
+ */
239
+ function handleContextMenu(item: T, event: MouseEvent) {
240
+ event.preventDefault()
241
+ event.stopPropagation()
242
+
243
+ const isSameRow = selectedRowIds.value.size === 1 && selectedRowIds.value.has(item.id)
244
+
245
+ selectedRowIds.value = new Set([item.id])
246
+ lastSelectedId.value = item.id
247
+
248
+ if (!isSameRow) {
249
+ eventBus.emit('chart-row-click', item)
250
+ }
251
+
252
+ contextMenuTargetItem.value = item
253
+ contextMenuPosition.value = {
254
+ x: event.clientX,
255
+ y: event.clientY,
256
+ }
257
+ showContextMenu.value = true
258
+ }
259
+
260
+ /**
261
+ * 计算右键菜单目标行的位置
262
+ * 供菜单判断是否允许上移 / 下移
263
+ */
264
+ const rowPosition = computed(() => {
265
+ if (!contextMenuTargetItem.value) {
266
+ return { isFirst: true, isLast: true }
267
+ }
268
+
269
+ const index = items.value.findIndex(item => item.id === contextMenuTargetItem.value?.id)
270
+ if (index === -1) {
271
+ return { isFirst: true, isLast: true }
272
+ }
273
+
274
+ return {
275
+ isFirst: index === 0,
276
+ isLast: index === items.value.length - 1,
277
+ }
278
+ })
279
+
280
+ const canMoveUp = computed(() => !rowPosition.value.isFirst)
281
+ const canMoveDown = computed(() => !rowPosition.value.isLast)
282
+
283
+ /**
284
+ * 打开属性配置
285
+ */
286
+ function handlePropertyConfig() {
287
+ if (!contextMenuTargetItem.value) return
288
+ eventBus.emit('chart-property-config', contextMenuTargetItem.value)
289
+ }
290
+
291
+ /**
292
+ * 在树上定位当前行
293
+ */
294
+ function handleTreeHighlight() {
295
+ if (!contextMenuTargetItem.value) return
296
+ eventBus.emit('chart-tree-highlight', contextMenuTargetItem.value)
297
+ }
298
+
299
+ /**
300
+ * 通过右键菜单执行上移
301
+ */
302
+ function handleMoveUpFromMenu() {
303
+ if (!canMoveUp.value || !contextMenuTargetItem.value) return
304
+ moveUp()
305
+ }
306
+
307
+ /**
308
+ * 通过右键菜单执行下移
309
+ */
310
+ function handleMoveDownFromMenu() {
311
+ if (!canMoveDown.value || !contextMenuTargetItem.value) return
312
+ moveDown()
313
+ }
314
+
315
+ /**
316
+ * 通过右键菜单执行删除
317
+ */
318
+ function handleDeleteFromMenu() {
319
+ if (!contextMenuTargetItem.value) return
320
+ deleteRows()
321
+ }
322
+
323
+ /**
324
+ * 通过右键菜单执行移除
325
+ */
326
+ function handleRemoveFromMenu() {
327
+ if (!contextMenuTargetItem.value) return
328
+ eventBus.emit('chart-remove', contextMenuTargetItem.value)
329
+ }
330
+
331
+ /**
332
+ * 获取当前选中行的索引列表
333
+ */
334
+ function getSelectedIndices(): number[] {
335
+ return items.value
336
+ .map((item, index) => (selectedRowIds.value.has(item.id) ? index : -1))
337
+ .filter(index => index !== -1)
338
+ .sort((a, b) => a - b)
339
+ }
340
+
341
+ /**
342
+ * 触发行移除
343
+ * 先缓存原始位置,等待接口成功后再做本地对齐
344
+ */
345
+ function removeRows() {
346
+ if (selectedRowIds.value.size === 0) return
347
+
348
+ const selectedIndices = getSelectedIndices()
349
+ if (selectedIndices.length === 0) return
350
+
351
+ const removedRows = items.value.filter(item => selectedRowIds.value.has(item.id))
352
+ const removedIds = removedRows.map(row => row.id)
353
+
354
+ pendingRemoveCtx.value = {
355
+ removedIds,
356
+ selectedIndices,
357
+ }
358
+ eventBus.emit('chart-remove-rows', removedIds)
359
+ }
360
+
361
+ /**
362
+ * 接口成功后执行本地移除与回选
363
+ */
364
+ function applyRemoveAfterSuccess(removedIds: string[], selectedIndices: number[]) {
365
+ const removedIdSet = new Set(removedIds)
366
+ items.value = items.value.filter(item => !removedIdSet.has(item.id))
367
+ onAfterRemoveSuccess?.()
368
+
369
+ const nextSelectedIds: string[] = []
370
+ selectedIndices.forEach(index => {
371
+ const nextItem = items.value[index]
372
+ if (nextItem) {
373
+ nextSelectedIds.push(nextItem.id)
374
+ }
375
+ })
376
+
377
+ selectedRowIds.value = new Set(nextSelectedIds)
378
+ lastSelectedId.value = nextSelectedIds.length > 0 ? nextSelectedIds[nextSelectedIds.length - 1] : null
379
+ }
380
+
381
+ /**
382
+ * 响应外部移除成功回执
383
+ */
384
+ function onRemoveSuccess() {
385
+ const ctx = pendingRemoveCtx.value
386
+ if (!ctx) return
387
+
388
+ applyRemoveAfterSuccess(ctx.removedIds, ctx.selectedIndices)
389
+ pendingRemoveCtx.value = null
390
+ }
391
+
392
+ /**
393
+ * 选中最后一行
394
+ * 用于新增成功后的默认回选
395
+ */
396
+ function selectLastRow() {
397
+ const list = items.value
398
+ if (!list.length) {
399
+ selectedRowIds.value = new Set()
400
+ lastSelectedId.value = null
401
+ return
402
+ }
403
+
404
+ const last = list[list.length - 1]
405
+ selectedRowIds.value = new Set([last.id])
406
+ lastSelectedId.value = last.id
407
+ }
408
+
409
+ /**
410
+ * 同步“单行选中”状态
411
+ * 供属性面板等只关心单选对象的模块使用
412
+ */
413
+ watch(
414
+ [selectedRowIds, items],
415
+ ([newSet]) => {
416
+ if (newSet.size !== 1) {
417
+ eventBus.emit('row-single-selected', null)
418
+ return
419
+ }
420
+
421
+ const selectedId = [...newSet][0]
422
+ const row = items.value.find(item => item.id === selectedId)
423
+ eventBus.emit('row-single-selected', row ?? null)
424
+ },
425
+ { deep: true }
426
+ )
427
+
428
+ /**
429
+ * 同步工具栏依赖的选中位置信息
430
+ */
431
+ watch([selectedRowIds, () => items.value.length], emitRowSelected, { deep: true })
432
+
433
+ return {
434
+ canMoveDown,
435
+ canMoveUp,
436
+ clearSelection,
437
+ contextMenuPosition,
438
+ contextMenuTargetItem,
439
+ deleteRows,
440
+ handleContextMenu,
441
+ handleDeleteFromMenu,
442
+ handleMoveDownFromMenu,
443
+ handleMoveUpFromMenu,
444
+ handlePropertyConfig,
445
+ handleRemoveFromMenu,
446
+ handleTreeHighlight,
447
+ moveDown,
448
+ moveUp,
449
+ onRemoveSuccess,
450
+ removeRows,
451
+ selectLastRow,
452
+ selectRow,
453
+ selectedRowIds,
454
+ showContextMenu,
455
+ }
456
+ }
@@ -64,7 +64,7 @@ export interface UseResizeReturn {
64
64
  groupGhost: Ref<Record<string, Rect>>;
65
65
  startResize: (e: MouseEvent, dir: "nw" | "ne" | "sw" | "se", target: Shape) => void;
66
66
  handleResize: (e: MouseEvent) => void;
67
- stopResize: () => void;
67
+ stopResize: () => Promise<void> | void;
68
68
  cancelResize: () => void;
69
69
  getMinDimensions: (shape: Shape, baseMinW?: number, mode?: MinDimensionMode) => MinDimensions;
70
70
  }
@@ -254,7 +254,7 @@ export function useResize(
254
254
  /**
255
255
  * 停止缩放
256
256
  */
257
- const stopResize = () => {
257
+ const stopResize = async () => {
258
258
  if (!isResizing.value || !resizingTarget.value) return;
259
259
 
260
260
  const id = resizingTarget.value.id;
@@ -0,0 +1,258 @@
1
+ import { computed, ref, watch, nextTick, type Ref } from 'vue'
2
+
3
+ /**
4
+ * 虚拟滚动配置选项
5
+ */
6
+ export interface VirtualScrollOptions {
7
+ /**
8
+ * 每行的高度(px)
9
+ */
10
+ itemHeight: number
11
+
12
+ /**
13
+ * 表头高度(px),默认 0
14
+ */
15
+ headerHeight?: number
16
+
17
+ /**
18
+ * 缓冲区行数(上下各预渲染多少行),默认 8
19
+ */
20
+ overscan?: number
21
+
22
+ /**
23
+ * 容器元素的 ref(用于获取滚动位置和视口高度)
24
+ */
25
+ containerRef: Ref<HTMLElement | null | undefined>
26
+
27
+ /**
28
+ * 可选的额外容器 ref(用于多容器同步场景,如甘特图左右两侧)
29
+ */
30
+ extraContainerRefs?: Ref<HTMLElement | null | undefined>[]
31
+
32
+ /**
33
+ * 可见列数(用于 colspan),默认 1
34
+ */
35
+ columnCount?: Ref<number> | number
36
+ }
37
+
38
+ /**
39
+ * 虚拟滚动返回值
40
+ */
41
+ export interface VirtualScrollReturn<T> {
42
+ /**
43
+ * 当前可见的虚拟行数据
44
+ */
45
+ virtualRows: Ref<Array<{ item: T; index: number }>>
46
+
47
+ /**
48
+ * 上方占位空间高度(px)
49
+ */
50
+ virtualTopSpacerHeight: Ref<number>
51
+
52
+ /**
53
+ * 下方占位空间高度(px)
54
+ */
55
+ virtualBottomSpacerHeight: Ref<number>
56
+
57
+ /**
58
+ * 起始索引
59
+ */
60
+ virtualStartIndex: Ref<number>
61
+
62
+ /**
63
+ * 结束索引
64
+ */
65
+ virtualEndIndex: Ref<number>
66
+
67
+ /**
68
+ * 可见行数
69
+ */
70
+ virtualVisibleRowCount: Ref<number>
71
+
72
+ /**
73
+ * 当前滚动位置
74
+ */
75
+ virtualScrollTop: Ref<number>
76
+
77
+ /**
78
+ * 视口高度
79
+ */
80
+ virtualViewportHeight: Ref<number>
81
+
82
+ /**
83
+ * 同步视口状态(手动调用,用于滚动事件或尺寸变化时)
84
+ */
85
+ syncVirtualViewportState: (preferredEl?: HTMLElement | null) => void
86
+
87
+ /**
88
+ * 可见列数(用于 colspan)
89
+ */
90
+ visibleColumnSpan: Ref<number>
91
+ }
92
+
93
+ /**
94
+ * 虚拟滚动 Hook
95
+ *
96
+ * @description
97
+ * 用于大数据量列表的性能优化,只渲染可见区域的行,大幅减少 DOM 节点数量。
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const containerRef = ref<HTMLElement>()
102
+ * const items = ref([...]) // 1000 条数据
103
+ *
104
+ * const {
105
+ * virtualRows,
106
+ * virtualTopSpacerHeight,
107
+ * virtualBottomSpacerHeight,
108
+ * syncVirtualViewportState
109
+ * } = useVirtualScroll(items, {
110
+ * itemHeight: 32,
111
+ * headerHeight: 56,
112
+ * overscan: 8,
113
+ * containerRef
114
+ * })
115
+ *
116
+ * // 在滚动事件中调用
117
+ * onScroll(() => {
118
+ * syncVirtualViewportState()
119
+ * })
120
+ * ```
121
+ */
122
+ export function useVirtualScroll<T = any>(
123
+ items: Ref<T[]>,
124
+ options: VirtualScrollOptions
125
+ ): VirtualScrollReturn<T> {
126
+ const {
127
+ itemHeight,
128
+ headerHeight = 0,
129
+ overscan = 8,
130
+ containerRef,
131
+ extraContainerRefs = [],
132
+ columnCount = 1,
133
+ } = options
134
+
135
+ // 状态变量
136
+ const virtualScrollTop = ref(0)
137
+ const virtualViewportHeight = ref(0)
138
+
139
+ /**
140
+ * 同步视口状态
141
+ * 从容器元素中读取当前滚动位置和视口高度
142
+ */
143
+ function syncVirtualViewportState(preferredEl?: HTMLElement | null) {
144
+ const allRefs = [containerRef, ...extraContainerRefs]
145
+ const sourceEl = preferredEl ?? allRefs.find(r => r.value)?.value
146
+
147
+ if (!sourceEl) return
148
+
149
+ // 更新滚动位置
150
+ virtualScrollTop.value = sourceEl.scrollTop
151
+
152
+ // 更新视口高度(取所有容器的最大值)
153
+ virtualViewportHeight.value = Math.max(
154
+ ...allRefs.map(r => r.value?.clientHeight ?? 0)
155
+ )
156
+ }
157
+
158
+ /**
159
+ * 计算可见行数
160
+ * 视口内容区高度 ÷ 行高 = 可见行数
161
+ */
162
+ const virtualVisibleRowCount = computed(() => {
163
+ const bodyViewportHeight = Math.max(
164
+ virtualViewportHeight.value - headerHeight,
165
+ itemHeight
166
+ )
167
+
168
+ return Math.max(1, Math.ceil(bodyViewportHeight / itemHeight))
169
+ })
170
+
171
+ /**
172
+ * 计算起始索引
173
+ * 当前滚动位置对应的行索引 - 缓冲区行数
174
+ */
175
+ const virtualStartIndex = computed(() => {
176
+ return Math.max(
177
+ 0,
178
+ Math.floor(virtualScrollTop.value / itemHeight) - overscan
179
+ )
180
+ })
181
+
182
+ /**
183
+ * 计算结束索引
184
+ * 起始索引 + 可见行数 + 上下缓冲区行数
185
+ */
186
+ const virtualEndIndex = computed(() => {
187
+ return Math.min(
188
+ items.value.length,
189
+ virtualStartIndex.value + virtualVisibleRowCount.value + overscan * 2
190
+ )
191
+ })
192
+
193
+ /**
194
+ * 获取当前需要渲染的虚拟行数据
195
+ * 从原始数据中切片,并附加原始索引信息
196
+ */
197
+ const virtualRows = computed(() => {
198
+ return items.value
199
+ .slice(virtualStartIndex.value, virtualEndIndex.value)
200
+ .map((item, offset) => ({
201
+ item,
202
+ index: virtualStartIndex.value + offset,
203
+ }))
204
+ })
205
+
206
+ /**
207
+ * 上方占位空间高度
208
+ * 起始索引 × 行高 = 已滚过的行的总高度
209
+ */
210
+ const virtualTopSpacerHeight = computed(() => {
211
+ return virtualStartIndex.value * itemHeight
212
+ })
213
+
214
+ /**
215
+ * 下方占位空间高度
216
+ * (总行数 - 结束索引) × 行高 = 未滚到的行的总高度
217
+ */
218
+ const virtualBottomSpacerHeight = computed(() => {
219
+ return Math.max(
220
+ 0,
221
+ (items.value.length - virtualEndIndex.value) * itemHeight
222
+ )
223
+ })
224
+
225
+ /**
226
+ * 可见列数(用于 colspan)
227
+ */
228
+ const visibleColumnSpan = computed(() => {
229
+ return typeof columnCount === 'number' ? columnCount : columnCount.value
230
+ })
231
+
232
+ /**
233
+ * 监听数据长度变化
234
+ * 当过滤结果变短时,浏览器会自动修正 scrollTop,需要重新读取
235
+ */
236
+ watch(
237
+ () => items.value.length,
238
+ () => {
239
+ nextTick(() => {
240
+ syncVirtualViewportState()
241
+ })
242
+ },
243
+ { immediate: true }
244
+ )
245
+
246
+ return {
247
+ virtualRows,
248
+ virtualTopSpacerHeight,
249
+ virtualBottomSpacerHeight,
250
+ virtualStartIndex,
251
+ virtualEndIndex,
252
+ virtualVisibleRowCount,
253
+ virtualScrollTop,
254
+ virtualViewportHeight,
255
+ syncVirtualViewportState,
256
+ visibleColumnSpan,
257
+ }
258
+ }