From 19b5b70fb7c76cde50e1e8f2b64df1efbdf79d8f Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 12 Jun 2026 01:09:41 +0800 Subject: [PATCH] feat: confirm before closing unsaved SQL query tabs --- apps/desktop/src/App.vue | 61 ++++++++++++++++++- .../src/components/layout/AppTabBar.vue | 42 ++++++++++++- apps/desktop/src/i18n/locales/en.ts | 3 + apps/desktop/src/i18n/locales/zh-CN.ts | 3 + apps/desktop/src/stores/queryStore.ts | 52 +++++++++++++++- apps/desktop/src/types/database.ts | 1 + 6 files changed, 157 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 33dc49896..dc3099066 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -121,6 +121,8 @@ const newQueryContextSource = ref<"tab" | "sidebar">("tab"); const showSaveSqlDialog = ref(false); const saveSqlName = ref(""); const saveSqlFolderId = ref(""); +const pendingSaveAndCloseTabId = ref(null); +const pendingPrevActiveTabId = ref(null); const ROOT_SAVED_SQL_FOLDER = "__root__"; const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore.activeTabId)); @@ -390,6 +392,36 @@ function defaultSavedSqlName(title: string) { return normalized.endsWith(".sql") ? normalized : `${normalized}.sql`; } +async function handleSaveTab(tabId: string) { + const tab = queryStore.tabs.find((t) => t.id === tabId); + if (!tab || !tab.sql.trim()) return; + const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined; + if (existing) { + const updated = await savedSqlStore.saveFile({ + id: existing.id, + connectionId: tab.connectionId, + folderId: existing.folderId, + name: existing.name, + database: tab.database, + schema: tab.schema, + sql: tab.sql, + }); + queryStore.linkSavedSql(tab.id, updated.id, updated.name); + queryStore.markTabClean(tab); + toast(t("savedSql.saved"), 2000); + queryStore.closeTab(tabId, { force: true }); + return; + } + // No existing saved SQL — open save dialog, then close after save + const prevActive = queryStore.activeTabId; + queryStore.activeTabId = tabId; + saveSqlName.value = defaultSavedSqlName(tab.title); + saveSqlFolderId.value = ROOT_SAVED_SQL_FOLDER; + pendingSaveAndCloseTabId.value = tabId; + pendingPrevActiveTabId.value = prevActive; + showSaveSqlDialog.value = true; +} + async function openSaveSqlDialog() { const tab = activeTab.value; if (!tab || !tab.sql.trim()) return; @@ -459,7 +491,15 @@ async function confirmSaveSqlToLibrary() { sql: tab.sql, }); queryStore.linkSavedSql(tab.id, saved.id, saved.name); + queryStore.markTabClean(tab); showSaveSqlDialog.value = false; + if (pendingSaveAndCloseTabId.value) { + const closeId = pendingSaveAndCloseTabId.value; + pendingSaveAndCloseTabId.value = null; + if (pendingPrevActiveTabId.value) queryStore.activeTabId = pendingPrevActiveTabId.value; + pendingPrevActiveTabId.value = null; + queryStore.closeTab(closeId, { force: true }); + } toast(t("savedSql.saved"), 2000); } catch (e: any) { toast(t("savedSql.saveFailed", { message: e?.message || String(e) }), 5000); @@ -1056,7 +1096,7 @@ onUnmounted(() => {
- +
{
- + {{ t("savedSql.saveToLibrary") }} @@ -1245,7 +1293,14 @@ onUnmounted(() => {
- + diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue index f0d69f69e..69e1ffcba 100644 --- a/apps/desktop/src/components/layout/AppTabBar.vue +++ b/apps/desktop/src/components/layout/AppTabBar.vue @@ -2,10 +2,12 @@ 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 } from "@lucide/vue"; +import { X, Pin, ChevronDown, Table2, Code2, TableProperties, PencilRuler, KeyRound, Pencil, Package, Check, Lock, Copy, AlertTriangle } 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"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useTabScroll } from "@/composables/useTabScroll"; @@ -22,6 +24,7 @@ const props = defineProps<{ const emit = defineEmits<{ "toggle-driver-store": []; "close-driver-store": []; + "save-tab": [tabId: string]; }>(); const { t } = useI18n(); @@ -113,6 +116,19 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] { ]; } +function handleSaveAndClose() { + const id = queryStore.saveAndClosePendingTab(); + if (id) emit("save-tab", id); +} + +function handleDiscardAndClose() { + queryStore.forceClosePendingTab(); +} + +function handleCancelClose() { + queryStore.cancelClosePendingTab(); +} + const tabsContainerRef = ref(null); const { hasTabOverflow, scrollThumbLeftPercent, scrollThumbWidthPercent, isScrollbarDragging, updateScrollButtons, onTabsWheel, startScrollbarDrag } = useTabScroll(tabsContainerRef); const tabScrollBehavior = ref("smooth"); @@ -387,6 +403,30 @@ function activateTab(tabId: string) { /> + + + + + + + {{ t("editor.unsavedChangesTitle") }} + + +

{{ t("editor.unsavedChangesMessage") }}

+ + + + + +
+