fix: multiple sub nodes

This commit is contained in:
Yourz
2026-01-06 23:18:14 +08:00
parent 77ab1a58a8
commit 1cd97cb7e7

View File

@@ -42,12 +42,11 @@
<ContextMenu ref="menu" :model="menuItems" /> <ContextMenu ref="menu" :model="menuItems" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useElementSize, useScroll, whenever } from '@vueuse/core' import { useElementSize, useScroll } from '@vueuse/core'
import { clamp } from 'es-toolkit/compat'
import ContextMenu from 'primevue/contextmenu' import ContextMenu from 'primevue/contextmenu'
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem' import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
import Tree from 'primevue/tree' import Tree from 'primevue/tree'
import { computed, provide, ref, watch } from 'vue' import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue' import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
@@ -98,16 +97,13 @@ const {
} }
) )
const BUFFER_ROWS = 10
const DEFAULT_NODE_HEIGHT = 32 const DEFAULT_NODE_HEIGHT = 32
const SCROLL_THROTTLE = 64 const SCROLL_THROTTLE = 64
const parentWindowRanges = ref<Record<string, WindowRange>>({})
const treeContainerRef = ref<HTMLDivElement | null>(null) const treeContainerRef = ref<HTMLDivElement | null>(null)
const menu = ref<InstanceType<typeof ContextMenu> | null>(null) const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null) const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null)
const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null) const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null)
const bufferRowsRef = ref(BUFFER_ROWS)
const { height: containerHeight } = useElementSize(treeContainerRef) const { height: containerHeight } = useElementSize(treeContainerRef)
const { y: scrollY } = useScroll(treeContainerRef, { const { y: scrollY } = useScroll(treeContainerRef, {
@@ -115,108 +111,110 @@ const { y: scrollY } = useScroll(treeContainerRef, {
eventListenerOptions: { passive: true } eventListenerOptions: { passive: true }
}) })
// Reset window ranges when nodes are collapsed // Computed values for window calculation
watch( const viewRows = computed(() =>
expandedKeys, containerHeight.value
(newKeys, oldKeys) => { ? Math.ceil(containerHeight.value / DEFAULT_NODE_HEIGHT)
if (!oldKeys) return : 0
for (const key in oldKeys) { )
if (oldKeys[key] && !newKeys[key]) { const bufferRows = computed(() => Math.max(1, Math.floor(viewRows.value / 3)))
delete parentWindowRanges.value[key] const windowSize = computed(() => viewRows.value + bufferRows.value * 2)
// Compute window ranges for all nodes based on scroll position
// Each node's window is calculated relative to its children list
const parentWindowRanges = computed<Record<string, WindowRange>>(() => {
if (!containerHeight.value || !renderedRoot.value.children) {
return {}
}
const ranges: Record<string, WindowRange> = {}
const scrollTop = scrollY.value
const scrollBottom = scrollTop + containerHeight.value
// Calculate cumulative positions for nodes in the tree
const nodePositions = new Map<string, number>()
let currentPos = 0
const calculatePositions = (node: RenderedTreeExplorerNode): number => {
const nodeStart = currentPos
nodePositions.set(node.key, nodeStart)
currentPos += DEFAULT_NODE_HEIGHT
if (node.children && !node.leaf && expandedKeys.value?.[node.key]) {
for (const child of node.children) {
currentPos = calculatePositions(child)
} }
} }
},
{ deep: true }
)
// Update windows for all nodes based on current scroll position return currentPos
const updateWindows = () => { }
if (!treeContainerRef.value || !containerHeight.value) return
const viewRows = Math.ceil(containerHeight.value / DEFAULT_NODE_HEIGHT) for (const child of renderedRoot.value.children) {
const offsetRows = Math.floor(scrollY.value / DEFAULT_NODE_HEIGHT) currentPos = calculatePositions(child)
bufferRowsRef.value = viewRows / 3 }
const updateNodeWindow = (node: RenderedTreeExplorerNode) => { // Compute windows for each node based on scroll position
const computeNodeWindow = (node: RenderedTreeExplorerNode) => {
if (!node.children || node.leaf) return if (!node.children || node.leaf) return
const isExpanded = expandedKeys.value?.[node.key] ?? false const isExpanded = expandedKeys.value?.[node.key] ?? false
if (!isExpanded) { if (!isExpanded) return
delete parentWindowRanges.value[node.key]
return const nodeStart = nodePositions.get(node.key) ?? 0
} const childrenStart = nodeStart + DEFAULT_NODE_HEIGHT
const childrenEnd =
childrenStart + node.children.length * DEFAULT_NODE_HEIGHT
// Check if this node's children are in the visible range
const isVisible =
childrenEnd >= scrollTop - bufferRows.value * DEFAULT_NODE_HEIGHT &&
childrenStart <= scrollBottom + bufferRows.value * DEFAULT_NODE_HEIGHT
const totalChildren = node.children.length const totalChildren = node.children.length
const currentRange = parentWindowRanges.value[node.key]
if (currentRange) { if (isVisible && totalChildren > 0) {
const fromRow = Math.max(0, offsetRows - bufferRowsRef.value) // Calculate which children should be visible based on scroll position
const toRow = offsetRows + bufferRowsRef.value + viewRows const relativeScrollTop = Math.max(0, scrollTop - childrenStart)
const newStart = clamp(fromRow, 0, totalChildren) const relativeScrollBottom = Math.max(0, scrollBottom - childrenStart)
const newEnd = clamp(toRow, newStart, totalChildren)
if ( const fromRow = Math.max(
Math.abs(currentRange.start - newStart) > bufferRowsRef.value || 0,
Math.abs(currentRange.end - newEnd) > bufferRowsRef.value Math.floor(relativeScrollTop / DEFAULT_NODE_HEIGHT) - bufferRows.value
) { )
parentWindowRanges.value[node.key] = { const toRow = Math.min(
start: newStart, totalChildren,
end: newEnd Math.ceil(relativeScrollBottom / DEFAULT_NODE_HEIGHT) + bufferRows.value
} )
ranges[node.key] = {
start: Math.max(0, fromRow),
end: Math.min(
totalChildren,
Math.max(fromRow + windowSize.value, toRow)
)
} }
} else { } else {
const windowSize = viewRows + bufferRowsRef.value * 2 // Node is outside visible range, use minimal window
parentWindowRanges.value[node.key] = createInitialWindowRange( ranges[node.key] = createInitialWindowRange(
totalChildren, totalChildren,
windowSize windowSize.value
) )
} }
const range = parentWindowRanges.value[node.key] // Recursively compute windows for children
const range = ranges[node.key]
for (let i = range.start; i < range.end && i < node.children.length; i++) { for (let i = range.start; i < range.end && i < node.children.length; i++) {
updateNodeWindow(node.children[i]) computeNodeWindow(node.children[i])
} }
} }
for (const child of renderedRoot.value.children || []) { for (const child of renderedRoot.value.children) {
updateNodeWindow(child) computeNodeWindow(child)
} }
}
// Watch scroll position and update windows reactively return ranges
watch([scrollY, containerHeight, expandedKeys], updateWindows, {
immediate: true,
flush: 'post'
}) })
// Reset windows to top when scroll reaches top
whenever(
() => scrollY.value === 0,
() => {
const resetNodeWindow = (node: RenderedTreeExplorerNode) => {
if (!node.children || node.leaf) return
const isExpanded = expandedKeys.value?.[node.key] ?? false
if (!isExpanded) return
const totalChildren = node.children.length
parentWindowRanges.value[node.key] = createInitialWindowRange(
totalChildren,
Math.ceil((containerHeight.value / DEFAULT_NODE_HEIGHT) * 2)
)
for (const child of node.children) {
if (expandedKeys.value?.[child.key]) {
resetNodeWindow(child)
}
}
}
for (const parent of renderedRoot.value.children || []) {
resetNodeWindow(parent)
}
}
)
const getTreeNodeIcon = (node: TreeExplorerNode): string => { const getTreeNodeIcon = (node: TreeExplorerNode): string => {
if (node.getIcon) { if (node.getIcon) {
const icon = node.getIcon() const icon = node.getIcon()
@@ -266,11 +264,6 @@ const nodeKeyMap = computed<Record<string, RenderedTreeExplorerNode>>(() => {
return map return map
}) })
const windowSize = computed(() => {
if (!containerHeight.value) return 60
return Math.ceil((containerHeight.value / DEFAULT_NODE_HEIGHT) * 2)
})
const displayRoot = computed<RenderedTreeExplorerNode>(() => ({ const displayRoot = computed<RenderedTreeExplorerNode>(() => ({
...renderedRoot.value, ...renderedRoot.value,
children: (renderedRoot.value.children || []).map((node) => children: (renderedRoot.value.children || []).map((node) =>