perf(export): 优化导出ui体验和性能
* docs(getting-started): set brew upgrade to use official homebrew cask * fix(mysql): make admin show handling dialect aware * fix(data-compare): create missing target tables during sync * feat(sql): suggest join conditions from foreign keys * fix(sidebar): use filtered order for range selection * fix(tabs): close connection tabs on disconnect * fix(sqlserver): execute cte queries without wrapping * fix(opengauss): open table structure editor * perf(export): 优化导出ui体验和性能 * perf(export): 优化导出ui体验和性能 * fix(export): normalize status casing and fix web route issues - Add missing table export web routes (POST /export/table, SSE progress, cancel) - Fix tauri table export Promise to wait for terminal events - Change cancel to return Ok(()) so it doesn't get overwritten as Error - Unify ExportStatus to PascalCase across all frontend components - Fix http.ts table export POST body wrapping - Fix duplicate pub mod in dbx-web routes * fix(export): return export file for browser download in web mode Write table export output to temp dir, register a download endpoint, and trigger browser download from the frontend when export completes. * fix(export): avoid table export progress races --------- Co-authored-by: Francesco Lucarelli <francesco06lucarelli@gmail.com> Co-authored-by: t8y2 <1156263951@qq.com> Co-authored-by: dqn <someonegdeng@qq.com>
This commit is contained in:
22 files changed
+1282
-159
No files matched your search
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2, CheckCircle2, XCircle, AlertCircle, X } from "lucide-vue-next";
|
||||
|
||||
const { t } = useI18n();
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
tableName: string;
|
||||
format: string;
|
||||
rowsExported: number;
|
||||
totalRows: number | null;
|
||||
status: string;
|
||||
errorMessage: string | null;
|
||||
disableCancel?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: [];
|
||||
"update:open": [value: boolean];
|
||||
}>();
|
||||
|
||||
const isActive = computed(() => props.status === "Running" || props.status === "Writing");
|
||||
const isFinished = computed(() => props.status === "Done" || props.status === "Error" || props.status === "Cancelled");
|
||||
const progressPercent = computed(() => {
|
||||
if (!props.totalRows || props.totalRows <= 0) return 0;
|
||||
return Math.min(100, Math.round((props.rowsExported / props.totalRows) * 100));
|
||||
});
|
||||
const rowsText = computed(() => {
|
||||
if (props.totalRows) {
|
||||
return t("exportProgress.rowsCount", {
|
||||
exported: props.rowsExported.toLocaleString(),
|
||||
total: props.totalRows.toLocaleString(),
|
||||
});
|
||||
}
|
||||
return t("exportProgress.rowsExported", { count: props.rowsExported.toLocaleString() });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:open="open"
|
||||
@update:open="
|
||||
(v: boolean) => {
|
||||
if (!isActive) emit('update:open', v);
|
||||
}
|
||||
"
|
||||
>
|
||||
<DialogContent class="sm:max-w-md" :class="{ 'pointer-events-none': isActive }" @interact-outside.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ title }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="py-4 space-y-4">
|
||||
<!-- Table name and format info -->
|
||||
<div class="text-sm text-muted-foreground">{{ tableName }} (.{{ format }})</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="w-full bg-muted rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
v-if="status === 'Running' || status === 'Writing'"
|
||||
class="h-full bg-primary rounded-full transition-all duration-300"
|
||||
:class="{ 'animate-pulse': !totalRows }"
|
||||
:style="{ width: totalRows ? `${progressPercent}%` : '50%' }"
|
||||
/>
|
||||
<div v-else-if="status === 'Done'" class="h-full bg-green-500 rounded-full" style="width: 100%" />
|
||||
</div>
|
||||
|
||||
<!-- Status message -->
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<template v-if="status === 'Running'">
|
||||
<Loader2 class="h-4 w-4 animate-spin text-primary" />
|
||||
<span>{{ t("exportProgress.fetching") }}</span>
|
||||
</template>
|
||||
<template v-else-if="status === 'Writing'">
|
||||
<Loader2 class="h-4 w-4 animate-spin text-primary" />
|
||||
<span>{{ t("exportProgress.writing") }}</span>
|
||||
</template>
|
||||
<template v-else-if="status === 'Done'">
|
||||
<CheckCircle2 class="h-4 w-4 text-green-500" />
|
||||
<span class="text-green-700 dark:text-green-400">{{ t("exportProgress.done") }}</span>
|
||||
</template>
|
||||
<template v-else-if="status === 'Error'">
|
||||
<XCircle class="h-4 w-4 text-destructive" />
|
||||
<span class="text-destructive">{{ errorMessage || t("exportProgress.error") }}</span>
|
||||
</template>
|
||||
<template v-else-if="status === 'Cancelled'">
|
||||
<AlertCircle class="h-4 w-4 text-yellow-500" />
|
||||
<span class="text-yellow-700 dark:text-yellow-400">{{ t("exportProgress.cancelled") }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Row count -->
|
||||
<div class="text-xs text-muted-foreground tabular-nums">
|
||||
{{ rowsText }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<template v-if="isActive">
|
||||
<Button variant="outline" size="sm" :disabled="disableCancel" @click="emit('cancel')">
|
||||
<X class="h-3.5 w-3.5 mr-1" />
|
||||
{{ t("exportProgress.cancel") }}
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else-if="isFinished">
|
||||
<Button variant="outline" size="sm" @click="emit('update:open', false)">
|
||||
{{ t("exportProgress.close") }}
|
||||
</Button>
|
||||
</template>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in new issue
Block a user