perf(sidebar): reduce tree selection overhead

This commit is contained in:
t8y2 committed 2026-06-05 14:30:25 +08:00
1 parent b4a207fc6b
commit a2c9503a3d
5 files changed
+56 -10

No files matched your search

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch, type Component } from "vue";
import { ref, computed, nextTick, watch, provide, type Component } 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";
@@ -26,6 +26,7 @@ import {
shouldVirtualizeFlatTree,
type FlatTreeNode,
} from "@/composables/useFlatTree";
import { sidebarTreeContextKey } from "@/lib/sidebarTreeContext";
import TreeItem from "./TreeItem.vue";
import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
@@ -148,6 +149,11 @@ const filteredNodes = computed(() => {
const flatNodes = computed<FlatTreeNode[]>(() => flattenTree(filteredNodes.value));
const visibleNodes = computed<TreeNode[]>(() => flatNodes.value.map((item) => item.node));
const visibleNodeIndexById = computed(() => {
const next = new Map<string, number>();
visibleNodes.value.forEach((node, index) => next.set(node.id, index));
return next;
});
const useVirtualTree = computed(() => shouldVirtualizeFlatTree(flatNodes.value.length));
const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId));
const sidebarTreeOverflowClass = computed(() =>
@@ -156,6 +162,11 @@ const sidebarTreeOverflowClass = computed(() =>
: "overflow-x-hidden",
);
provide(sidebarTreeContextKey, {
getVisibleNodes: () => visibleNodes.value,
getVisibleNodeIndex: (id: string) => visibleNodeIndexById.value.get(id) ?? -1,
});
const pendingRenameGroupId = ref<string | null>(null);
const highlightedNodeId = ref<string | null>(null);
let highlightTimer: number | undefined;
@@ -495,7 +506,6 @@ defineExpose({ focusSearch, createNewGroup });
:drag-disabled="isFiltering"
:pending-rename="pendingRenameGroupId === item.node.id"
:highlighted="highlightedNodeId === item.node.id"
:visible-nodes="visibleNodes"
@node-toggled="onNodeToggled"
@search-toggle="onSearchToggle"
@rename-started="pendingRenameGroupId = null"
@@ -516,7 +526,6 @@ defineExpose({ focusSearch, createNewGroup });
:drag-disabled="isFiltering"
:pending-rename="pendingRenameGroupId === item.node.id"
:highlighted="highlightedNodeId === item.id"
:visible-nodes="visibleNodes"
@node-toggled="onNodeToggled"
@search-toggle="onSearchToggle"
@rename-started="pendingRenameGroupId = null"
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch, onBeforeUnmount } from "vue";
import { ref, computed, nextTick, watch, onBeforeUnmount, inject } from "vue";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
@@ -127,8 +127,10 @@ import { hasTreeNodeDatabaseContext } from "@/lib/treeNodeContext";
import { sidebarDisplayTableName } from "@/lib/sidebarTableNameDisplay";
import {
selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes,
treeSelectionRangeIdsByIndex,
treeSelectionRangeIds,
} from "@/lib/sidebarTreeSelection";
import { sidebarTreeContextKey } from "@/lib/sidebarTreeContext";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue";
import { useExportTracker, type ExportTask } from "@/composables/useExportTracker";
@@ -174,7 +176,6 @@ const props = defineProps<{
dragDisabled?: boolean;
pendingRename?: boolean;
highlighted?: boolean;
visibleNodes?: TreeNode[];
}>();
const emit = defineEmits<{
@@ -186,6 +187,7 @@ const emit = defineEmits<{
const usesFullWidthLabel = computed(() =>
usesFullWidthTreeLabel(props.node.type, settingsStore.editorSettings.sidebarAllowHorizontalScroll),
);
const sidebarTreeContext = inject(sidebarTreeContextKey, null);
const rowWidthClass = computed(() => (usesFullWidthLabel.value ? "w-max min-w-full" : "w-full min-w-0"));
const labelWidthClass = computed(() =>
usesFullWidthLabel.value ? "shrink-0 whitespace-nowrap" : "min-w-0 flex-1 truncate",
@@ -459,7 +461,7 @@ function runRowClickAction() {
}
function visibleTreeNodes(): TreeNode[] {
if (props.visibleNodes) return props.visibleNodes;
if (sidebarTreeContext) return sidebarTreeContext.getVisibleNodes();
return flattenTree(connectionStore.treeNodes).map((item) => item.node);
}
@@ -485,10 +487,20 @@ function toggleTreeNodeSelection(node: TreeNode) {
function selectTreeNodeRange(node: TreeNode) {
const visible = visibleTreeNodes();
const anchorId = connectionStore.treeSelectionAnchorId || connectionStore.selectedTreeNodeId || node.id;
const currentIndex = sidebarTreeContext ? sidebarTreeContext.getVisibleNodeIndex(node.id) : -1;
const anchorIndex = sidebarTreeContext ? sidebarTreeContext.getVisibleNodeIndex(anchorId) : -1;
if (sidebarTreeContext && currentIndex >= 0 && anchorIndex >= 0) {
connectionStore.selectedTreeNodeIds = treeSelectionRangeIdsByIndex(visible, currentIndex, anchorIndex, node.id);
connectionStore.selectedTreeNodeId = node.id;
return;
}
if (!visible.some((item) => item.id === anchorId) || !visible.some((item) => item.id === node.id)) {
selectSingleTreeNode(node);
return;
}
const rangeIds = treeSelectionRangeIds(visible, node.id, anchorId, connectionStore.selectedTreeNodeId);
connectionStore.selectedTreeNodeIds = rangeIds;
connectionStore.selectedTreeNodeId = node.id;