From cac383e6bfbcd0478c0321ecd6237c57eab2ea73 Mon Sep 17 00:00:00 2001
From: t8y2 <1156263951@qq.com>
Date: Sun, 10 May 2026 00:33:25 +0800
Subject: [PATCH] feat(sql): add saved SQL library
---
src/App.vue | 129 +++++++++++++---
src/components/layout/ContentArea.vue | 7 +-
src/components/layout/EditorToolbar.vue | 21 ++-
src/components/sidebar/TreeItem.vue | 197 +++++++++++++++++++++++-
src/i18n/locales/en.ts | 24 ++-
src/i18n/locales/zh-CN.ts | 24 ++-
src/lib/openTabsPersistence.ts | 2 +
src/stores/connectionStore.ts | 126 ++++++++++++---
src/stores/queryStore.ts | 36 +++++
src/stores/savedSqlStore.ts | 176 +++++++++++++++++++++
src/types/database.ts | 6 +
11 files changed, 694 insertions(+), 54 deletions(-)
create mode 100644 src/stores/savedSqlStore.ts
diff --git a/src/App.vue b/src/App.vue
index 4caef8ea4d..e35a5c6046 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -16,6 +16,7 @@ import LoginPage from "@/components/auth/LoginPage.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
+import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { useToast } from "@/composables/useToast";
import { useTheme } from "@/composables/useTheme";
import { useAppUpdater } from "@/composables/useAppUpdater";
@@ -35,11 +36,16 @@ import { isTauriRuntime } from "@/lib/tauriRuntime";
import { isCloseTabShortcut, isExecuteSqlShortcut } from "@/lib/keyboardShortcuts";
import { isPreviewTab } from "@/lib/tabPresentation";
import { SQL_FILE_UNSUPPORTED_TYPES } from "@/lib/databaseCapabilities";
+import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
const { t } = useI18n();
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
+const savedSqlStore = useSavedSqlStore();
const { message: toastMessage, visible: toastVisible, toast } = useToast();
const { isDark, applyTheme, toggleTheme } = useTheme();
const {
@@ -75,6 +81,10 @@ const selectedSql = ref("");
const cursorPos = ref(0);
const formatSqlRequestId = ref(0);
const activeOutputView = ref<"result" | "explain" | "chart">("result");
+const showSaveSqlDialog = ref(false);
+const saveSqlName = ref("");
+const saveSqlFolderId = ref("");
+const ROOT_SAVED_SQL_FOLDER = "__root__";
const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore.activeTabId));
@@ -114,6 +124,10 @@ const connectionStats = computed(() => ({
types: new Set(connectionStore.connections.map((c) => c.driver_profile || c.db_type)).size,
}));
const recentConnections = computed(() => connectionStore.connections.slice(0, 5));
+const saveSqlFolders = computed(() => {
+ const tab = activeTab.value;
+ return tab ? savedSqlStore.listFolders(tab.connectionId) : [];
+});
watch(
() => queryStore.activeTabId,
@@ -142,32 +156,56 @@ function formatActiveSql() {
formatSqlRequestId.value++;
}
-async function saveSqlToFile() {
+function defaultSavedSqlName(title: string) {
+ const trimmed = title.trim() || "Query";
+ return trimmed.endsWith(".sql") ? trimmed : `${trimmed}.sql`;
+}
+
+function openSaveSqlDialog() {
const tab = activeTab.value;
if (!tab || !tab.sql.trim()) return;
+ const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined;
+ if (existing) {
+ const updated = 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);
+ connectionStore.refreshSavedSqlTree(tab.connectionId);
+ toast(t("savedSql.saved"), 2000);
+ return;
+ }
+
+ saveSqlName.value = defaultSavedSqlName(tab.title);
+ saveSqlFolderId.value = ROOT_SAVED_SQL_FOLDER;
+ showSaveSqlDialog.value = true;
+}
+
+function confirmSaveSqlToLibrary() {
+ const tab = activeTab.value;
+ const name = saveSqlName.value.trim();
+ if (!tab || !tab.sql.trim() || !name) 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);
- }
+ const saved = savedSqlStore.saveFile({
+ id: tab.savedSqlId,
+ connectionId: tab.connectionId,
+ folderId: saveSqlFolderId.value === ROOT_SAVED_SQL_FOLDER ? undefined : saveSqlFolderId.value,
+ name: defaultSavedSqlName(name),
+ database: tab.database,
+ schema: tab.schema,
+ sql: tab.sql,
+ });
+ queryStore.linkSavedSql(tab.id, saved.id, saved.name);
+ connectionStore.refreshSavedSqlTree(tab.connectionId);
+ showSaveSqlDialog.value = false;
+ toast(t("savedSql.saved"), 2000);
} catch (e: any) {
- toast(t("toolbar.sqlSaveFailed", { message: e?.message || String(e) }), 5000);
+ toast(t("savedSql.saveFailed", { message: e?.message || String(e) }), 5000);
}
}
@@ -330,6 +368,19 @@ function handleKeydown(e: KeyboardEvent) {
if (queryStore.activeTabId) queryStore.closeTab(queryStore.activeTabId);
return;
}
+ if (
+ activeTab.value?.mode === "query" &&
+ !showSaveSqlDialog.value &&
+ !e.isComposing &&
+ !e.altKey &&
+ (e.metaKey || e.ctrlKey) &&
+ e.key.toLowerCase() === "s"
+ ) {
+ e.preventDefault();
+ e.stopPropagation();
+ openSaveSqlDialog();
+ return;
+ }
if (
activeTab.value?.mode === "query" &&
isExecuteSqlShortcut(e) &&
@@ -480,7 +531,7 @@ onUnmounted(() => {
@cancel="cancelActiveExecution()"
@explain="tryExplain()"
@format-sql="formatActiveSql"
- @save-sql="saveSqlToFile"
+ @save-sql="openSaveSqlDialog"
@open-sql="openSqlFile"
@change-connection="changeActiveConnection"
@change-database="changeActiveDatabase"
@@ -608,6 +659,38 @@ onUnmounted(() => {
+
+
diff --git a/src/components/layout/ContentArea.vue b/src/components/layout/ContentArea.vue
index 1bcee57967..321d30f3b7 100644
--- a/src/components/layout/ContentArea.vue
+++ b/src/components/layout/ContentArea.vue
@@ -77,6 +77,8 @@ const editorDialect = computed<"mysql" | "postgres" | "sqlserver">(() => {
return "mysql";
});
+const shortcutModifier = computed(() => (navigator.platform.toLowerCase().includes("mac") ? "Cmd" : "Ctrl"));
+
const hasNumericData = computed(() => {
const r = props.activeTab.result;
if (!r || r.rows.length === 0) return false;
@@ -313,9 +315,10 @@ function onHandleCloseColumnPanel() {
- {{ t("editor.pressToExecute") }}
+
{{ t("editor.pressToExecute", { mod: shortcutModifier }) }}
+
{{ t("editor.pressToSaveSql", { mod: shortcutModifier }) }}
diff --git a/src/components/layout/EditorToolbar.vue b/src/components/layout/EditorToolbar.vue
index 24711e8636..b0af6cc246 100644
--- a/src/components/layout/EditorToolbar.vue
+++ b/src/components/layout/EditorToolbar.vue
@@ -91,6 +91,11 @@ function databaseDisplayName(database: string): string {
:variant="activeTab.isExecuting ? 'destructive' : 'ghost'"
size="icon"
class="h-6 w-6"
+ :class="
+ activeTab.isExecuting
+ ? ''
+ : 'bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/20 hover:text-emerald-800 dark:text-emerald-300 dark:hover:text-emerald-200'
+ "
:disabled="
activeTab.isCancelling || activeTab.isExplaining || (!activeTab.isExecuting && !executableSql.trim())
"
@@ -111,6 +116,11 @@ function databaseDisplayName(database: string): string {
:variant="activeTab.isExplaining ? 'destructive' : 'ghost'"
size="icon"
class="h-6 w-6"
+ :class="
+ activeTab.isExplaining
+ ? ''
+ : 'text-violet-600 hover:bg-violet-500/10 hover:text-violet-700 dark:text-violet-300 dark:hover:text-violet-200'
+ "
:disabled="activeTab.isExecuting || (!activeTab.isExplaining && !executableSql.trim())"
@click="activeTab.isExplaining ? emit('cancel') : emit('explain')"
>
@@ -127,7 +137,7 @@ function databaseDisplayName(database: string): string {