mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-30 08:20:33 +00:00
fix(sidebar): 修复侧边栏树状态显示
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user