From 26ed2b4aff789737a7003051f704bb65df7d1ace Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 30 May 2026 10:31:32 +0800 Subject: [PATCH] perf(desktop): use lightweight sidebar tree tooltips --- .../src/components/sidebar/TreeItem.vue | 37 ++--- .../src/components/ui/LightTooltip.vue | 139 ++++++++++++++++++ 2 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src/components/ui/LightTooltip.vue diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 9cefbdced..b51fddb3f 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -122,15 +122,15 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from " import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import LightTooltip from "@/components/ui/LightTooltip.vue"; const { t } = useI18n(); const labelRef = ref(); const rowRef = ref(); -const isTruncated = computed(() => { +function isLabelTruncated(): boolean { const el = labelRef.value; return !!el && el.scrollWidth > el.clientWidth; -}); +} const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); const savedSqlStore = useSavedSqlStore(); @@ -263,7 +263,7 @@ function visibleLabel(node: TreeNode): string { } function isTooltipDisabled(node: TreeNode): boolean { - return !isTruncated.value && visibleLabel(node) === displayLabel(node); + return !isLabelTruncated() && visibleLabel(node) === displayLabel(node); } async function toggle() { @@ -1955,14 +1955,12 @@ function treeItemMenuItems(): ContextMenuItem[] { } items.push({ label: "", separator: true }); if (availableGroups.value.length > 0 || currentGroupId.value) { - const groupChildren: ContextMenuItem[] = [ - ...availableGroups.value.map((group: { id: string; name: string }) => ({ - label: group.name, - action: () => moveToGroup(group.id), - icon: FolderOpen, - disabled: group.id === currentGroupId.value, - })), - ]; + const groupChildren: ContextMenuItem[] = availableGroups.value.map((group: { id: string; name: string }) => ({ + label: group.name, + action: () => moveToGroup(group.id), + icon: FolderOpen, + disabled: group.id === currentGroupId.value, + })); if (currentGroupId.value) { groupChildren.push({ label: "", separator: true }); groupChildren.push({ label: t("connectionGroup.ungrouped"), action: () => moveToGroup(null) }); @@ -2273,12 +2271,15 @@ function treeItemMenuItems(): ContextMenuItem[] { @keydown.escape.prevent="isRenamingGroup = false" @click.stop /> - - - {{ visibleLabel(node) }} - - {{ displayLabel(node) }} - + + {{ visibleLabel(node) }} + +import { computed, onBeforeUnmount, ref } from "vue"; + +const props = withDefaults( + defineProps<{ + text: string; + disabled?: boolean; + side?: "top" | "right" | "bottom" | "left"; + sideOffset?: number; + delay?: number; + }>(), + { + disabled: false, + side: "top", + sideOffset: 8, + delay: 300, + }, +); + +const triggerRef = ref(); +const show = ref(false); +const x = ref(0); +const y = ref(0); +let timer: ReturnType | null = null; + +function triggerElement(): HTMLElement | undefined { + const root = triggerRef.value; + const child = root?.firstElementChild; + return child instanceof HTMLElement ? child : root; +} + +const tooltipTransformClass = computed(() => { + switch (props.side) { + case "right": + return "-translate-y-1/2"; + case "left": + return "-translate-x-full -translate-y-1/2"; + case "bottom": + return "-translate-x-1/2"; + case "top": + default: + return "-translate-x-1/2 -translate-y-full"; + } +}); + +const arrowClass = computed(() => { + switch (props.side) { + case "right": + return "absolute -left-1 top-1/2 -translate-y-1/2"; + case "left": + return "absolute -right-1 top-1/2 -translate-y-1/2"; + case "bottom": + return "absolute -top-1 left-1/2 -translate-x-1/2"; + case "top": + default: + return "absolute -bottom-1 left-1/2 -translate-x-1/2"; + } +}); + +function clearTimer() { + if (!timer) return; + clearTimeout(timer); + timer = null; +} + +function updatePosition() { + const el = triggerElement(); + if (!el) return; + const rect = el.getBoundingClientRect(); + const offset = props.sideOffset; + switch (props.side) { + case "right": + x.value = Math.min(window.innerWidth - 8, rect.right + offset); + y.value = Math.min(Math.max(8, rect.top + rect.height / 2), window.innerHeight - 8); + break; + case "left": + x.value = Math.max(8, rect.left - offset); + y.value = Math.min(Math.max(8, rect.top + rect.height / 2), window.innerHeight - 8); + break; + case "bottom": + x.value = Math.min(Math.max(8, rect.left + rect.width / 2), window.innerWidth - 8); + y.value = Math.min(window.innerHeight - 8, rect.bottom + offset); + break; + case "top": + default: + x.value = Math.min(Math.max(8, rect.left + rect.width / 2), window.innerWidth - 8); + y.value = Math.max(8, rect.top - offset); + break; + } +} + +function close() { + clearTimer(); + show.value = false; + window.removeEventListener("scroll", close, true); + window.removeEventListener("resize", close); +} + +function open() { + if (props.disabled || !props.text) return; + updatePosition(); + show.value = true; + window.addEventListener("scroll", close, true); + window.addEventListener("resize", close); +} + +function scheduleOpen() { + if (props.disabled || !props.text) return; + clearTimer(); + timer = setTimeout(open, props.delay); +} + +onBeforeUnmount(close); + + +