fix(sidebar): 修复侧边栏树状态显示

This commit is contained in:
t8y2
2026-05-11 02:16:36 +08:00
parent 043bd5dd8d
commit 9355fa7e93
7 changed files with 180 additions and 95 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ async function refreshTree() {
<TooltipContent>{{ t("sidebar.export") }}</TooltipContent>
</Tooltip>
</div>
<div class="flex-1 overflow-y-auto">
<div class="flex-1 min-h-0">
<ConnectionTree />
</div>
</div>
+36 -33
View File
@@ -5,8 +5,11 @@ import { Search, X, ListFilter, Check, FolderPlus } from "lucide-vue-next";
import { useConnectionStore } from "@/stores/connectionStore";
import type { TreeNode } from "@/types/database";
import { matchSidebarLabel } from "@/lib/sidebarSearch";
import { flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree";
import TreeItem from "./TreeItem.vue";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -145,22 +148,8 @@ const filteredNodes = computed(() => {
return nodes;
});
const TOP_LEVEL_PAGE_SIZE = 50;
const topLevelDisplayLimit = ref(TOP_LEVEL_PAGE_SIZE);
const visibleFilteredNodes = computed(() => filteredNodes.value.slice(0, topLevelDisplayLimit.value));
const hasMoreTopLevel = computed(() => filteredNodes.value.length > topLevelDisplayLimit.value);
const remainingTopLevelCount = computed(() => filteredNodes.value.length - topLevelDisplayLimit.value);
function showMoreTopLevel() {
topLevelDisplayLimit.value += TOP_LEVEL_PAGE_SIZE;
}
watch(filteredNodes, () => {
topLevelDisplayLimit.value = TOP_LEVEL_PAGE_SIZE;
});
const flatNodes = computed<FlatTreeNode[]>(() => flattenTree(filteredNodes.value));
const useVirtualTree = computed(() => shouldVirtualizeFlatTree(flatNodes.value.length));
const pendingRenameGroupId = ref<string | null>(null);
@@ -179,7 +168,7 @@ function onSearchToggle(node: TreeNode) {
</script>
<template>
<div class="text-sm select-none">
<div class="h-full min-h-0 flex flex-col text-sm select-none">
<div v-if="store.treeNodes.length > 0" class="sticky top-0 z-10 bg-background px-2 py-1">
<div class="relative flex items-center gap-1">
<div class="relative flex-1">
@@ -243,23 +232,37 @@ function onSearchToggle(node: TreeNode) {
</DropdownMenu>
</div>
</div>
<TreeItem
v-for="node in visibleFilteredNodes"
:key="node.id"
:node="node"
:depth="0"
:drag-disabled="isFiltering"
:pending-rename="pendingRenameGroupId === node.id"
@search-toggle="onSearchToggle"
@rename-started="pendingRenameGroupId = null"
/>
<div
v-if="hasMoreTopLevel"
class="flex items-center gap-1.5 py-1 px-2 cursor-pointer hover:bg-accent text-xs text-muted-foreground"
style="padding-left: 8px"
@click="showMoreTopLevel"
<RecycleScroller
v-if="flatNodes.length > 0 && useVirtualTree"
class="min-h-0 flex-1 overflow-y-auto"
:items="flatNodes"
:item-size="28"
key-field="id"
type-field="type"
flow-mode
>
<span>{{ t("sidebar.showMore", { count: Math.min(TOP_LEVEL_PAGE_SIZE, remainingTopLevelCount) }) }}</span>
<template #default="{ item }">
<TreeItem
:node="item.node"
:depth="item.depth"
:drag-disabled="isFiltering"
:pending-rename="pendingRenameGroupId === item.node.id"
@search-toggle="onSearchToggle"
@rename-started="pendingRenameGroupId = null"
/>
</template>
</RecycleScroller>
<div v-else-if="flatNodes.length > 0" class="min-h-0 flex-1 overflow-y-auto">
<TreeItem
v-for="item in flatNodes"
:key="item.id"
:node="item.node"
:depth="item.depth"
:drag-disabled="isFiltering"
:pending-rename="pendingRenameGroupId === item.node.id"
@search-toggle="onSearchToggle"
@rename-started="pendingRenameGroupId = null"
/>
</div>
<div v-if="store.treeNodes.length === 0" class="px-3 py-8 text-center text-muted-foreground text-xs">
{{ t("sidebar.noConnections") }}
+13 -61
View File
@@ -58,6 +58,7 @@ import type { DatabaseType, QueryResult, TreeNode, TreeNodeType } from "@/types/
import * as api from "@/lib/api";
import { uuid } from "@/lib/utils";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { canTreeNodeShowExpander, treeItemPaddingLeft } from "@/lib/sidebarTreeItemLayout";
import {
DATABASE_EXPORT_PAGE_SIZE,
DATABASE_EXPORT_ROW_LIMIT,
@@ -173,7 +174,7 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
case "redis-db":
return { icon: Database, colorClass: "text-red-400" };
case "mongo-db":
return { icon: Database, colorClass: "text-green-500" };
return { icon: Database, colorClass: "text-yellow-500" };
case "mongo-collection":
return { icon: Table, colorClass: "text-green-400" };
default:
@@ -181,16 +182,6 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
}
}
const leafTypes: Set<TreeNodeType> = new Set([
"column",
"index",
"fkey",
"trigger",
"object-browser",
"redis-db",
"mongo-collection",
"saved-sql-file",
]);
const groupTypes: Set<TreeNodeType> = new Set([
"group-columns",
"group-indexes",
@@ -291,8 +282,8 @@ async function toggle() {
function onClick() {
const node = props.node;
if (canOpenObjectBrowser.value) {
const shouldOpenObjectBrowser = !canExpand || !node.isExpanded;
if (canExpand) void toggle();
const shouldOpenObjectBrowser = !canExpand.value || !node.isExpanded;
if (canExpand.value) void toggle();
if (shouldOpenObjectBrowser) void openObjectBrowser();
return;
}
@@ -300,7 +291,7 @@ function onClick() {
void openObjectBrowser();
return;
}
const action = treeNodeRowAction(node.type, canExpand);
const action = treeNodeRowAction(node.type, canExpand.value);
if (action === "open-data") {
openData();
} else if (node.type === "saved-sql-file") {
@@ -1087,7 +1078,12 @@ function openFieldLineage() {
};
}
const canExpand = !leafTypes.has(props.node.type);
const canExpand = computed(() =>
canTreeNodeShowExpander({
type: props.node.type,
childCount: props.node.children?.length ?? 0,
}),
);
const nodeConfig = computed(() =>
props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined,
);
@@ -1163,7 +1159,7 @@ const columnComment = computed(() =>
? (props.node.meta as any).comment
: null,
);
const paddingLeft = `${props.depth * 16 + 8}px`;
const paddingLeft = computed(() => treeItemPaddingLeft(props.depth));
const isConnected = computed(
() =>
props.node.type === "connection" &&
@@ -1186,40 +1182,15 @@ const isActiveConnectionScope = computed(
const rowStyle = computed(() => {
const color = connectionColor.value;
return {
paddingLeft,
paddingLeft: paddingLeft.value,
backgroundColor: hexToRgba(color, isActiveConnectionScope.value ? 0.14 : 0.08),
};
});
const CHILDREN_PAGE_SIZE = 50;
const displayLimit = ref(CHILDREN_PAGE_SIZE);
const visibleChildren = computed(() => {
if (!props.node.children) return [];
return props.node.children.slice(0, displayLimit.value);
});
const hasMoreChildren = computed(() => (props.node.children?.length ?? 0) > displayLimit.value);
const remainingCount = computed(() => (props.node.children?.length ?? 0) - displayLimit.value);
function togglePin() {
connectionStore.toggleTreeNodePin(props.node.id);
}
watch(
() => props.node.isExpanded,
(expanded) => {
if (!expanded) displayLimit.value = CHILDREN_PAGE_SIZE;
},
);
async function showMore() {
if ((props.node.children?.length ?? 0) > displayLimit.value) {
displayLimit.value += CHILDREN_PAGE_SIZE;
}
}
// --- Connection Group Management ---
const isRenamingGroup = ref(false);
const renameInput = ref("");
@@ -1484,25 +1455,6 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
<Pin class="w-3 h-3" :class="{ 'fill-current': isPinned }" />
</button>
</div>
<template v-if="node.isExpanded && node.children">
<TreeItem
v-for="child in visibleChildren"
:key="child.id"
:node="child"
:depth="depth + 1"
:drag-disabled="dragDisabled"
@search-toggle="emit('search-toggle', $event)"
/>
<div
v-if="hasMoreChildren"
class="flex items-center gap-1.5 py-1 px-2 cursor-pointer hover:bg-accent text-xs text-muted-foreground"
:style="{ paddingLeft: `${(depth + 1) * 16 + 8}px` }"
@click="showMore"
>
<Loader2 v-if="node.isLoading" class="w-3 h-3 shrink-0 animate-spin" />
<span>{{ t("sidebar.showMore", { count: Math.min(CHILDREN_PAGE_SIZE, remainingCount) }) }}</span>
</div>
</template>
</div>
</ContextMenuTrigger>
+27
View File
@@ -0,0 +1,27 @@
import type { TreeNode, TreeNodeType } from "@/types/database";
export interface FlatTreeNode {
node: TreeNode;
depth: number;
id: string;
type: TreeNodeType;
}
function walk(children: TreeNode[], depth: number, result: FlatTreeNode[]) {
for (const node of children) {
result.push({ node, depth, id: node.id, type: node.type });
if (node.isExpanded && node.children) {
walk(node.children, depth + 1, result);
}
}
}
export function flattenTree(nodes: TreeNode[]): FlatTreeNode[] {
const result: FlatTreeNode[] = [];
walk(nodes, 0, result);
return result;
}
export function shouldVirtualizeFlatTree(count: number): boolean {
return count >= 400;
}
+28
View File
@@ -0,0 +1,28 @@
import type { TreeNodeType } from "@/types/database";
const leafTypes: Set<TreeNodeType> = new Set([
"column",
"index",
"fkey",
"trigger",
"object-browser",
"redis-db",
"mongo-collection",
"saved-sql-file",
]);
export function treeItemPaddingLeft(depth: number): string {
return `${depth * 16 + 8}px`;
}
export function canTreeNodeExpand(type: TreeNodeType): boolean {
return !leafTypes.has(type);
}
export function canTreeNodeShowExpander({ type, childCount }: { type: TreeNodeType; childCount?: number }): boolean {
if (!canTreeNodeExpand(type)) return false;
if (type === "saved-sql-root" || type === "saved-sql-folder") {
return (childCount ?? 0) > 0;
}
return true;
}