feat: add web deployment with axum server, Docker support, and browser UI

Introduce src-web (axum HTTP server) exposing all 43 database operations
as REST APIs with SSE streaming for AI, transfers, and SQL file execution.
Frontend adapts automatically between Tauri and HTTP backends via api.ts.
Includes password authentication, Docker packaging, and login page.
This commit is contained in:
t8y2
2026-05-05 03:16:50 +08:00
parent cf4f25fd52
commit 41bceffc0e
48 changed files with 3044 additions and 109 deletions
+44 -11
View File
@@ -17,7 +17,8 @@ import {
type DropPosition,
} from "@/lib/sidebarLayout";
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sqlCompletion";
import * as api from "@/lib/tauri";
import * as api from "@/lib/api";
import { isTauriRuntime } from "@/lib/tauriRuntime";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
@@ -620,23 +621,55 @@ export const useConnectionStore = defineStore("connection", () => {
}
async function exportConnectionsToFile(passphrase: string) {
const { save } = await import("@tauri-apps/plugin-dialog");
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
const { encryptConfig } = await import("@/lib/configCrypto");
const path = await save({ filters: [{ name: "JSON", extensions: ["json"] }], defaultPath: "dbx-connections.json" });
if (!path) return;
const exportData = { connections: connections.value, layout: sidebarLayout.value };
const json = JSON.stringify(exportData);
const payload = await encryptConfig(json, passphrase);
await writeTextFile(path, JSON.stringify(payload, null, 2));
const content = JSON.stringify(payload, null, 2);
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
const path = await save({ filters: [{ name: "JSON", extensions: ["json"] }], defaultPath: "dbx-connections.json" });
if (!path) return;
await writeTextFile(path, content);
} else {
const blob = new Blob([content], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "dbx-connections.json";
a.click();
URL.revokeObjectURL(url);
}
}
async function readImportFile(): Promise<{ content: string; encrypted: boolean } | null> {
const { open } = await import("@tauri-apps/plugin-dialog");
const { readTextFile } = await import("@tauri-apps/plugin-fs");
const path = await open({ filters: [{ name: "JSON", extensions: ["json"] }], multiple: false });
if (!path) return null;
const content = await readTextFile(path as string);
let content: string;
if (isTauriRuntime()) {
const { open } = await import("@tauri-apps/plugin-dialog");
const { readTextFile } = await import("@tauri-apps/plugin-fs");
const path = await open({ filters: [{ name: "JSON", extensions: ["json"] }], multiple: false });
if (!path) return null;
content = await readTextFile(path as string);
} else {
content = await new Promise<string>((resolve, reject) => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = () => {
const file = input.files?.[0];
if (!file) { reject(new Error("No file selected")); return; }
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsText(file);
};
input.click();
});
}
const { isEncryptedConfig } = await import("@/lib/configCrypto");
const parsed = JSON.parse(content);
return { content, encrypted: isEncryptedConfig(parsed) };
+2 -2
View File
@@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import * as api from "@/lib/tauri";
import type { HistoryEntry } from "@/lib/tauri";
import * as api from "@/lib/api";
import type { HistoryEntry } from "@/lib/api";
export const useHistoryStore = defineStore("history", () => {
const entries = ref<HistoryEntry[]>([]);
+1 -1
View File
@@ -5,7 +5,7 @@ import { orderPinnedFirst } from "@/lib/pinnedItems";
import { canCancelQueryExecution } from "@/lib/queryExecutionState";
import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
import { buildExplainSql, parseExplainResult } from "@/lib/explainPlan";
import * as api from "@/lib/tauri";
import * as api from "@/lib/api";
export const useQueryStore = defineStore("query", () => {
const tabs = ref<QueryTab[]>([]);
+1 -1
View File
@@ -1,6 +1,6 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import * as api from "@/lib/tauri";
import * as api from "@/lib/api";
export type AiProvider = "claude" | "openai" | "custom";
export type AiApiStyle = "completions" | "responses";