fix(export): show progress while preparing database export

This commit is contained in:
Freedom authored and GitHub committed 2026-07-29 14:43:07 +08:00
1 parent 8841a41261
commit a4352d6560
13 files changed
+210 -68

No files matched your search

@@ -17,7 +17,7 @@ import { buildSelectedTablesPayload } from "@/lib/export/databaseExportSelection
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { useToast } from "@/composables/useToast";
import { Input } from "@/components/ui/input";
import { Download, Square, CheckSquare, Search, X } from "@lucide/vue";
import { Download, Square, CheckSquare, Search, X, Loader2 } from "@lucide/vue";
import { useExportTracker } from "@/composables/useExportTracker";
const { t } = useI18n();
@@ -230,11 +230,6 @@ async function startExport() {
await startAllDatabasesExport();
return;
}
isExporting.value = true;
exportDone.value = false;
exportError.value = null;
exportCancelled.value = false;
exportProgress.value = null;
exportId.value = generateDatabaseExportId();
@@ -248,13 +243,9 @@ async function startExport() {
defaultPath: `${safeName}.sql`,
filters: [{ name: "SQL", extensions: ["sql"] }],
});
if (!path) {
isExporting.value = false;
return;
}
if (!path) return;
filePath = path;
} catch (e: any) {
isExporting.value = false;
toast(e?.message || String(e), 5000);
return;
}
@@ -263,6 +254,24 @@ async function startExport() {
filePath = `__web_export_${exportId.value}.sql`;
}
// Switch to the progress view only after the save dialog closes, and seed a
// preparing state so the dialog is never a blank panel while metadata loads.
isExporting.value = true;
exportDone.value = false;
exportError.value = null;
exportCancelled.value = false;
exportProgress.value = {
exportId: exportId.value,
currentObject: "",
objectIndex: 0,
totalObjects: 0,
rowsExported: 0,
totalRows: null,
status: "Running",
error: null,
preparing: true,
};
const request: api.DatabaseExportRequest = {
exportId: exportId.value,
connectionId: connectionId.value,
@@ -339,19 +348,40 @@ async function startAllDatabasesExport() {
exportDone.value = false;
exportError.value = null;
exportCancelled.value = false;
exportProgress.value = null;
batchDatabaseIndex.value = 0;
batchRowsExported.value = 0;
const dbs = [...selectedDatabases.value];
const batchId = generateDatabaseExportId();
exportId.value = batchId;
exportProgress.value = {
exportId: batchId,
currentObject: "",
objectIndex: 0,
totalObjects: 0,
rowsExported: 0,
totalRows: null,
status: "Running",
error: null,
preparing: true,
};
addDatabaseExportTask(batchId, t("databaseExport.allDatabasesTask", { count: dbs.length }), directoryPath);
let exportPlan: AllDatabaseExportPlanItem[] = [];
try {
exportPlan = await buildExportPlanForDatabases(dbs);
batchDatabaseTotal.value = exportPlan.length;
exportProgress.value = {
exportId: batchId,
currentObject: "",
objectIndex: 0,
totalObjects: exportPlan.length,
rowsExported: 0,
totalRows: null,
status: "Running",
error: null,
preparing: true,
};
for (let index = 0; index < exportPlan.length; index += 1) {
if (exportCancelled.value) break;
@@ -490,14 +520,40 @@ const progressPercent = computed(() => {
if (exportAllDatabases.value && batchDatabaseTotal.value > 0) {
if (exportDone.value) return 100;
const current = exportProgress.value;
const currentDatabaseProgress = current && current.totalObjects > 0 ? current.objectIndex / current.totalObjects : 0;
const currentDatabaseProgress = current && !current.preparing && current.totalObjects > 0 ? current.objectIndex / current.totalObjects : 0;
return Math.round(Math.min(1, (Math.max(0, batchDatabaseIndex.value - 1) + currentDatabaseProgress) / batchDatabaseTotal.value) * 100);
}
const p = exportProgress.value;
if (!p || p.totalObjects === 0) return 0;
if (!p || p.preparing || p.totalObjects === 0) return 0;
return Math.round((p.objectIndex / p.totalObjects) * 100);
});
/** True while schema metadata is still loading — before objects are written. */
const isPreparingExport = computed(() => {
if (!isExporting.value) return false;
if (exportDone.value || exportError.value || exportCancelled.value) return false;
const p = exportProgress.value;
if (!p) return true;
return !!p.preparing || p.totalObjects <= 0;
});
const progressStatusText = computed(() => {
const p = exportProgress.value;
if (isPreparingExport.value) {
// Keep it as presence feedback only — no second progress counter that later resets.
if (p?.currentObject) {
return t("databaseExport.preparingObject", { object: p.currentObject });
}
return t("databaseExport.preparing");
}
if (!p) return t("databaseExport.exporting");
return t("databaseExport.currentTable", {
table: p.currentObject,
current: p.objectIndex,
total: p.totalObjects,
});
});
const skipConnectionWatch = ref(false);
watch(connectionId, (id) => {
@@ -732,22 +788,18 @@ watch(
<div v-if="exportAllDatabases && batchDatabaseTotal" class="text-xs text-muted-foreground">
{{ t("databaseExport.currentDatabase", { current: batchDatabaseIndex, total: batchDatabaseTotal }) }}
</div>
<div v-if="exportProgress" class="space-y-2">
<div v-if="!exportAllDatabases || !exportDone" class="text-xs text-muted-foreground">
{{
t("databaseExport.currentTable", {
table: exportProgress.currentObject,
current: exportProgress.objectIndex,
total: exportProgress.totalObjects,
})
}}
<div class="space-y-2">
<div v-if="!exportAllDatabases || !exportDone" class="flex items-center gap-2 text-xs text-muted-foreground">
<Loader2 v-if="isExporting && !exportDone && !exportError && !exportCancelled" class="h-3.5 w-3.5 shrink-0 animate-spin text-primary" />
<span>{{ progressStatusText }}</span>
</div>
<div class="w-full bg-muted rounded-full h-2 overflow-hidden">
<div class="h-full rounded-full transition-[width] duration-300" :class="exportError ? 'bg-destructive' : exportCancelled ? 'bg-yellow-500' : 'bg-primary'" :style="{ width: `${progressPercent}%` }" />
<div v-if="isPreparingExport" class="database-export-progress-indeterminate h-full rounded-full bg-primary" />
<div v-else class="h-full rounded-full transition-[width] duration-300" :class="exportError ? 'bg-destructive' : exportCancelled ? 'bg-yellow-500' : exportDone ? 'bg-green-500' : 'bg-primary'" :style="{ width: `${exportDone ? 100 : progressPercent}%` }" />
</div>
<div class="text-xs text-muted-foreground">
<div v-if="exportProgress && !isPreparingExport" class="text-xs text-muted-foreground">
{{ exportAllDatabases ? t("databaseExport.allRowsExported", { count: exportProgress.rowsExported.toLocaleString() }) : t("databaseExport.rowsExported", { current: exportProgress.objectIndex, total: exportProgress.totalObjects, count: exportProgress.rowsExported.toLocaleString() }) }}
</div>
</div>
@@ -793,3 +845,22 @@ watch(
</DialogContent>
</Dialog>
</template>
<style scoped>
.database-export-progress-indeterminate {
width: 42%;
animation: database-export-progress-slide 1.15s ease-in-out infinite;
}
@keyframes database-export-progress-slide {
0% {
transform: translateX(-110%);
}
50% {
transform: translateX(70%);
}
100% {
transform: translateX(250%);
}
}
</style>
@@ -381,7 +381,10 @@ export function useExportTracker() {
function updateDatabaseExportTask(exportId: string, progress: api.ExportProgress) {
const task = taskMap.get(exportId);
if (!task) return;
task.tableName = progress.currentObject || task.tableName;
// Keep the database label during metadata prefetch; only follow object names while writing.
if (!progress.preparing && progress.currentObject) {
task.tableName = progress.currentObject;
}
task.rowsExported = progress.rowsExported;
task.totalRows = progress.totalRows;
task.status = normalizeExportStatus(progress.status);
+2
View File
@@ -4986,6 +4986,8 @@ export default {
export: "Export",
exportAllDatabases: "Export All Databases",
exporting: "Exporting...",
preparing: "Preparing export (reading tables and metadata)...",
preparingObject: "Preparing: {object}",
selectExportDirectory: "Select export directory",
currentDatabase: "Database {current}/{total}",
currentTable: "Current: {table} ({current}/{total})",
+2
View File
@@ -4753,6 +4753,8 @@ export default withEnglishFallback({
export: "Exportar",
exportAllDatabases: "Exportar todas las bases de datos",
exporting: "Exportando...",
preparing: "Preparando exportación (leyendo tablas y metadatos)...",
preparingObject: "Preparando: {object}",
selectExportDirectory: "Seleccionar directorio de exportación",
currentDatabase: "Base de datos {current}/{total}",
currentTable: "Actual: {table} ({current}/{total})",
+2
View File
@@ -4752,6 +4752,8 @@ export default withEnglishFallback({
export: "Esporta",
exportAllDatabases: "Esporta tutti i database",
exporting: "Esportazione in corso...",
preparing: "Preparazione esportazione (lettura tabelle e metadati)...",
preparingObject: "Preparazione: {object}",
selectExportDirectory: "Seleziona directory di esportazione",
currentDatabase: "Database {current}/{total}",
currentTable: "Corrente: {table} ({current}/{total})",
+2
View File
@@ -4753,6 +4753,8 @@ export default withEnglishFallback({
export: "エクスポート",
exportAllDatabases: "すべてのデータベースをエクスポート",
exporting: "エクスポート中...",
preparing: "エクスポートを準備中(テーブルとメタデータを読み取り中)...",
preparingObject: "準備中: {object}",
selectExportDirectory: "エクスポート先ディレクトリを選択",
currentDatabase: "データベース {current}/{total}",
currentTable: "現在: {table} ({current}/{total})",
+2
View File
@@ -4755,6 +4755,8 @@ export default withEnglishFallback({
export: "Exportar",
exportAllDatabases: "Exportar todos os bancos de dados",
exporting: "Exportando...",
preparing: "Preparando exportação (lendo tabelas e metadados)...",
preparingObject: "Preparando: {object}",
selectExportDirectory: "Selecionar diretório de exportação",
currentDatabase: "Banco de dados {current}/{total}",
currentTable: "Atual: {table} ({current}/{total})",
+2
View File
@@ -4985,6 +4985,8 @@ export default withEnglishFallback({
export: "导出",
exportAllDatabases: "导出全部数据库",
exporting: "正在导出...",
preparing: "正在准备导出(读取表与元数据)...",
preparingObject: "正在准备: {object}",
selectExportDirectory: "选择导出目录",
currentDatabase: "数据库 {current}/{total}",
currentTable: "当前: {table} ({current}/{total})",
+2
View File
@@ -4422,6 +4422,8 @@ export default withEnglishFallback({
export: "匯出",
exportAllDatabases: "匯出全部資料庫",
exporting: "正在匯出……",
preparing: "正在準備匯出(讀取資料表與中繼資料)...",
preparingObject: "正在準備: {object}",
selectExportDirectory: "選擇匯出目錄",
currentDatabase: "資料庫 {current}/{total}",
currentTable: "目前: {table} ({current}/{total})",
+2
View File
@@ -2851,6 +2851,8 @@ export interface ExportProgress {
totalRows: number | null;
status: "Running" | "Done" | "Error" | "Cancelled";
error: string | null;
/** True while listing schema / prefetching metadata before objects are written. */
preparing?: boolean;
}
// --- Table Export ---