feat(tabs): display pinned tabs in a separate row

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipg 2026-06-30 18:02:52 +08:00 committed by GitHub
parent aae2cae068
commit 8162a420e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 311 additions and 133 deletions

View File

@ -2,7 +2,7 @@
import { computed, ref, watch, nextTick } from "vue";
import type { CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Check, Lock, Copy, AlertTriangle, Network } from "@lucide/vue";
import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Lock, Copy, AlertTriangle, Network, Minimize2, Maximize2 } from "@lucide/vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import LightDropdown from "@/components/ui/LightDropdown.vue";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
@ -40,6 +40,10 @@ const tabDrag = useTabDrag((draggedId, targetId, position) => {
});
const editingTabId = ref<string | null>(null);
const editingTitle = ref("");
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
const fixedTabs = computed(() => queryStore.tabs.filter((tab) => tab.pinned));
const regularTabs = computed(() => queryStore.tabs.filter((tab) => !tab.pinned));
const hasFixedTabs = computed(() => fixedTabs.value.length > 0);
const compactTabTitle = computed({
get: () => settingsStore.editorSettings.compactTabTitle,
set: (checked: boolean | "indeterminate") => {
@ -80,10 +84,9 @@ function cancelRenameTab() {
function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
return [
{
label: t("contextMenu.compactTabTitle"),
label: compactTabTitle.value ? t("contextMenu.fullTabTitle") : t("contextMenu.compactTabTitle"),
action: toggleCompactTabTitle,
icon: Check,
iconClass: compactTabTitle.value ? "opacity-100" : "opacity-0",
icon: compactTabTitle.value ? Maximize2 : Minimize2,
},
{
label: t("contextMenu.renameTab"),
@ -111,7 +114,7 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
},
{ label: "", separator: true },
{
label: tab.pinned ? t("contextMenu.unpin") : t("contextMenu.pin"),
label: tab.pinned ? t("contextMenu.unfixTab") : t("contextMenu.fixTab"),
action: () => queryStore.togglePinnedTab(tab.id),
icon: Pin,
iconClass: tab.pinned ? "fill-current" : "",
@ -148,12 +151,36 @@ function handleCancelClose() {
const tabsContainerRef = ref<HTMLElement | null>(null);
const { hasTabOverflow, scrollThumbLeftPercent, scrollThumbWidthPercent, isScrollbarDragging, updateScrollButtons, onTabsWheel, startScrollbarDrag } = useTabScroll(tabsContainerRef);
const fixedTabsContainerRef = ref<HTMLElement | null>(null);
const {
hasTabOverflow: hasFixedTabOverflow,
scrollThumbLeftPercent: fixedScrollThumbLeftPercent,
scrollThumbWidthPercent: fixedScrollThumbWidthPercent,
isScrollbarDragging: isFixedScrollbarDragging,
updateScrollButtons: updateFixedScrollButtons,
onTabsWheel: onFixedTabsWheel,
startScrollbarDrag: startFixedScrollbarDrag,
} = useTabScroll(fixedTabsContainerRef);
const tabScrollBehavior = ref<ScrollBehavior>("smooth");
function updateAllScrollButtons() {
updateScrollButtons();
updateFixedScrollButtons();
}
function activeTabScrollInline(container: HTMLElement, tabId: string | null): ScrollLogicalPosition {
if (!tabId) return "center";
const lastRegularTab = regularTabs.value[regularTabs.value.length - 1];
const lastFixedTab = fixedTabs.value[fixedTabs.value.length - 1];
if (container === tabsContainerRef.value && lastRegularTab?.id === tabId) return "end";
if (container === fixedTabsContainerRef.value && lastFixedTab?.id === tabId) return "end";
return "center";
}
watch(
() => queryStore.tabs.length,
() => queryStore.tabs.map((tab) => `${tab.id}:${tab.pinned ? "1" : "0"}`).join("|"),
() => {
nextTick(updateScrollButtons);
nextTick(updateAllScrollButtons);
},
);
@ -161,13 +188,15 @@ watch(
() => queryStore.activeTabId,
() => {
nextTick(() => {
const container = tabsContainerRef.value;
if (!container) return;
const activeEl = container.querySelector('[data-active-tab="true"]');
if (activeEl) {
activeEl.scrollIntoView({ behavior: tabScrollBehavior.value, block: "nearest", inline: "center" });
for (const container of [tabsContainerRef.value, fixedTabsContainerRef.value]) {
if (!container) continue;
const activeEl = container.querySelector('[data-active-tab="true"]');
if (activeEl) {
activeEl.scrollIntoView({ behavior: tabScrollBehavior.value, block: "nearest", inline: activeTabScrollInline(container, queryStore.activeTabId) });
break;
}
}
updateScrollButtons();
updateAllScrollButtons();
tabScrollBehavior.value = "smooth";
});
},
@ -184,7 +213,7 @@ watch(
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
}
updateScrollButtons();
updateAllScrollButtons();
});
},
);
@ -192,7 +221,7 @@ watch(
function tabColorStyle(tab: QueryTab) {
const color = connectionColor(tab.connectionId);
const isActive = tab.id === queryStore.activeTabId && !props.driverStoreActive;
const isClassic = settingsStore.editorSettings.appLayout === "classic";
const isClassic = isClassicLayout.value;
if (!color) {
if (isClassic) {
return isActive ? { boxShadow: "inset 0 -2px 0 var(--ring)" } : undefined;
@ -222,7 +251,9 @@ function tabIconClass(tab: QueryTab) {
return "text-blue-600 dark:text-blue-400";
}
const showTabOverflowControls = computed(() => queryStore.tabs.length > 0 && hasTabOverflow.value);
const showRegularTabScrollbar = computed(() => hasTabOverflow.value);
const showFixedTabScrollbar = computed(() => hasFixedTabOverflow.value);
const showRegularTabOverflowControls = computed(() => regularTabs.value.length > 0 && hasTabOverflow.value);
const openTabMenuItems = computed(() =>
queryStore.tabs.map((tab) => ({
@ -234,6 +265,16 @@ const openTabMenuItems = computed(() =>
})),
);
const fixedTabMenuItems = computed(() =>
fixedTabs.value.map((tab) => ({
value: tab.id,
label: tabDisplayTitle(tab, t),
title: tabDisplayTitle(tab, t),
icon: tabMenuIcon(tab),
iconClass: tabIconClass(tab),
})),
);
function tabMenuIcon(tab: QueryTab) {
if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "redis") return Table2;
if (tab.mode === "vector") return TableProperties;
@ -257,6 +298,15 @@ function handleTabMouseDown(event: MouseEvent, tabId: string) {
tabDrag.startDrag(event, tabId);
}
function handleTabDragTarget(event: MouseEvent, tab: QueryTab) {
const draggedTab = queryStore.tabs.find((item) => item.id === tabDrag.state.draggedId);
if (draggedTab && draggedTab.pinned !== tab.pinned) {
tabDrag.clearTarget(tab.id);
return;
}
tabDrag.updateTarget(event, tab.id);
}
function tabDropStyle(tabId: string) {
if (!tabDrag.state.active) return {};
if (tabDrag.state.draggedId === tabId) return { opacity: 0.4 };
@ -279,10 +329,16 @@ const tabScrollbarThumbStyle = computed<CSSProperties>(() => ({
width: `${scrollThumbWidthPercent.value}%`,
}));
const tabTailDragRegionClass = computed(() => (showTabOverflowControls.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
const fixedTabScrollbarThumbStyle = computed<CSSProperties>(() => ({
insetInlineStart: `${fixedScrollThumbLeftPercent.value}%`,
width: `${fixedScrollThumbWidthPercent.value}%`,
}));
const tabTailDragRegionClass = computed(() => (showRegularTabOverflowControls.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
const fixedTabTailDragRegionClass = computed(() => (showFixedTabScrollbar.value ? "w-0 flex-none self-stretch" : "min-w-8 flex-1 self-stretch"));
const tabOverflowControlClass = computed(() =>
settingsStore.editorSettings.appLayout === "classic"
isClassicLayout.value
? "h-full w-8 border-r border-border/80 dark:border-border/45 bg-background/80 text-foreground/75 hover:bg-accent hover:text-foreground disabled:cursor-default disabled:opacity-40"
: "h-7 w-7 rounded-md border border-border/60 bg-background text-foreground/70 hover:border-border hover:text-foreground",
);
@ -301,124 +357,223 @@ function activateTab(tabId: string) {
</script>
<template>
<div v-if="queryStore.tabs.length > 0 || driverStoreOpen" class="relative flex border-b shrink-0" :class="settingsStore.editorSettings.appLayout === 'classic' ? 'h-9 items-stretch bg-muted' : 'h-10 items-center bg-background px-2'">
<div class="app-tab-strip relative h-full min-w-0 flex-1">
<div v-if="showTabOverflowControls" class="app-tab-scrollbar" :class="{ 'app-tab-scrollbar--dragging': isScrollbarDragging }" @pointerdown="startScrollbarDrag">
<div class="app-tab-scrollbar__thumb" :style="tabScrollbarThumbStyle" />
</div>
<div ref="tabsContainerRef" class="app-tab-scroll flex min-w-0 flex-1 items-center overflow-x-auto" :class="settingsStore.editorSettings.appLayout === 'classic' ? 'h-full' : 'h-10 gap-1.5 py-1.5'" :style="tabsContainerStyle" @scroll="updateScrollButtons" @wheel="onTabsWheel">
<CustomContextMenu v-for="tab in queryStore.tabs" :key="tab.id" :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
<div :class="settingsStore.editorSettings.appLayout === 'classic' ? 'h-full' : ''" @contextmenu="onContextMenu">
<Tooltip>
<TooltipTrigger as-child>
<div
class="group flex items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap select-none"
:class="
settingsStore.editorSettings.appLayout === 'classic'
? [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-full border-r border-border/80 font-medium dark:border-border/45', tab.id === queryStore.activeTabId && !driverStoreActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90']
: [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-7 rounded-md border', tab.id === queryStore.activeTabId && !driverStoreActive ? 'text-foreground font-medium' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
"
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive"
@click="handleTabClick(tab)"
@dblclick.stop="startRenameTab(tab)"
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
@mousedown="handleTabMouseDown($event, tab.id)"
@mouseenter="tabDrag.updateTarget($event, tab.id)"
@mousemove="tabDrag.updateTarget($event, tab.id)"
@mouseleave="tabDrag.clearTarget(tab.id)"
>
<span class="shrink-0" :class="tabIconClass(tab)">
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper'" class="h-3.5 w-3.5" />
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
<Code2 v-else class="h-3.5 w-3.5" />
</span>
<input
v-if="editingTabId === tab.id"
v-model="editingTitle"
:data-tab-title-input="tab.id"
:aria-label="t('contextMenu.renameTab')"
class="h-5 min-w-0 flex-1 rounded border border-ring bg-background px-1.5 text-xs font-normal text-foreground outline-none"
@click.stop
@mousedown.stop
@keydown.enter.prevent="commitRenameTab(tab)"
@keydown.escape.prevent="cancelRenameTab"
@blur="commitRenameTab(tab)"
/>
<span v-else class="min-w-0 truncate flex-1">{{ tabDisplayTitle(tab, t) }}</span>
<Tooltip v-if="isConnectionReadonly(tab.connectionId)">
<TooltipTrigger as-child>
<Lock class="h-3 w-3 text-muted-foreground shrink-0" />
</TooltipTrigger>
<TooltipContent>{{ t("connection.readOnlyBadge") }}</TooltipContent>
</Tooltip>
<Pin v-if="tab.pinned" class="h-3 w-3 shrink-0 text-primary fill-current" aria-hidden="true" />
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="queryStore.closeTab(tab.id)">
<X class="h-3 w-3" />
</button>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
<template v-for="line in tabTooltipLines(tab, t)" :key="line.label">
<span class="text-muted-foreground">{{ line.label }}</span>
<span>{{ line.value }}</span>
</template>
</TooltipContent>
</Tooltip>
</div>
</CustomContextMenu>
<!-- Driver Store Tab -->
<div
v-if="driverStoreOpen"
data-driver-store-tab
class="group flex min-w-38 items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap"
:class="
settingsStore.editorSettings.appLayout === 'classic'
? ['h-full border-r border-border/80 dark:border-border/45 font-medium', driverStoreActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90']
: ['h-7 rounded-md border font-medium', driverStoreActive ? 'border-ring text-foreground' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
"
:style="settingsStore.editorSettings.appLayout === 'classic' && driverStoreActive ? { boxShadow: '0 1px 0 0 var(--color-background)' } : {}"
@click="emit('activate-driver-store')"
>
<span class="shrink-0 text-amber-600 dark:text-amber-400">
<Package class="h-3.5 w-3.5" />
</span>
<span class="min-w-0 truncate flex-1">{{ t("toolbar.driverManager") }}</span>
<span v-if="(agentDriverUpdateCount ?? 0) > 0" class="inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium leading-none text-white" :aria-label="t('toolbar.updatableDriverCount')">
{{ (agentDriverUpdateCount ?? 0) > 99 ? "99+" : agentDriverUpdateCount }}
</span>
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="emit('close-driver-store')">
<X class="h-3 w-3" />
</button>
<div v-if="queryStore.tabs.length > 0 || driverStoreOpen" class="relative flex w-full min-w-0 shrink-0 overflow-hidden border-b" :class="[isClassicLayout ? 'bg-muted' : 'bg-background', hasFixedTabs ? 'flex-col' : '']">
<div class="flex w-full min-w-0 shrink-0 overflow-hidden" :class="isClassicLayout ? 'h-9 items-stretch' : 'h-10 items-center px-2'">
<div class="app-tab-strip relative h-full min-w-0 flex-1 overflow-hidden">
<div v-if="showRegularTabScrollbar" class="app-tab-scrollbar" :class="{ 'app-tab-scrollbar--dragging': isScrollbarDragging }" @pointerdown="startScrollbarDrag">
<div class="app-tab-scrollbar__thumb" :style="tabScrollbarThumbStyle" />
</div>
<div :class="tabTailDragRegionClass" data-tauri-drag-region />
<div ref="tabsContainerRef" class="app-tab-scroll flex w-full min-w-0 flex-1 items-center overflow-x-auto" :class="isClassicLayout ? 'h-full' : 'h-full gap-1.5 py-1.5'" :style="tabsContainerStyle" @scroll="updateScrollButtons" @wheel="onTabsWheel">
<CustomContextMenu v-for="tab in regularTabs" :key="tab.id" :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
<Tooltip>
<TooltipTrigger as-child>
<div
class="group flex items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap select-none"
:class="
isClassicLayout
? [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-full border-r border-border/80 font-medium dark:border-border/45', tab.id === queryStore.activeTabId && !driverStoreActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90']
: [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-7 rounded-md border', tab.id === queryStore.activeTabId && !driverStoreActive ? 'text-foreground font-medium' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
"
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive"
@click="handleTabClick(tab)"
@dblclick.stop="startRenameTab(tab)"
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
@mousedown="handleTabMouseDown($event, tab.id)"
@mouseenter="handleTabDragTarget($event, tab)"
@mousemove="handleTabDragTarget($event, tab)"
@mouseleave="tabDrag.clearTarget(tab.id)"
>
<span class="shrink-0" :class="tabIconClass(tab)">
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper'" class="h-3.5 w-3.5" />
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
<Code2 v-else class="h-3.5 w-3.5" />
</span>
<input
v-if="editingTabId === tab.id"
v-model="editingTitle"
:data-tab-title-input="tab.id"
:aria-label="t('contextMenu.renameTab')"
class="h-5 min-w-0 flex-1 rounded border border-ring bg-background px-1.5 text-xs font-normal text-foreground outline-none"
@click.stop
@mousedown.stop
@keydown.enter.prevent="commitRenameTab(tab)"
@keydown.escape.prevent="cancelRenameTab"
@blur="commitRenameTab(tab)"
/>
<span v-else class="min-w-0 truncate flex-1">{{ tabDisplayTitle(tab, t) }}</span>
<Tooltip v-if="isConnectionReadonly(tab.connectionId)">
<TooltipTrigger as-child>
<Lock class="h-3 w-3 text-muted-foreground shrink-0" />
</TooltipTrigger>
<TooltipContent>{{ t("connection.readOnlyBadge") }}</TooltipContent>
</Tooltip>
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="queryStore.closeTab(tab.id)">
<X class="h-3 w-3" />
</button>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
<template v-for="line in tabTooltipLines(tab, t)" :key="line.label">
<span class="text-muted-foreground">{{ line.label }}</span>
<span>{{ line.value }}</span>
</template>
</TooltipContent>
</Tooltip>
</div>
</CustomContextMenu>
<!-- Driver Store Tab -->
<div
v-if="driverStoreOpen"
data-driver-store-tab
class="group flex min-w-38 items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap"
:class="
isClassicLayout
? ['h-full border-r border-border/80 dark:border-border/45 font-medium', driverStoreActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90']
: ['h-7 rounded-md border font-medium', driverStoreActive ? 'border-ring text-foreground' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
"
:style="isClassicLayout && driverStoreActive ? { boxShadow: '0 1px 0 0 var(--color-background)' } : {}"
@click="emit('activate-driver-store')"
>
<span class="shrink-0 text-amber-600 dark:text-amber-400">
<Package class="h-3.5 w-3.5" />
</span>
<span class="min-w-0 truncate flex-1">{{ t("toolbar.driverManager") }}</span>
<span v-if="(agentDriverUpdateCount ?? 0) > 0" class="inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium leading-none text-white" :aria-label="t('toolbar.updatableDriverCount')">
{{ (agentDriverUpdateCount ?? 0) > 99 ? "99+" : agentDriverUpdateCount }}
</span>
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="emit('close-driver-store')">
<X class="h-3 w-3" />
</button>
</div>
<div :class="tabTailDragRegionClass" data-tauri-drag-region />
</div>
</div>
<div v-if="showRegularTabOverflowControls" class="relative z-30 flex shrink-0 items-center">
<LightDropdown
:model-value="queryStore.activeTabId || ''"
:items="openTabMenuItems"
:aria-label="t('tabs.openTabs')"
:trigger-title="t('tabs.openTabs')"
:trigger-icon="ChevronDown"
:trigger-class="['inline-flex shrink-0 items-center justify-center', tabOverflowControlClass].join(' ')"
trigger-icon-class="h-4 w-4"
item-icon-class="w-3.5 h-3.5 mr-2"
item-class="max-w-full"
content-class="w-auto min-w-48 max-w-72"
:show-trigger-label="false"
:show-chevron="false"
:highlight-selected="false"
:match-trigger-width="false"
check-position="none"
align="end"
@update:model-value="activateTab"
/>
</div>
</div>
<div v-if="showTabOverflowControls" class="relative z-30 flex shrink-0 items-center">
<LightDropdown
:model-value="queryStore.activeTabId || ''"
:items="openTabMenuItems"
:aria-label="t('tabs.openTabs')"
:trigger-title="t('tabs.openTabs')"
:trigger-icon="ChevronDown"
:trigger-class="['inline-flex shrink-0 items-center justify-center', tabOverflowControlClass].join(' ')"
trigger-icon-class="h-4 w-4"
item-icon-class="w-3.5 h-3.5 mr-2"
item-class="max-w-full"
content-class="w-auto min-w-48 max-w-72"
:show-trigger-label="false"
:show-chevron="false"
:highlight-selected="false"
:match-trigger-width="false"
check-position="none"
align="end"
@update:model-value="activateTab"
/>
<div v-if="hasFixedTabs" class="flex w-full min-w-0 shrink-0 overflow-hidden border-t" :class="isClassicLayout ? 'h-8 items-stretch border-border/80 bg-background/45 dark:border-border/45 dark:bg-background/20' : 'h-9 items-center border-border/70 bg-muted/45 px-2 dark:bg-muted/25'">
<div class="app-tab-strip relative h-full min-w-0 flex-1 overflow-hidden">
<div v-if="showFixedTabScrollbar" class="app-tab-scrollbar app-tab-scrollbar--bottom" :class="{ 'app-tab-scrollbar--dragging': isFixedScrollbarDragging }" @pointerdown="startFixedScrollbarDrag">
<div class="app-tab-scrollbar__thumb" :style="fixedTabScrollbarThumbStyle" />
</div>
<div ref="fixedTabsContainerRef" class="app-tab-scroll flex w-full min-w-0 flex-1 items-center overflow-x-auto" :class="isClassicLayout ? 'h-full' : 'h-full gap-1.5 py-1'" :style="tabsContainerStyle" @scroll="updateFixedScrollButtons" @wheel="onFixedTabsWheel">
<CustomContextMenu v-for="tab in fixedTabs" :key="tab.id" :items="getTabMenuItems(tab)" v-slot="{ onContextMenu }">
<div :class="isClassicLayout ? 'h-full' : ''" @contextmenu="onContextMenu">
<Tooltip>
<TooltipTrigger as-child>
<div
class="group flex items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap select-none"
:class="
isClassicLayout
? [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-full border-r border-border/80 font-medium dark:border-border/45', tab.id === queryStore.activeTabId && !driverStoreActive ? 'bg-background text-foreground' : 'text-foreground/70 hover:text-foreground/90']
: [compactTabTitle ? 'min-w-24' : 'min-w-38', 'h-7 rounded-md border', tab.id === queryStore.activeTabId && !driverStoreActive ? 'text-foreground font-medium' : 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90']
"
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive"
@click="handleTabClick(tab)"
@dblclick.stop="startRenameTab(tab)"
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
@mousedown="handleTabMouseDown($event, tab.id)"
@mouseenter="handleTabDragTarget($event, tab)"
@mousemove="handleTabDragTarget($event, tab)"
@mouseleave="tabDrag.clearTarget(tab.id)"
>
<span class="shrink-0" :class="tabIconClass(tab)">
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper'" class="h-3.5 w-3.5" />
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
<Code2 v-else class="h-3.5 w-3.5" />
</span>
<input
v-if="editingTabId === tab.id"
v-model="editingTitle"
:data-tab-title-input="tab.id"
:aria-label="t('contextMenu.renameTab')"
class="h-5 min-w-0 flex-1 rounded border border-ring bg-background px-1.5 text-xs font-normal text-foreground outline-none"
@click.stop
@mousedown.stop
@keydown.enter.prevent="commitRenameTab(tab)"
@keydown.escape.prevent="cancelRenameTab"
@blur="commitRenameTab(tab)"
/>
<span v-else class="min-w-0 truncate flex-1 text-foreground">{{ tabDisplayTitle(tab, t) }}</span>
<Tooltip v-if="isConnectionReadonly(tab.connectionId)">
<TooltipTrigger as-child>
<Lock class="h-3 w-3 text-muted-foreground shrink-0" />
</TooltipTrigger>
<TooltipContent>{{ t("connection.readOnlyBadge") }}</TooltipContent>
</Tooltip>
<button class="rounded p-0.5 text-primary hover:bg-muted-foreground/20 shrink-0" :aria-label="t('contextMenu.unfixTab')" :title="t('contextMenu.unfixTab')" @click.stop="queryStore.togglePinnedTab(tab.id)">
<Pin class="h-3 w-3 fill-current" aria-hidden="true" />
</button>
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="queryStore.closeTab(tab.id)">
<X class="h-3 w-3" />
</button>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" class="text-xs grid grid-cols-[auto_1fr] gap-x-2">
<template v-for="line in tabTooltipLines(tab, t)" :key="line.label">
<span class="text-muted-foreground">{{ line.label }}</span>
<span>{{ line.value }}</span>
</template>
</TooltipContent>
</Tooltip>
</div>
</CustomContextMenu>
<div :class="fixedTabTailDragRegionClass" data-tauri-drag-region />
</div>
</div>
<div v-if="showFixedTabScrollbar" class="relative z-30 flex shrink-0 items-center">
<LightDropdown
:model-value="queryStore.activeTabId || ''"
:items="fixedTabMenuItems"
:aria-label="t('tabs.fixedTabs')"
:trigger-title="t('tabs.fixedTabs')"
:trigger-icon="ChevronDown"
:trigger-class="['inline-flex shrink-0 items-center justify-center', tabOverflowControlClass].join(' ')"
trigger-icon-class="h-4 w-4"
item-icon-class="w-3.5 h-3.5 mr-2"
item-class="max-w-full"
content-class="w-auto min-w-48 max-w-72"
:show-trigger-label="false"
:show-chevron="false"
:highlight-selected="false"
:match-trigger-width="false"
check-position="none"
align="end"
@update:model-value="activateTab"
/>
</div>
</div>
</div>
@ -482,6 +637,16 @@ function activateTab(tabId: string) {
background: color-mix(in oklch, var(--foreground) 8%, transparent);
}
.app-tab-scrollbar--bottom {
top: auto;
bottom: 0;
}
.app-tab-scrollbar--bottom::before {
top: auto;
bottom: 0;
}
.app-tab-scrollbar__thumb {
position: absolute;
top: 0;
@ -494,6 +659,11 @@ function activateTab(tabId: string) {
background-color 120ms ease;
}
.app-tab-scrollbar--bottom .app-tab-scrollbar__thumb {
top: auto;
bottom: 0;
}
.app-tab-scrollbar:hover .app-tab-scrollbar__thumb,
.app-tab-scrollbar--dragging .app-tab-scrollbar__thumb {
height: 5px;

View File

@ -514,6 +514,7 @@ export default {
scrollLeft: "Scroll tabs left",
scrollRight: "Scroll tabs right",
openTabs: "Open tabs",
fixedTabs: "Fixed tabs",
openDataTabs: "Open tables",
},
executionSummary: {
@ -1209,10 +1210,13 @@ export default {
expandAll: "Expand All",
pin: "Pin",
unpin: "Unpin",
fixTab: "Fix Tab",
unfixTab: "Unfix Tab",
refreshTab: "Refresh Data",
renameTab: "Rename Tab",
duplicateTab: "Duplicate Tab",
compactTabTitle: "Shorten Tab Titles",
fullTabTitle: "Show Full Tab Titles",
closeTab: "Close Tab",
closeOtherTabs: "Close Other Tabs",
closeAllTabs: "Close All Tabs",

View File

@ -517,6 +517,7 @@ export default withEnglishFallback({
scrollLeft: "向左滚动标签页",
scrollRight: "向右滚动标签页",
openTabs: "已打开的标签页",
fixedTabs: "固定标签页",
openDataTabs: "已打开的数据表",
},
executionSummary: {
@ -1212,10 +1213,13 @@ export default withEnglishFallback({
expandAll: "展开全部",
pin: "置顶",
unpin: "取消置顶",
fixTab: "固定标签页",
unfixTab: "取消固定标签页",
refreshTab: "刷新数据",
renameTab: "重命名标签页",
duplicateTab: "复制标签页",
compactTabTitle: "缩短标签标题",
fullTabTitle: "完整标签标题",
closeTab: "关闭标签页",
closeOtherTabs: "关闭其他标签页",
closeAllTabs: "关闭全部标签页",