perf(desktop): use lightweight sidebar tree tooltips

This commit is contained in:
t8y2 2026-05-30 10:31:32 +08:00
parent 58b8552316
commit 26ed2b4aff
2 changed files with 158 additions and 18 deletions

View File

@ -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<HTMLElement>();
const rowRef = ref<HTMLElement>();
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
/>
<Tooltip v-else :disabled="isTooltipDisabled(node)">
<TooltipTrigger as-child>
<span ref="labelRef" class="min-w-0 flex-1 truncate">{{ visibleLabel(node) }}</span>
</TooltipTrigger>
<TooltipContent side="right" :side-offset="8">{{ displayLabel(node) }}</TooltipContent>
</Tooltip>
<LightTooltip
v-else
:text="displayLabel(node)"
:disabled="isTooltipDisabled(node)"
side="right"
:side-offset="8"
>
<span ref="labelRef" class="min-w-0 flex-1 truncate">{{ visibleLabel(node) }}</span>
</LightTooltip>
<span
v-if="
(node.type === 'group-tables' ||

View File

@ -0,0 +1,139 @@
<script setup lang="ts">
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<HTMLElement>();
const show = ref(false);
const x = ref(0);
const y = ref(0);
let timer: ReturnType<typeof setTimeout> | 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);
</script>
<template>
<span
ref="triggerRef"
class="contents"
@mouseenter="scheduleOpen"
@mouseleave="close"
@focusin="scheduleOpen"
@focusout="close"
>
<slot />
</span>
<Teleport to="body">
<div
v-if="show"
class="pointer-events-none fixed z-50 inline-flex w-fit max-w-xs items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background"
:class="tooltipTransformClass"
:style="{ left: `${x}px`, top: `${y}px` }"
role="tooltip"
>
{{ text }}
<span :class="[arrowClass, 'size-2.5 rotate-45 rounded-[2px] bg-foreground']" aria-hidden="true" />
</div>
</Teleport>
</template>