feat: add panel resizing functionality and styles for AI and sidebar panels

This commit is contained in:
t8y2 2026-05-01 00:35:59 +08:00
parent 75244339e4
commit e6ecb216d5
3 changed files with 80 additions and 38 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { ref, computed, watch, onMounted, onUnmounted, type Ref } from "vue";
import { useI18n } from "vue-i18n";
import { DatabaseZap, FilePlus2, Play, Loader2, Square, X, Globe, Moon, Sun, Upload, Download, Plus, History, Server, Table2, Database, Search, ShieldCheck, Bot, Pin, AlignLeft, CloudDownload } from "lucide-vue-next";
import { DatabaseZap, FilePlus2, Play, Loader2, Square, X, Globe, Moon, Sun, Upload, Download, Plus, History, Server, Table2, Database, Search, ShieldCheck, Bot, Pin, AlignLeft, CloudDownload, ArrowLeftRight } from "lucide-vue-next";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import { Button } from "@/components/ui/button";
@ -59,21 +59,41 @@ const { message: toastMessage, visible: toastVisible, toast } = useToast();
const showConnectionDialog = ref(false);
const showHistory = ref(false);
const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") !== "false");
const aiPanelSize = ref(Number(localStorage.getItem("dbx-ai-panel-size")) || 25);
const aiPanelWidth = ref(Number(localStorage.getItem("dbx-ai-panel-width")) || 360);
const sidebarWidth = ref(Number(localStorage.getItem("dbx-sidebar-width")) || 260);
const historyWidth = ref(Number(localStorage.getItem("dbx-history-width")) || 288);
function toggleAiPanel() {
showAiPanel.value = !showAiPanel.value;
localStorage.setItem("dbx-ai-panel-open", String(showAiPanel.value));
}
function onAiPanelResized(panes: { size: number }[]) {
const aiPane = panes[panes.length - 1];
if (aiPane) {
aiPanelSize.value = aiPane.size;
localStorage.setItem("dbx-ai-panel-size", String(aiPane.size));
}
function startPanelResize(widthRef: Ref<number>, storageKey: string, direction: 'left' | 'right') {
return (e: MouseEvent) => {
e.preventDefault();
const startX = e.clientX;
const startWidth = widthRef.value;
const onMouseMove = (ev: MouseEvent) => {
const delta = ev.clientX - startX;
widthRef.value = Math.max(180, Math.min(800, startWidth + (direction === 'right' ? delta : -delta)));
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
localStorage.setItem(storageKey, String(widthRef.value));
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
}
const startSidebarResize = startPanelResize(sidebarWidth, "dbx-sidebar-width", 'right');
const startAiPanelResize = startPanelResize(aiPanelWidth, "dbx-ai-panel-width", 'left');
const startHistoryResize = startPanelResize(historyWidth, "dbx-history-width", 'left');
const showUpdateDialog = ref(false);
const dangerSql = ref("");
const pendingDangerSql = ref("");
@ -637,23 +657,20 @@ async function setupFileDrop() {
<div class="h-screen w-screen flex flex-col bg-background text-foreground overflow-hidden">
<!-- Toolbar -->
<div class="h-10 flex items-center gap-1 px-2 border-b bg-muted/30 shrink-0">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="showConnectionDialog = true">
<DatabaseZap class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t('toolbar.newConnection') }}</TooltipContent>
</Tooltip>
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs gap-1" @click="showConnectionDialog = true">
<DatabaseZap class="h-3.5 w-3.5" />
{{ t('toolbar.newConnection') }}
</Button>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="newQuery" :disabled="!connectionStore.connections.length">
<FilePlus2 class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t('toolbar.newQuery') }}</TooltipContent>
</Tooltip>
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs gap-1" @click="newQuery" :disabled="!connectionStore.connections.length">
<FilePlus2 class="h-3.5 w-3.5" />
{{ t('toolbar.newQuery') }}
</Button>
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs gap-1" @click="showTransferDialog = true" :disabled="!connectionStore.connections.length">
<ArrowLeftRight class="h-3.5 w-3.5" />
{{ t('transfer.dataTransfer') }}
</Button>
<div class="flex-1" />
@ -716,9 +733,8 @@ async function setupFileDrop() {
<!-- Main Content -->
<div class="flex-1 flex min-h-0">
<Splitpanes class="flex-1 min-h-0 min-w-0" @resized="onAiPanelResized">
<!-- Sidebar -->
<Pane :size="20" :min-size="10" :max-size="40">
<!-- Sidebar (fixed pixel width) -->
<div class="h-full shrink-0 relative" :style="{ width: sidebarWidth + 'px' }">
<div class="h-full flex flex-col overflow-hidden">
<div class="h-9 flex items-center px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20">
{{ t('sidebar.connections') }}
@ -744,10 +760,11 @@ async function setupFileDrop() {
<ConnectionTree />
</div>
</div>
</Pane>
<div class="panel-resize-handle panel-resize-handle--right" @mousedown="startSidebarResize" />
</div>
<!-- Editor + Results -->
<Pane :size="80">
<div class="flex-1 min-w-0">
<div class="h-full flex flex-col min-w-0">
<!-- Tabs Bar -->
<div v-if="queryStore.tabs.length > 0" class="h-9 flex items-center border-b bg-muted/20 overflow-x-auto shrink-0">
@ -1076,10 +1093,12 @@ async function setupFileDrop() {
</div>
</div>
</div>
</Pane>
</div>
<Pane v-if="showAiPanel" :size="aiPanelSize" :min-size="15" :max-size="40">
<div class="h-full min-h-0 overflow-hidden bg-background">
<!-- AI Panel (outside Splitpanes, pixel-based width) -->
<div v-if="showAiPanel" class="h-full shrink-0 relative bg-background" :style="{ width: aiPanelWidth + 'px' }">
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startAiPanelResize" />
<div class="h-full min-h-0 overflow-hidden">
<AiAssistant
:tab="activeTab"
:connection="activeConnection"
@ -1088,11 +1107,11 @@ async function setupFileDrop() {
@close="toggleAiPanel"
/>
</div>
</Pane>
</Splitpanes>
</div>
<!-- History Panel (fixed width, outside Splitpanes) -->
<div v-if="showHistory" class="w-72 h-full shrink-0 border-l bg-background">
<!-- History Panel -->
<div v-if="showHistory" class="h-full shrink-0 relative bg-background" :style="{ width: historyWidth + 'px' }">
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startHistoryResize" />
<QueryHistory @restore="onHistoryRestore" @close="showHistory = false" />
</div>
</div>

View File

@ -50,7 +50,7 @@ onMounted(() => store.load());
<template>
<div class="h-full flex flex-col overflow-hidden border-l">
<div class="flex items-center gap-1 px-2 py-1.5 border-b shrink-0 bg-muted/20">
<div class="h-9 flex items-center gap-1 px-2 border-b shrink-0 bg-muted/20">
<Clock class="w-3.5 h-3.5 text-muted-foreground shrink-0" />
<span class="text-xs font-medium">{{ t('history.title') }}</span>
<span class="flex-1" />

View File

@ -160,6 +160,29 @@
background: oklch(0.6 0.15 250) !important;
}
/* Panel resize handles */
.panel-resize-handle {
position: absolute;
top: 0;
bottom: 0;
width: 3px;
cursor: col-resize;
z-index: 10;
background: transparent;
}
.panel-resize-handle--right {
right: 0;
border-right: 1px solid var(--border);
}
.panel-resize-handle--left {
left: 0;
border-left: 1px solid var(--border);
}
.panel-resize-handle:hover,
.panel-resize-handle:active {
background: oklch(0.6 0.15 250);
}
/* Frosted glass menus */
.cn-menu-translucent {
backdrop-filter: blur(16px) saturate(1.6) !important;