feat(export): move text exports to Rust

This commit is contained in:
t8y2
2026-05-22 15:41:33 +08:00
parent 209eff528d
commit c91fbc9fd6
17 changed files with 367 additions and 142 deletions
@@ -92,7 +92,7 @@ import {
treeNodeRowAction,
treeNodeRowDoubleClickAction,
} from "@/lib/treeNodeClick";
import { formatJson, formatSqlInsert } from "@/lib/exportFormats";
import { formatSqlInsert } from "@/lib/exportFormats";
import { fetchTableDataForExport } from "@/lib/tableDataExport";
import {
buildCreateDatabaseSql,
@@ -1199,33 +1199,6 @@ async function saveFileContent(content: string, defaultFileName: string, filterN
}
}
async function saveBinaryFileContent(
content: Uint8Array,
defaultFileName: string,
filterName: string,
filterExt: string,
) {
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const { writeFile } = await import("@tauri-apps/plugin-fs");
const path = await save({
defaultPath: defaultFileName,
filters: [{ name: filterName, extensions: [filterExt] }],
});
if (path) await writeFile(path, content);
} else {
const blob = new Blob([content], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = defaultFileName;
a.click();
URL.revokeObjectURL(url);
}
}
async function exportStructure() {
const node = props.node;
if (!node.connectionId || !node.database) return;
@@ -1282,18 +1255,24 @@ async function exportData(format: "csv" | "json" | "sql") {
return;
}
let content: string;
let ext: string;
if (format === "json") {
ext = "json";
content = formatJson(result.columns, result.rows);
} else {
ext = "sql";
content = formatSqlInsert(qualifiedName, result.columns, result.rows, quoteIdent);
let outputPath = `${node.label}.json`;
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: outputPath,
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (!path) return;
outputPath = path as string;
}
await api.exportQueryResultJson(outputPath, result.columns, result.rows);
toast(t("grid.exported"));
return;
}
await saveFileContent(content, `${node.label}.${ext}`, ext.toUpperCase(), ext);
const content = formatSqlInsert(qualifiedName, result.columns, result.rows, quoteIdent);
await saveFileContent(content, `${node.label}.sql`, "SQL", "sql");
toast(t("grid.exported"));
} catch (e: any) {
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
@@ -1,7 +1,6 @@
import { computed, type ComputedRef, type Ref } from "vue";
import { useI18n } from "vue-i18n";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { formatJson } from "@/lib/exportFormats";
import * as api from "@/lib/api";
import {
formatSelectionAsCsv,
@@ -238,35 +237,6 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
await copyText(`${header}\n${body}`);
}
// --- File save helpers ---
async function saveFileContent(
content: string,
defaultFileName: string,
filterName: string,
filterExt: string,
): Promise<boolean> {
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
const path = await save({
defaultPath: defaultFileName,
filters: [{ name: filterName, extensions: [filterExt] }],
});
if (!path) return false;
await writeTextFile(path, "" + content);
return true;
} else {
const blob = new Blob(["", content], { type: "text/csv;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = defaultFileName;
a.click();
URL.revokeObjectURL(url);
return true;
}
}
// --- Export functions ---
async function runExclusiveExport(action: () => Promise<void>) {
const finish = tryStartExclusiveActivation(exportGuard);
@@ -303,10 +273,22 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
async function exportJson(rowIds?: number[]) {
await runExclusiveExport(async () => {
try {
const rows = rowsToExport(rowIds).map((item) => item.data);
if (await saveFileContent(formatJson(columns.value, rows), "export.json", "JSON", "json")) {
toast(t("grid.exported"));
let outputPath = "export.json";
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: outputPath,
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (!path) return;
outputPath = path as string;
}
await api.exportQueryResultJson(
outputPath,
columns.value,
rowsToExport(rowIds).map((item) => item.data),
);
toast(t("grid.exported"));
} catch (e: any) {
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
}
@@ -316,13 +298,22 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
async function exportMarkdown(rowIds?: number[]) {
await runExclusiveExport(async () => {
try {
const cols = columns.value;
const visibleRows = rowsToExport(rowIds).map((item) => item.data);
const { formatMarkdownTable } = await import("@/lib/markdownTable");
const md = formatMarkdownTable({ columns: cols, rows: visibleRows });
if (await saveFileContent(md, "export.md", "Markdown", "md")) {
toast(t("grid.exported"));
let outputPath = "export.md";
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: outputPath,
filters: [{ name: "Markdown", extensions: ["md"] }],
});
if (!path) return;
outputPath = path as string;
}
await api.exportQueryResultMarkdown(
outputPath,
columns.value,
rowsToExport(rowIds).map((item) => item.data),
);
toast(t("grid.exported"));
} catch (e: any) {
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
}
+2
View File
@@ -132,6 +132,8 @@ export const exportDatabaseSql = forward("exportDatabaseSql");
export const cancelDatabaseExport = forward("cancelDatabaseExport");
export const exportQueryResultCsv = forward("exportQueryResultCsv");
export const exportQueryResultXlsx = forward("exportQueryResultXlsx");
export const exportQueryResultJson = forward("exportQueryResultJson");
export const exportQueryResultMarkdown = forward("exportQueryResultMarkdown");
// Redis
export const redisListDatabases = forward("redisListDatabases");
-11
View File
@@ -7,17 +7,6 @@ export function formatCsv(columns: string[], rows: ExportCellValue[][]): string
return `${header}\n${body}`;
}
export function formatJson(columns: string[], rows: ExportCellValue[][]): string {
const data = rows.map((row) => {
const obj: Record<string, unknown> = {};
columns.forEach((col, i) => {
obj[col] = row[i];
});
return obj;
});
return JSON.stringify(data, null, 2);
}
export function formatSqlInsert(
qualifiedName: string,
columns: string[],
+29
View File
@@ -709,6 +709,17 @@ export async function exportQueryResultCsv(
URL.revokeObjectURL(url);
}
function downloadTextFile(filePath: string, fallbackFileName: string, content: string, mimeType: string): void {
const fileName = filePath.split(/[\\/]/).pop() || fallbackFileName;
const blob = new Blob(["\uFEFF", content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
}
export async function exportQueryResultXlsx(
filePath: string,
sheetName: string | undefined,
@@ -733,6 +744,24 @@ export async function exportQueryResultXlsx(
URL.revokeObjectURL(url);
}
export async function exportQueryResultJson(
filePath: string,
columns: string[],
rows: readonly (readonly XlsxCellValue[])[],
): Promise<void> {
const result = await post<{ content: string }>("/api/export/query-result-json", { columns, rows });
downloadTextFile(filePath, "export.json", result.content, "application/json;charset=utf-8");
}
export async function exportQueryResultMarkdown(
filePath: string,
columns: string[],
rows: readonly (readonly XlsxCellValue[])[],
): Promise<void> {
const result = await post<{ content: string }>("/api/export/query-result-markdown", { columns, rows });
downloadTextFile(filePath, "export.md", result.content, "text/markdown;charset=utf-8");
}
// ---------------------------------------------------------------------------
// Redis
// ---------------------------------------------------------------------------
-34
View File
@@ -1,34 +0,0 @@
import type { GridCellValue } from "./dataGridSql.ts";
export interface MarkdownTableData {
columns: string[];
rows: GridCellValue[][];
}
export function formatMarkdownTable(data: MarkdownTableData): string {
const normalizedColumns = data.columns.map(markdownCell);
const normalizedRows = data.rows.map((row) => row.map((cell) => markdownCell(displayCell(cell))));
const widths = normalizedColumns.map((column, index) =>
Math.max(column.length, ...normalizedRows.map((row) => row[index]?.length ?? 0), 3),
);
const header = `| ${normalizedColumns.map((column, index) => pad(column, widths[index])).join(" | ")} |`;
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
const body = normalizedRows
.map((row) => `| ${row.map((cell, index) => pad(cell, widths[index])).join(" | ")} |`)
.join("\n");
return `${[header, separator, body].filter(Boolean).join("\n")}\n`;
}
function displayCell(value: GridCellValue): string {
if (value === null) return "NULL";
if (typeof value === "boolean") return value ? "true" : "false";
return String(value);
}
function markdownCell(value: string): string {
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
}
function pad(value: string, width: number): string {
return value.padEnd(width);
}
+28
View File
@@ -1023,3 +1023,31 @@ export async function exportQueryResultXlsx(
},
});
}
export async function exportQueryResultJson(
filePath: string,
columns: string[],
rows: readonly (readonly XlsxCellValue[])[],
): Promise<void> {
return invoke("export_query_result_json", {
request: {
filePath,
columns,
rows,
},
});
}
export async function exportQueryResultMarkdown(
filePath: string,
columns: string[],
rows: readonly (readonly XlsxCellValue[])[],
): Promise<void> {
return invoke("export_query_result_markdown", {
request: {
filePath,
columns,
rows,
},
});
}