feat(export): move csv and xlsx generation to Rust backend

This commit is contained in:
t8y2
2026-05-22 14:56:40 +08:00
parent 0655d309d8
commit c736fe372a
14 changed files with 591 additions and 53 deletions
@@ -92,7 +92,7 @@ import {
treeNodeRowAction,
treeNodeRowDoubleClickAction,
} from "@/lib/treeNodeClick";
import { formatCsv, formatJson, formatSqlInsert } from "@/lib/exportFormats";
import { formatJson, formatSqlInsert } from "@/lib/exportFormats";
import { fetchTableDataForExport } from "@/lib/tableDataExport";
import {
buildCreateDatabaseSql,
@@ -1266,13 +1266,26 @@ async function exportData(format: "csv" | "json" | "sql") {
executePage: (sql) => api.executeQuery(connectionId, database, sql),
});
if (format === "csv") {
let outputPath = `${node.label}.csv`;
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: outputPath,
filters: [{ name: "CSV", extensions: ["csv"] }],
});
if (!path) return;
outputPath = path as string;
}
await api.exportQueryResultCsv(outputPath, result.columns, result.rows);
toast(t("grid.exported"));
return;
}
let content: string;
let ext: string;
if (format === "csv") {
ext = "csv";
content = formatCsv(result.columns, result.rows);
} else if (format === "json") {
if (format === "json") {
ext = "json";
content = formatJson(result.columns, result.rows);
} else {
@@ -1311,13 +1324,17 @@ async function exportDataXlsx() {
executePage: (sql) => api.executeQuery(connectionId, database, sql),
});
const { buildXlsxWorkbook } = await import("@/lib/xlsxExport");
const workbook = buildXlsxWorkbook({
sheetName: node.label,
columns: result.columns,
rows: result.rows,
});
await saveBinaryFileContent(workbook, `${node.label}.xlsx`, "Excel", "xlsx");
let outputPath = `${node.label}.xlsx`;
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: outputPath,
filters: [{ name: "Excel", extensions: ["xlsx"] }],
});
if (!path) return;
outputPath = path as string;
}
await api.exportQueryResultXlsx(outputPath, node.label, result.columns, result.rows);
toast(t("grid.exported"));
} catch (e: any) {
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
@@ -1,7 +1,8 @@
import { computed, type ComputedRef, type Ref } from "vue";
import { useI18n } from "vue-i18n";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { formatCsv, formatJson } from "@/lib/exportFormats";
import { formatJson } from "@/lib/exportFormats";
import * as api from "@/lib/api";
import {
formatSelectionAsCsv,
formatSelectionAsJson,
@@ -266,36 +267,6 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
}
}
async function saveBinaryFileContent(
content: Uint8Array,
defaultFileName: string,
filterName: string,
filterExt: string,
): Promise<boolean> {
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) return false;
await writeFile(path, content);
return true;
} 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);
return true;
}
}
// --- Export functions ---
async function runExclusiveExport(action: () => Promise<void>) {
const finish = tryStartExclusiveActivation(exportGuard);
@@ -311,9 +282,18 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
await runExclusiveExport(async () => {
try {
const rows = rowsToExport(rowIds).map((item) => item.data.map((c) => displayCellValue(c)));
if (await saveFileContent(formatCsv(columns.value, rows), "export.csv", "CSV", "csv")) {
toast(t("grid.exported"));
let outputPath = "export.csv";
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: outputPath,
filters: [{ name: "CSV", extensions: ["csv"] }],
});
if (!path) return;
outputPath = path as string;
}
await api.exportQueryResultCsv(outputPath, columns.value, rows);
toast(t("grid.exported"));
} catch (e: any) {
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
}
@@ -352,15 +332,23 @@ export function useDataGridExport(options: UseDataGridExportOptions) {
async function exportXlsx(rowIds?: number[]) {
await runExclusiveExport(async () => {
try {
const { buildXlsxWorkbook } = await import("@/lib/xlsxExport");
const workbook = buildXlsxWorkbook({
sheetName: tableMeta.value?.tableName || "Export",
columns: columns.value,
rows: rowsToExport(rowIds).map((item) => item.data),
});
if (await saveBinaryFileContent(workbook, "export.xlsx", "Excel", "xlsx")) {
toast(t("grid.exported"));
let outputPath = "export.xlsx";
if (isTauriRuntime()) {
const { save } = await import("@tauri-apps/plugin-dialog");
const path = await save({
defaultPath: outputPath,
filters: [{ name: "Excel", extensions: ["xlsx"] }],
});
if (!path) return;
outputPath = path as string;
}
await api.exportQueryResultXlsx(
outputPath,
tableMeta.value?.tableName || "Export",
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
@@ -128,6 +128,8 @@ export const cancelTableImport = forward("cancelTableImport");
// Database Export
export const exportDatabaseSql = forward("exportDatabaseSql");
export const cancelDatabaseExport = forward("cancelDatabaseExport");
export const exportQueryResultCsv = forward("exportQueryResultCsv");
export const exportQueryResultXlsx = forward("exportQueryResultXlsx");
// Redis
export const redisListDatabases = forward("redisListDatabases");
+42
View File
@@ -47,6 +47,7 @@ import type {
TableImportProgress,
DatabaseExportRequest,
ExportProgress,
XlsxCellValue,
} from "./tauri";
// ---------------------------------------------------------------------------
@@ -680,6 +681,47 @@ export async function cancelDatabaseExport(exportId: string): Promise<void> {
await post("/api/export/database/cancel", { exportId });
}
export async function exportQueryResultCsv(
filePath: string,
columns: string[],
rows: readonly (readonly XlsxCellValue[])[],
): Promise<void> {
const { formatCsv } = await import("./exportFormats");
const content = formatCsv(columns, rows as (string | number | boolean | null)[][]);
const fileName = filePath.split(/[\\/]/).pop() || "export.csv";
const blob = new Blob(["\uFEFF", content], { type: "text/csv;charset=utf-8" });
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,
columns: string[],
rows: readonly (readonly XlsxCellValue[])[],
): Promise<void> {
const { buildXlsxWorkbook } = await import("./xlsxExport");
const workbook = buildXlsxWorkbook({
sheetName: sheetName || "Export",
columns,
rows,
});
const fileName = filePath.split(/[\\/]/).pop() || "export.xlsx";
const blob = new Blob([workbook], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
}
// ---------------------------------------------------------------------------
// Redis
// ---------------------------------------------------------------------------
+32
View File
@@ -45,6 +45,8 @@ export interface DesktopSettings {
show_tray_icon: boolean;
}
export type XlsxCellValue = string | number | boolean | null;
export interface DriverInstallProgress {
step: string;
downloaded?: number;
@@ -974,3 +976,33 @@ export async function exportDatabaseSql(
export async function cancelDatabaseExport(exportId: string): Promise<void> {
await invoke("cancel_database_export", { exportId });
}
export async function exportQueryResultCsv(
filePath: string,
columns: string[],
rows: readonly (readonly XlsxCellValue[])[],
): Promise<void> {
return invoke("export_query_result_csv", {
request: {
filePath,
columns,
rows,
},
});
}
export async function exportQueryResultXlsx(
filePath: string,
sheetName: string | undefined,
columns: string[],
rows: readonly (readonly XlsxCellValue[])[],
): Promise<void> {
return invoke("export_query_result_xlsx", {
request: {
filePath,
sheetName,
columns,
rows,
},
});
}