fix: 优化侧边栏数据库吸顶效果

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipg 2026-06-25 22:17:40 +08:00 committed by GitHub
parent 9572a9e52d
commit c63c9e451c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 42 additions and 25 deletions

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch, provide, onMounted, onUnmounted, type Component } from "vue";
import { ref, computed, nextTick, watch, provide, onMounted, onUnmounted, type Component, type CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { Search, X, ListFilter, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw } from "@lucide/vue";
import { useConnectionStore } from "@/stores/connectionStore";
@ -218,16 +218,32 @@ const stickyNode = computed<FlatTreeNode | null>(() => {
const topIndex = Math.min(Math.floor(stickyScrollTop.value / SIDEBAR_TREE_ROW_HEIGHT), len - 1);
// Walk UP from the topmost visible row to the nearest database-level ancestor.
// If the topmost row is itself a database node (it hasn't scrolled past the
// viewport yet), return null so the overlay doesn't duplicate the real row.
// Show the overlay as soon as that database row starts crossing the top edge,
// instead of waiting for it to fully scroll out by one row.
for (let i = topIndex; i >= 0; i--) {
const item = nodes[i];
if (!DATABASE_LEVEL_TYPES.has(item.type)) continue;
return i === topIndex ? null : item;
const rowTop = i * SIDEBAR_TREE_ROW_HEIGHT;
return stickyScrollTop.value > rowTop ? item : null;
}
return null;
});
const stickyHeaderStyle = computed<CSSProperties>(() => {
const node = stickyNode.value;
if (!node) return {};
const nodes = flatNodes.value;
const currentIndex = nodes.findIndex((item) => item.id === node.id);
if (currentIndex < 0) return {};
const nextDatabaseIndex = nodes.findIndex((item, index) => index > currentIndex && DATABASE_LEVEL_TYPES.has(item.type));
if (nextDatabaseIndex < 0) return {};
const distanceToNext = nextDatabaseIndex * SIDEBAR_TREE_ROW_HEIGHT - stickyScrollTop.value;
if (distanceToNext >= SIDEBAR_TREE_ROW_HEIGHT) return {};
return {
transform: `translateY(${Math.min(0, distanceToNext - SIDEBAR_TREE_ROW_HEIGHT)}px)`,
};
});
// Reset tracking when the tree rebuilds (connect/disconnect/collapse) so a
// stale scrollTop doesn't keep the overlay mounted after a structural change.
watch(flatNodes, () => {
@ -657,28 +673,29 @@ defineExpose({ focusSearch, createNewGroup });
/>
</div>
</div>
<div v-if="stickyNode" class="sticky-database-header relative z-[5] border-b border-border/60">
<TreeItem :node="stickyNode.node" :depth="stickyNode.depth" :drag-disabled="true" @search-toggle="onSearchToggle" />
<div v-if="flatNodes.length > 0 && useVirtualTree" class="connection-tree-scroll-shell relative min-h-0 flex-1">
<RecycleScroller
ref="treeScrollerRef"
class="sidebar-tree connection-tree-scroller h-full overflow-y-auto"
:class="sidebarTreeOverflowClass"
@click="clearSidebarSelection"
:items="flatNodes"
:item-size="SIDEBAR_TREE_ROW_HEIGHT"
:buffer="SIDEBAR_TREE_SCROLL_BUFFER"
:prerender="SIDEBAR_TREE_PRERENDER_COUNT"
:skip-hover="true"
key-field="id"
type-field="type"
flow-mode
>
<template #default="{ item }">
<TreeItem :node="item.node" :depth="item.depth" :drag-disabled="isFiltering" :pending-rename="pendingRenameGroupId === item.node.id" :highlighted="highlightedNodeId === item.node.id" @search-toggle="onSearchToggle" @rename-started="pendingRenameGroupId = null" />
</template>
</RecycleScroller>
<div v-if="stickyNode" class="sticky-database-header pointer-events-auto absolute inset-x-0 top-0 z-[5] border-b border-border/60" :style="stickyHeaderStyle">
<TreeItem :node="stickyNode.node" :depth="stickyNode.depth" :drag-disabled="true" @search-toggle="onSearchToggle" />
</div>
</div>
<RecycleScroller
v-if="flatNodes.length > 0 && useVirtualTree"
ref="treeScrollerRef"
class="sidebar-tree connection-tree-scroller min-h-0 flex-1 overflow-y-auto"
:class="sidebarTreeOverflowClass"
@click="clearSidebarSelection"
:items="flatNodes"
:item-size="SIDEBAR_TREE_ROW_HEIGHT"
:buffer="SIDEBAR_TREE_SCROLL_BUFFER"
:prerender="SIDEBAR_TREE_PRERENDER_COUNT"
:skip-hover="true"
key-field="id"
type-field="type"
flow-mode
>
<template #default="{ item }">
<TreeItem :node="item.node" :depth="item.depth" :drag-disabled="isFiltering" :pending-rename="pendingRenameGroupId === item.node.id" :highlighted="highlightedNodeId === item.node.id" @search-toggle="onSearchToggle" @rename-started="pendingRenameGroupId = null" />
</template>
</RecycleScroller>
<div v-else-if="flatNodes.length > 0" ref="plainTreeScrollerRef" class="sidebar-tree min-h-0 flex-1 overflow-y-auto" :class="sidebarTreeOverflowClass" @click="clearSidebarSelection">
<TreeItem
v-for="item in flatNodes"