diff --git a/src/App.vue b/src/App.vue index 0e75a357e..91d1c7839 100644 --- a/src/App.vue +++ b/src/App.vue @@ -132,6 +132,66 @@ function formatActiveSql() { formatSqlRequestId.value++; } +async function saveSqlToFile() { + const tab = activeTab.value; + if (!tab || !tab.sql.trim()) return; + try { + if (isTauriRuntime()) { + const { save } = await import("@tauri-apps/plugin-dialog"); + const { writeTextFile } = await import("@tauri-apps/plugin-fs"); + const path = await save({ defaultPath: `${tab.title}.sql`, filters: [{ name: "SQL", extensions: ["sql"] }] }); + if (path) { + await writeTextFile(path, tab.sql); + toast(t("toolbar.sqlSaved"), 2000); + } + } else { + const blob = new Blob([tab.sql], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${tab.title}.sql`; + a.click(); + URL.revokeObjectURL(url); + } + } catch (e: any) { + toast(t("toolbar.sqlSaveFailed", { message: e?.message || String(e) }), 5000); + } +} + +async function openSqlFile() { + const tab = activeTab.value; + if (!tab) return; + try { + if (isTauriRuntime()) { + const { open } = await import("@tauri-apps/plugin-dialog"); + const { readTextFile } = await import("@tauri-apps/plugin-fs"); + const path = await open({ filters: [{ name: "SQL", extensions: ["sql"] }], multiple: false }); + if (path) { + const content = await readTextFile(path as string); + queryStore.updateSql(tab.id, content); + } + } else { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".sql"; + input.onchange = () => { + const file = input.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + if (typeof reader.result === "string") { + queryStore.updateSql(tab.id, reader.result); + } + }; + reader.readAsText(file); + }; + input.click(); + } + } catch (e: any) { + toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000); + } +} + function newQuery() { const connId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id; if (!connId) return; @@ -295,6 +355,7 @@ onUnmounted(() => { window.removeEventListener("keydown", handleKeydown, true); :active-tab="activeTab" :active-connection="activeConnection" :executable-sql="executableSql" @execute="tryExecute()" @cancel="cancelActiveExecution()" @explain="tryExplain()" @format-sql="formatActiveSql" + @save-sql="saveSqlToFile" @open-sql="openSqlFile" @change-connection="changeActiveConnection" @change-database="changeActiveDatabase" /> import { computed } from "vue"; import { useI18n } from "vue-i18n"; -import { Play, Loader2, Square, Database, Table2, AlignLeft, GitBranch } from "lucide-vue-next"; +import { Play, Loader2, Square, Database, Table2, AlignLeft, GitBranch, Save, FolderOpen } from "lucide-vue-next"; import { Button } from "@/components/ui/button"; import { Select, @@ -29,6 +29,8 @@ const emit = defineEmits<{ cancel: []; explain: []; formatSql: []; + saveSql: []; + openSql: []; changeConnection: [connectionId: string]; changeDatabase: [database: string]; }>(); @@ -94,6 +96,22 @@ function databaseDisplayName(database: string): string { {{ t('toolbar.formatSql') }} + + + + + {{ t('toolbar.saveSql') }} + + + + + + {{ t('toolbar.openSql') }} +
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 366377cec..f12fb503b 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -12,6 +12,11 @@ export default { stopExplain: "Stop explain", formatSql: "Format SQL", formatSqlFailed: "Failed to format SQL", + saveSql: "Save SQL to file", + openSql: "Open SQL file", + sqlSaved: "SQL saved", + sqlOpenFailed: "Failed to open file: {message}", + sqlSaveFailed: "Failed to save file: {message}", }, updates: { title: "Updates", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 9eed5c572..b631b7eb3 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -12,6 +12,11 @@ export default { stopExplain: "停止执行计划", formatSql: "格式化 SQL", formatSqlFailed: "SQL 格式化失败", + saveSql: "保存 SQL 到文件", + openSql: "打开 SQL 文件", + sqlSaved: "SQL 已保存", + sqlOpenFailed: "打开文件失败:{message}", + sqlSaveFailed: "保存文件失败:{message}", }, updates: { title: "更新", diff --git a/src/stores/queryStore.ts b/src/stores/queryStore.ts index 35d769e5f..a6a4cdfca 100644 --- a/src/stores/queryStore.ts +++ b/src/stores/queryStore.ts @@ -7,8 +7,6 @@ import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions"; import { buildExplainSql, parseExplainResult } from "@/lib/explainPlan"; import * as api from "@/lib/api"; -import { isTauriRuntime } from "@/lib/tauriRuntime"; - interface SavedTab { id: string; title: string; @@ -22,10 +20,8 @@ interface SavedTab { const STORAGE_KEY = "dbx-open-tabs"; const ACTIVE_TAB_KEY = "dbx-active-tab"; -const isDesktop = isTauriRuntime(); function saveTabs(tabs: QueryTab[], activeTabId: string | null) { - if (isDesktop) return; try { const saved: SavedTab[] = tabs.map(t => ({ id: t.id, title: t.title, connectionId: t.connectionId, @@ -38,7 +34,6 @@ function saveTabs(tabs: QueryTab[], activeTabId: string | null) { } function loadSavedTabs(): { tabs: QueryTab[]; activeTabId: string | null } { - if (isDesktop) return { tabs: [], activeTabId: null }; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return { tabs: [], activeTabId: null };