feat: configurable toolbar visibility with settings UI

This commit is contained in:
t8y2 committed 2026-06-11 13:32:39 +08:00
1 parent 4a264d638c
commit 21fd98d0ee
9 files changed
+214 -58

No files matched your search

@@ -14,7 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip";
import {
useSettingsStore,
AI_PROVIDER_PRESETS,
@@ -104,6 +104,7 @@ const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarH
const editSidebarHideTableComments = ref(settingsStore.editorSettings.sidebarHideTableComments);
const editSidebarAllowHorizontalScroll = ref(settingsStore.editorSettings.sidebarAllowHorizontalScroll);
const editExportBatchSize = ref(settingsStore.editorSettings.exportBatchSize);
const editToolbarItems = ref({ ...settingsStore.editorSettings.toolbarItems });
const redisScanPageSizeOptions = [200, 1000, 5000, 10000];
const systemFonts = ref<string[]>([]);
const systemFontsLoading = ref(false);
@@ -263,6 +264,7 @@ watch(
editSidebarHideTableComments.value = settingsStore.editorSettings.sidebarHideTableComments;
editSidebarAllowHorizontalScroll.value = settingsStore.editorSettings.sidebarAllowHorizontalScroll;
editExportBatchSize.value = settingsStore.editorSettings.exportBatchSize;
editToolbarItems.value = { ...settingsStore.editorSettings.toolbarItems };
editSnippets.value = settingsStore.editorSettings.snippets.map((s) => ({ ...s }));
void loadSystemFontOptions();
}
@@ -314,6 +316,7 @@ function hasChanges(): boolean {
editSidebarHideTableComments.value !== settingsStore.editorSettings.sidebarHideTableComments ||
editSidebarAllowHorizontalScroll.value !== settingsStore.editorSettings.sidebarAllowHorizontalScroll ||
editExportBatchSize.value !== settingsStore.editorSettings.exportBatchSize ||
JSON.stringify(editToolbarItems.value) !== JSON.stringify(settingsStore.editorSettings.toolbarItems) ||
JSON.stringify(normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value)) !== JSON.stringify(settingsStore.editorSettings.sidebarHiddenTablePrefixes) ||
JSON.stringify(editSnippets.value) !== JSON.stringify(settingsStore.editorSettings.snippets)
);
@@ -349,6 +352,7 @@ async function persistSettings() {
sidebarAllowHorizontalScroll: editSidebarAllowHorizontalScroll.value,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
exportBatchSize: editExportBatchSize.value,
toolbarItems: { ...editToolbarItems.value },
snippets: editSnippets.value,
});
await settingsStore.updateDesktopSettings({
@@ -401,6 +405,7 @@ function resetDefaults() {
editSidebarAllowHorizontalScroll.value = DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll;
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
editExportBatchSize.value = DEFAULT_EDITOR_SETTINGS.exportBatchSize;
editToolbarItems.value = { ...DEFAULT_EDITOR_SETTINGS.toolbarItems };
editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s }));
}
@@ -1602,6 +1607,46 @@ watch(
<Switch id="compact-column-header-actions" v-model="editCompactColumnHeaderActions" />
</div>
</div>
<Separator />
<div class="space-y-2">
<div class="flex items-center gap-2">
<Label>{{ t("settings.toolbarTitle") }}</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<CircleHelp class="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent class="max-w-64">
<p>{{ t("settings.toolbarHiddenHint") }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div class="grid grid-cols-3 gap-2 mt-2">
<div
v-for="item in [
{ key: 'dataTransfer', label: t('transfer.dataTransfer') },
{ key: 'driverManager', label: t('toolbar.driverManager') },
{ key: 'sqlFile', label: t('sqlFile.title') },
{ key: 'schemaDiff', label: t('diff.title') },
{ key: 'dataCompare', label: t('dataCompare.title') },
{ key: 'checkUpdates', label: t('updates.check') },
{ key: 'sqlLibrary', label: t('sqlLibrary.title') },
{ key: 'history', label: t('history.title') },
{ key: 'ai', label: 'AI' },
{ key: 'theme', label: t('toolbar.theme') },
{ key: 'github', label: 'GitHub' },
]"
:key="item.key"
class="flex items-center gap-2"
>
<Switch :id="`toolbar-${item.key}`" :model-value="(editToolbarItems as any)[item.key]" @update:model-value="(v: boolean) => ((editToolbarItems as any)[item.key] = v)" />
<Label :for="`toolbar-${item.key}`" class="text-sm cursor-pointer">{{ item.label }}</Label>
</div>
</div>
</div>
</section>
<section v-else-if="activeSettingsTab === 'navigation'" class="flex flex-col gap-5 py-2">
+106 -57
View File
@@ -8,6 +8,7 @@ import LightDropdown from "@/components/ui/LightDropdown.vue";
import WindowControls from "@/components/layout/WindowControls.vue";
import ExportProgressPopover from "@/components/export/ExportProgressPopover.vue";
import { shouldReserveMacTrafficLightInset, useWindowControls } from "@/composables/useWindowControls";
import { useSettingsStore } from "@/stores/settingsStore";
import type { AppThemeMode } from "@/lib/appTheme";
const props = defineProps<{
@@ -42,6 +43,8 @@ const emit = defineEmits<{
}>();
const { t } = useI18n();
const settingsStore = useSettingsStore();
const toolbarItems = computed(() => settingsStore.editorSettings.toolbarItems);
const { isMac, isDesktop, showControls, isMaximized, isFullscreen, minimize, toggleMaximize, close } = useWindowControls();
const themeItems = computed(() => [
@@ -82,47 +85,89 @@ onBeforeUnmount(() => {
resizeObserver?.disconnect();
});
const moreItems = computed(() => [
{
value: "sql-file",
label: t("sqlFile.title"),
icon: FileCode,
action: () => emit("open-sql-file"),
disabled: !props.hasSqlFileConnections,
},
{
value: "schema-diff",
label: t("diff.title"),
icon: GitCompareArrows,
action: () => emit("open-schema-diff"),
disabled: !props.hasConnections,
},
{
value: "data-compare",
label: t("dataCompare.title"),
icon: TableProperties,
action: () => emit("open-data-compare"),
disabled: !props.hasConnections,
},
]);
const moreItems = computed(() => {
const items: Array<{ value: string; label: string; icon: any; action: () => void; disabled: boolean }> = [];
const collapsedItems = computed(() => [
{
value: "transfer",
label: t("transfer.dataTransfer"),
icon: ArrowLeftRight,
action: () => emit("open-transfer"),
disabled: !props.hasConnections,
},
...moreItems.value,
{
value: "driver-store",
label: props.agentDriverUpdateCount > 0 ? `${t("toolbar.driverManager")} (${props.agentDriverUpdateCount})` : t("toolbar.driverManager"),
icon: Package,
action: () => emit("open-driver-store"),
disabled: false,
},
]);
// Hidden left-side items go into "更多"
if (!toolbarItems.value.dataTransfer) {
items.push({
value: "transfer",
label: t("transfer.dataTransfer"),
icon: ArrowLeftRight,
action: () => emit("open-transfer"),
disabled: !props.hasConnections,
});
}
if (!toolbarItems.value.driverManager) {
items.push({
value: "driver-store",
label: t("toolbar.driverManager"),
icon: Package,
action: () => emit("open-driver-store"),
disabled: false,
});
}
// "更多" menu items (individually toggleable)
if (toolbarItems.value.sqlFile) {
items.push({
value: "sql-file",
label: t("sqlFile.title"),
icon: FileCode,
action: () => emit("open-sql-file"),
disabled: !props.hasSqlFileConnections,
});
}
if (toolbarItems.value.schemaDiff) {
items.push({
value: "schema-diff",
label: t("diff.title"),
icon: GitCompareArrows,
action: () => emit("open-schema-diff"),
disabled: !props.hasConnections,
});
}
if (toolbarItems.value.dataCompare) {
items.push({
value: "data-compare",
label: t("dataCompare.title"),
icon: TableProperties,
action: () => emit("open-data-compare"),
disabled: !props.hasConnections,
});
}
return items;
});
const showMoreDropdown = computed(() => moreItems.value.length > 0);
const collapsedItems = computed(() => {
const items: Array<{ value: string; label: string; icon: any; action: () => void; disabled: boolean }> = [];
if (toolbarItems.value.dataTransfer) {
items.push({
value: "transfer",
label: t("transfer.dataTransfer"),
icon: ArrowLeftRight,
action: () => emit("open-transfer"),
disabled: !props.hasConnections,
});
}
if (toolbarItems.value.driverManager) {
items.push({
value: "driver-store",
label: props.agentDriverUpdateCount > 0 ? `${t("toolbar.driverManager")} (${props.agentDriverUpdateCount})` : t("toolbar.driverManager"),
icon: Package,
action: () => emit("open-driver-store"),
disabled: false,
});
}
// Always include moreItems (may contain hidden left-side items)
if (moreItems.value.length > 0) {
items.push(...moreItems.value);
}
return items;
});
</script>
<template>
@@ -138,12 +183,12 @@ const collapsedItems = computed(() => [
</Button>
<template v-if="!toolbarCollapsed">
<Button variant="ghost" size="sm" class="h-8 px-2 text-xs gap-1" @click="emit('open-transfer')" :disabled="!hasConnections">
<Button v-if="toolbarItems.dataTransfer" variant="ghost" size="sm" class="h-8 px-2 text-xs gap-1" @click="emit('open-transfer')" :disabled="!hasConnections">
<ArrowLeftRight class="h-3.5 w-3.5" />
{{ t("transfer.dataTransfer") }}
</Button>
<Button variant="ghost" size="sm" class="h-8 px-2 text-xs gap-1" :class="{ 'bg-accent': showDriverStore }" @click="emit('open-driver-store')">
<Button v-if="toolbarItems.driverManager" variant="ghost" size="sm" class="h-8 px-2 text-xs gap-1" :class="{ 'bg-accent': showDriverStore }" @click="emit('open-driver-store')">
<Package class="h-3.5 w-3.5" />
{{ t("toolbar.driverManager") }}
<span v-if="agentDriverUpdateCount > 0" class="ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium leading-none text-white" :aria-label="t('toolbar.updatableDriverCount')">
@@ -152,6 +197,7 @@ const collapsedItems = computed(() => [
</Button>
<LightDropdown
v-if="showMoreDropdown"
model-value=""
:items="moreItems"
:aria-label="t('common.more')"
@@ -172,6 +218,7 @@ const collapsedItems = computed(() => [
<template v-if="toolbarCollapsed">
<LightDropdown
v-if="collapsedItems.length > 0"
model-value=""
:items="collapsedItems"
:aria-label="t('common.more')"
@@ -192,20 +239,22 @@ const collapsedItems = computed(() => [
<div class="flex-1" data-tauri-drag-region />
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="relative h-8 w-8" :disabled="checkingUpdates" @click="emit('check-updates')">
<Loader2 v-if="checkingUpdates" class="h-4 w-4 animate-spin" />
<CloudDownload v-else class="h-4 w-4" />
<span v-if="hasUpdateAvailable" class="absolute right-1.5 top-1.5 h-2 w-2 rounded-full bg-red-500 ring-2 ring-background" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("updates.check") }}</TooltipContent>
</Tooltip>
<template v-if="toolbarItems.checkUpdates">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="relative h-8 w-8" :disabled="checkingUpdates" @click="emit('check-updates')">
<Loader2 v-if="checkingUpdates" class="h-4 w-4 animate-spin" />
<CloudDownload v-else class="h-4 w-4" />
<span v-if="hasUpdateAvailable" class="absolute right-1.5 top-1.5 h-2 w-2 rounded-full bg-red-500 ring-2 ring-background" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("updates.check") }}</TooltipContent>
</Tooltip>
</template>
<ExportProgressPopover />
<Tooltip>
<Tooltip v-if="toolbarItems.sqlLibrary">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-8 w-8" :class="{ 'bg-accent': showSqlLibrary }" @click="emit('toggle-sql-library')">
<BookMarked class="h-4 w-4" />
@@ -214,7 +263,7 @@ const collapsedItems = computed(() => [
<TooltipContent>{{ t("sqlLibrary.title") }}</TooltipContent>
</Tooltip>
<Tooltip>
<Tooltip v-if="toolbarItems.history">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-8 w-8" :class="{ 'bg-accent': showHistory }" @click="emit('toggle-history')">
<History class="h-4 w-4" />
@@ -223,7 +272,7 @@ const collapsedItems = computed(() => [
<TooltipContent>{{ t("history.title") }}</TooltipContent>
</Tooltip>
<Tooltip>
<Tooltip v-if="toolbarItems.ai">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-8 w-8" :class="{ 'bg-accent': showAiPanel }" @click="emit('toggle-ai')">
<Bot class="h-4 w-4" />
@@ -232,7 +281,7 @@ const collapsedItems = computed(() => [
<TooltipContent>AI</TooltipContent>
</Tooltip>
<Tooltip>
<Tooltip v-if="toolbarItems.theme">
<TooltipTrigger as-child>
<span class="inline-flex">
<LightDropdown
@@ -254,7 +303,7 @@ const collapsedItems = computed(() => [
<TooltipContent>{{ t("toolbar.theme") }}</TooltipContent>
</Tooltip>
<Tooltip>
<Tooltip v-if="toolbarItems.github">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="emit('open-github')">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">