mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-30 08:20:33 +00:00
feat(ui): add layout preference
This commit is contained in:
+26
-4
@@ -115,6 +115,7 @@ const { onExecuteSql, onReloadData, onPaginate, onSort } = useDataGridActions(ac
|
||||
const { setupTauriListeners } = useTauriEvents({ openTableTarget });
|
||||
|
||||
const appVersion = ref("");
|
||||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
const hasSqlFileConnections = computed(() =>
|
||||
connectionStore.connections.some((c) => !SQL_FILE_UNSUPPORTED_TYPES.has(c.db_type)),
|
||||
);
|
||||
@@ -511,15 +512,28 @@ onUnmounted(() => {
|
||||
@open-sql-file="dialogs.showSqlFileDialog.value = true"
|
||||
/>
|
||||
|
||||
<div class="app-panel-gutter flex-1 flex min-h-0 gap-1 p-1">
|
||||
<div
|
||||
:class="
|
||||
isClassicLayout
|
||||
? 'app-layout-classic flex-1 flex min-h-0'
|
||||
: 'app-panel-gutter flex-1 flex min-h-0 gap-1 p-1'
|
||||
"
|
||||
>
|
||||
<AppSidebar
|
||||
:sidebar-width="sidebarWidth"
|
||||
:classic-layout="isClassicLayout"
|
||||
@import="dialogs.onImportClick"
|
||||
@export="dialogs.onExportClick"
|
||||
@start-resize="startSidebarResize"
|
||||
/>
|
||||
|
||||
<div class="flex-1 min-w-0 overflow-hidden rounded-md border border-border/80 bg-background">
|
||||
<div
|
||||
:class="
|
||||
isClassicLayout
|
||||
? 'flex-1 min-w-0'
|
||||
: 'flex-1 min-w-0 overflow-hidden rounded-md border border-border/80 bg-background'
|
||||
"
|
||||
>
|
||||
<div class="h-full flex flex-col min-w-0">
|
||||
<AppTabBar />
|
||||
<div v-if="activeTab" class="flex flex-col flex-1 min-h-0">
|
||||
@@ -590,7 +604,11 @@ onUnmounted(() => {
|
||||
|
||||
<div
|
||||
v-if="showAiPanel"
|
||||
class="h-full shrink-0 relative overflow-hidden rounded-md border border-border/80 bg-background"
|
||||
:class="
|
||||
isClassicLayout
|
||||
? 'h-full shrink-0 relative bg-background'
|
||||
: 'h-full shrink-0 relative overflow-hidden rounded-md border border-border/80 bg-background'
|
||||
"
|
||||
:style="{ width: aiPanelWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startAiPanelResize" />
|
||||
@@ -608,7 +626,11 @@ onUnmounted(() => {
|
||||
|
||||
<div
|
||||
v-if="showHistory"
|
||||
class="h-full shrink-0 relative overflow-hidden rounded-md border border-border/80 bg-background"
|
||||
:class="
|
||||
isClassicLayout
|
||||
? 'h-full shrink-0 relative bg-background'
|
||||
: 'h-full shrink-0 relative overflow-hidden rounded-md border border-border/80 bg-background'
|
||||
"
|
||||
:style="{ width: historyWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startHistoryResize" />
|
||||
|
||||
@@ -463,7 +463,10 @@ function formatInlineText(text: string): string {
|
||||
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col overflow-hidden">
|
||||
<div class="h-10 flex items-center gap-2 border-b px-3 shrink-0">
|
||||
<div
|
||||
class="flex items-center gap-2 border-b px-3 shrink-0"
|
||||
:class="settings.editorSettings.appLayout === 'classic' ? 'h-9' : 'h-10'"
|
||||
>
|
||||
<span class="flex-1 truncate text-xs font-medium">{{ chatTitle }}</span>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6" @click="startNewChat" :title="t('ai.newChat')">
|
||||
<MessageSquarePlus class="h-3.5 w-3.5" />
|
||||
|
||||
@@ -31,6 +31,7 @@ const editFontSize = ref(settingsStore.editorSettings.fontSize);
|
||||
const editTheme = ref(settingsStore.editorSettings.theme);
|
||||
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
|
||||
|
||||
// Sync from store when dialog opens
|
||||
watch(
|
||||
@@ -42,6 +43,7 @@ watch(
|
||||
editTheme.value = settingsStore.editorSettings.theme;
|
||||
editExecuteMode.value = settingsStore.editorSettings.executeMode;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editAppLayout.value = settingsStore.editorSettings.appLayout;
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -52,7 +54,8 @@ function hasChanges(): boolean {
|
||||
editFontSize.value !== settingsStore.editorSettings.fontSize ||
|
||||
editTheme.value !== settingsStore.editorSettings.theme ||
|
||||
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
|
||||
editAppLayout.value !== settingsStore.editorSettings.appLayout
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,6 +66,7 @@ function applySettings() {
|
||||
theme: editTheme.value,
|
||||
executeMode: editExecuteMode.value,
|
||||
wordWrap: editWordWrap.value,
|
||||
appLayout: editAppLayout.value,
|
||||
});
|
||||
emit("update:open", false);
|
||||
}
|
||||
@@ -73,6 +77,7 @@ function resetDefaults() {
|
||||
editTheme.value = DEFAULT_EDITOR_SETTINGS.theme;
|
||||
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
|
||||
}
|
||||
|
||||
function onExecuteModeChange(v: any) {
|
||||
@@ -87,6 +92,10 @@ function onThemeChange(v: any) {
|
||||
if (typeof v === "string") editTheme.value = v as typeof DEFAULT_EDITOR_SETTINGS.theme;
|
||||
}
|
||||
|
||||
function setAppLayout(value: "separated" | "classic") {
|
||||
editAppLayout.value = value;
|
||||
}
|
||||
|
||||
const activeSettingsTab = ref("editor");
|
||||
const isWeb = !isTauriRuntime();
|
||||
|
||||
@@ -251,6 +260,7 @@ watch(
|
||||
<Tabs v-model="activeSettingsTab">
|
||||
<TabsList class="w-full">
|
||||
<TabsTrigger value="editor" class="flex-1">{{ t("settings.editorTab") }}</TabsTrigger>
|
||||
<TabsTrigger value="appearance" class="flex-1">{{ t("settings.appearanceTab") }}</TabsTrigger>
|
||||
<TabsTrigger v-if="isWeb" value="security" class="flex-1">{{ t("settings.securityTab") }}</TabsTrigger>
|
||||
<TabsTrigger value="about" class="flex-1">{{ t("settings.aboutTab") }}</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -330,6 +340,8 @@ watch(
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.executeMode") }}</Label>
|
||||
<Select :model-value="editExecuteMode" @update:model-value="onExecuteModeChange">
|
||||
@@ -384,6 +396,52 @@ watch(
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="appearance" class="space-y-5 py-2">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.appLayout") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">{{ t("settings.appLayoutDescription") }}</p>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="h-auto justify-start p-3"
|
||||
:class="editAppLayout === 'separated' ? 'border-primary bg-primary/10' : ''"
|
||||
@click="setAppLayout('separated')"
|
||||
>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-medium">{{ t("settings.appLayoutSeparated") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("settings.appLayoutSeparatedDescription") }}</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="h-auto justify-start p-3"
|
||||
:class="editAppLayout === 'classic' ? 'border-primary bg-primary/10' : ''"
|
||||
@click="setAppLayout('classic')"
|
||||
>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-medium">{{ t("settings.appLayoutClassic") }}</div>
|
||||
<div class="text-xs text-muted-foreground">{{ t("settings.appLayoutClassicDescription") }}</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="border-t-0 bg-transparent gap-2 sm:gap-0">
|
||||
<Button variant="outline" @click="resetDefaults">
|
||||
{{ t("settings.resetDefaults") }}
|
||||
</Button>
|
||||
<div class="flex-1" />
|
||||
<Button variant="outline" @click="emit('update:open', false)">
|
||||
{{ t("common.close") }}
|
||||
</Button>
|
||||
<Button :disabled="!hasChanges()" @click="applySettings">
|
||||
{{ t("settings.apply") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="isWeb" value="security" class="space-y-5 py-2">
|
||||
<div class="space-y-3">
|
||||
<Label class="text-base">{{ t("auth.changePassword") }}</Label>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useToast } from "@/composables/useToast";
|
||||
|
||||
defineProps<{
|
||||
sidebarWidth: number;
|
||||
classicLayout?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -32,11 +33,15 @@ async function refreshTree() {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full shrink-0 relative select-none overflow-hidden rounded-md border border-border/80 bg-background"
|
||||
class="h-full shrink-0 relative select-none"
|
||||
:class="classicLayout ? '' : 'overflow-hidden rounded-md border border-border/80 bg-background'"
|
||||
:style="{ width: sidebarWidth + 'px' }"
|
||||
>
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div class="h-10 flex items-center px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20">
|
||||
<div
|
||||
class="flex items-center px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20"
|
||||
:class="classicLayout ? 'h-9' : 'h-10'"
|
||||
>
|
||||
{{ t("sidebar.connections") }}
|
||||
<span class="flex-1" />
|
||||
<Tooltip>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/components/ui/context-menu";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useTabScroll } from "@/composables/useTabScroll";
|
||||
import { connectionColor, tabDisplayTitle, tabTooltipLines } from "@/lib/tabPresentation";
|
||||
import { hexToRgba } from "@/lib/color";
|
||||
@@ -18,6 +19,7 @@ import type { QueryTab } from "@/types/database";
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const tabsContainerRef = ref<HTMLElement | null>(null);
|
||||
const { canScrollLeft, canScrollRight, updateScrollButtons, scrollTabs } = useTabScroll(tabsContainerRef);
|
||||
@@ -47,8 +49,23 @@ watch(
|
||||
function tabColorStyle(tab: QueryTab) {
|
||||
const color = connectionColor(tab.connectionId);
|
||||
const isActive = tab.id === queryStore.activeTabId;
|
||||
const isClassic = settingsStore.editorSettings.appLayout === "classic";
|
||||
if (!color) {
|
||||
return isActive ? { borderColor: "var(--ring)" } : undefined;
|
||||
if (isClassic) {
|
||||
return isActive ? { boxShadow: "0 1px 0 0 var(--color-background)" } : undefined;
|
||||
}
|
||||
return isActive
|
||||
? {
|
||||
borderColor: "var(--ring)",
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (isClassic) {
|
||||
return {
|
||||
backgroundColor: hexToRgba(color, isActive ? 0.16 : 0.07),
|
||||
boxShadow: isActive ? `inset 0 -2px 0 ${color}` : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -59,7 +76,15 @@ function tabColorStyle(tab: QueryTab) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="queryStore.tabs.length > 0" class="relative h-10 flex items-center border-b bg-background px-2 shrink-0">
|
||||
<div
|
||||
v-if="queryStore.tabs.length > 0"
|
||||
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'
|
||||
"
|
||||
>
|
||||
<button
|
||||
v-if="canScrollLeft"
|
||||
class="absolute left-0 z-10 h-full pl-1 pr-6 bg-linear-to-r from-background from-40% to-transparent text-muted-foreground hover:text-foreground"
|
||||
@@ -70,20 +95,31 @@ function tabColorStyle(tab: QueryTab) {
|
||||
</button>
|
||||
<div
|
||||
ref="tabsContainerRef"
|
||||
class="flex-1 flex items-center gap-1.5 overflow-x-auto min-w-0"
|
||||
class="flex-1 flex items-center overflow-x-auto min-w-0"
|
||||
:class="settingsStore.editorSettings.appLayout === 'classic' ? '' : 'gap-1.5'"
|
||||
style="-ms-overflow-style: none; scrollbar-width: none; -webkit-overflow-scrolling: touch"
|
||||
@scroll="updateScrollButtons"
|
||||
>
|
||||
<ContextMenu v-for="tab in queryStore.tabs" :key="tab.id">
|
||||
<ContextMenuTrigger>
|
||||
<ContextMenuTrigger :class="settingsStore.editorSettings.appLayout === 'classic' ? 'h-full' : ''">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="group flex h-7 min-w-38 items-center gap-1 rounded-md border px-2 text-xs cursor-pointer transition-colors whitespace-nowrap"
|
||||
class="group flex min-w-38 items-center gap-1 px-2 text-xs cursor-pointer transition-colors whitespace-nowrap"
|
||||
:class="
|
||||
tab.id === queryStore.activeTabId
|
||||
? 'text-foreground font-medium'
|
||||
: 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90'
|
||||
settingsStore.editorSettings.appLayout === 'classic'
|
||||
? [
|
||||
'h-full border-r border-border/50',
|
||||
tab.id === queryStore.activeTabId
|
||||
? 'bg-background text-foreground font-medium'
|
||||
: 'text-foreground/70 hover:text-foreground/90',
|
||||
]
|
||||
: [
|
||||
'h-7 rounded-md border',
|
||||
tab.id === queryStore.activeTabId
|
||||
? 'text-foreground font-medium'
|
||||
: 'border-border/60 text-foreground/70 hover:border-border hover:text-foreground/90',
|
||||
]
|
||||
"
|
||||
:style="tabColorStyle(tab)"
|
||||
:data-active-tab="tab.id === queryStore.activeTabId"
|
||||
|
||||
@@ -1349,11 +1349,6 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
||||
class="w-3.5 h-3.5 shrink-0"
|
||||
:class="getIconInfo(node)?.colorClass"
|
||||
/>
|
||||
<span
|
||||
v-if="node.type === 'connection'"
|
||||
class="h-3 w-1.5 rounded-full shrink-0"
|
||||
:style="{ backgroundColor: connectionColor || '#9ca3af' }"
|
||||
/>
|
||||
<input
|
||||
v-if="isRenamingGroup"
|
||||
v-model="renameInput"
|
||||
|
||||
@@ -784,6 +784,7 @@ export default {
|
||||
settings: {
|
||||
title: "Settings",
|
||||
editorTab: "Editor",
|
||||
appearanceTab: "Appearance",
|
||||
securityTab: "Security",
|
||||
aboutTab: "About",
|
||||
fontFamily: "Font Family",
|
||||
@@ -791,6 +792,12 @@ export default {
|
||||
fontSize: "Font Size",
|
||||
theme: "Theme",
|
||||
selectTheme: "Select theme...",
|
||||
appLayout: "Interface Layout",
|
||||
appLayoutDescription: "Choose the main panel spacing, tab style, and header density.",
|
||||
appLayoutSeparated: "Modern separated",
|
||||
appLayoutSeparatedDescription: "Clear panel separation with block-style tabs.",
|
||||
appLayoutClassic: "Classic compact",
|
||||
appLayoutClassicDescription: "Continuous tab bar with compact panel headers.",
|
||||
apply: "Apply",
|
||||
resetDefaults: "Reset Defaults",
|
||||
executeMode: "Execute Mode (Cmd+Enter)",
|
||||
|
||||
@@ -771,6 +771,7 @@ export default {
|
||||
settings: {
|
||||
title: "设置",
|
||||
editorTab: "编辑器",
|
||||
appearanceTab: "外观",
|
||||
securityTab: "安全",
|
||||
aboutTab: "关于我们",
|
||||
fontFamily: "字体",
|
||||
@@ -778,6 +779,12 @@ export default {
|
||||
fontSize: "字号",
|
||||
theme: "主题",
|
||||
selectTheme: "选择主题...",
|
||||
appLayout: "界面布局",
|
||||
appLayoutDescription: "选择主界面的面板分隔、标签页和标题栏样式。",
|
||||
appLayoutSeparated: "现代分隔",
|
||||
appLayoutSeparatedDescription: "面板分隔更清晰,标签页为块状样式。",
|
||||
appLayoutClassic: "经典紧凑",
|
||||
appLayoutClassicDescription: "使用连续标签栏和更紧凑的面板标题。",
|
||||
apply: "应用",
|
||||
resetDefaults: "恢复默认",
|
||||
executeMode: "执行模式 (Cmd+Enter)",
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface EditorSettings {
|
||||
theme: EditorTheme;
|
||||
executeMode: "all" | "current";
|
||||
wordWrap: boolean;
|
||||
appLayout: "separated" | "classic";
|
||||
}
|
||||
|
||||
export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [
|
||||
@@ -76,6 +77,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
theme: "one-dark",
|
||||
executeMode: "all",
|
||||
wordWrap: false,
|
||||
appLayout: "classic",
|
||||
};
|
||||
|
||||
export const STORAGE_KEY = "dbx-editor-settings";
|
||||
@@ -93,6 +95,7 @@ function loadEditorSettings(): EditorSettings {
|
||||
theme: parsed.theme ?? DEFAULT_EDITOR_SETTINGS.theme,
|
||||
executeMode: parsed.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
|
||||
wordWrap: parsed.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
|
||||
appLayout: parsed.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -168,6 +168,24 @@
|
||||
.dark .app-panel-gutter {
|
||||
background: oklch(0.18 0 0);
|
||||
}
|
||||
.app-layout-classic .panel-resize-handle {
|
||||
width: 3px;
|
||||
}
|
||||
.app-layout-classic .panel-resize-handle--right {
|
||||
right: 0;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.app-layout-classic .panel-resize-handle--left {
|
||||
left: 0;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
.app-layout-classic .panel-resize-handle::before {
|
||||
display: none;
|
||||
}
|
||||
.app-layout-classic .panel-resize-handle:hover,
|
||||
.app-layout-classic .panel-resize-handle:active {
|
||||
background: oklch(0.6 0.15 250);
|
||||
}
|
||||
|
||||
.panel-resize-handle {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user