feat(sql): add save/open SQL file buttons and enable tab persistence on desktop

This commit is contained in:
t8y2 2026-05-05 15:18:54 +08:00
parent 2de0894a99
commit c295bf5780
5 changed files with 90 additions and 6 deletions

View File

@ -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"
/>
<ContentArea

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
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 {
</TooltipTrigger>
<TooltipContent>{{ t('toolbar.formatSql') }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6" :disabled="!activeTab.sql.trim()" @click="emit('saveSql')">
<Save class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t('toolbar.saveSql') }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6" @click="emit('openSql')">
<FolderOpen class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t('toolbar.openSql') }}</TooltipContent>
</Tooltip>
</div>
<span class="flex-1 min-w-0" />
<div class="flex items-center gap-2 shrink-0">

View File

@ -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",

View File

@ -12,6 +12,11 @@ export default {
stopExplain: "停止执行计划",
formatSql: "格式化 SQL",
formatSqlFailed: "SQL 格式化失败",
saveSql: "保存 SQL 到文件",
openSql: "打开 SQL 文件",
sqlSaved: "SQL 已保存",
sqlOpenFailed: "打开文件失败:{message}",
sqlSaveFailed: "保存文件失败:{message}",
},
updates: {
title: "更新",

View File

@ -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 };