fix: 增加下载完成提示弹窗
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
# 项目上下文记录
|
||||
## 2026-05-21:增加下载完成提示弹窗
|
||||
|
||||
- 数据表 CSV/XLSX 导出、历史数据压缩包下载和配置文件下载在下载流程完成后会弹出 Naive UI 成功对话框,使用绿色成功图标提示用户文件已下载完成。
|
||||
- 提示逻辑统一放在 `frontend/src/composables/downloadFeedback.ts`;桌面端会在 Tauri 原生保存流程确认写入后提示,用户取消保存时不弹完成提示,Web 端会在浏览器下载触发后提示。
|
||||
|
||||
## 2026-05-21:排查离线机器桌面端白屏
|
||||
|
||||
- 运行时外链扫描确认:`frontend/src`、`app`、`src-tauri/src` 中没有 CDN、在线字体或外网业务接口;桌面端前端只访问本机 `http://127.0.0.1:9081` sidecar,`src-tauri/tauri.conf.json` 的 `https://schema.tauri.app/config/2` 只是编辑器/构建 schema,不参与用户机器运行。
|
||||
|
||||
+11
-11
@@ -149,7 +149,7 @@ export function upload<T>(
|
||||
});
|
||||
}
|
||||
|
||||
export async function download(url: string, body: unknown, filename: string): Promise<void> {
|
||||
export async function download(url: string, body: unknown, filename: string): Promise<boolean> {
|
||||
const headers = new Headers({ 'Content-Type': 'application/json' });
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
@@ -157,14 +157,13 @@ export async function download(url: string, body: unknown, filename: string): Pr
|
||||
}
|
||||
|
||||
if (isTauriRuntime()) {
|
||||
await downloadWithTauri({
|
||||
return await downloadWithTauri({
|
||||
url: apiUrl(url),
|
||||
method: 'POST',
|
||||
filename,
|
||||
headers: headersToPairs(headers),
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetchWithRetry(apiUrl(url), {
|
||||
@@ -178,10 +177,10 @@ export async function download(url: string, body: unknown, filename: string): Pr
|
||||
throw new ApiRequestError(error.message, response.status, error.detail);
|
||||
}
|
||||
|
||||
await saveBlobResponse(response, parseFilename(response.headers.get('content-disposition')) || filename);
|
||||
return await saveBlobResponse(response, parseFilename(response.headers.get('content-disposition')) || filename);
|
||||
}
|
||||
|
||||
export async function downloadGet(url: string, fallbackFilename: string): Promise<void> {
|
||||
export async function downloadGet(url: string, fallbackFilename: string): Promise<boolean> {
|
||||
const headers = new Headers();
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
@@ -189,13 +188,12 @@ export async function downloadGet(url: string, fallbackFilename: string): Promis
|
||||
}
|
||||
|
||||
if (isTauriRuntime()) {
|
||||
await downloadWithTauri({
|
||||
return await downloadWithTauri({
|
||||
url: apiUrl(url),
|
||||
method: 'GET',
|
||||
filename: fallbackFilename,
|
||||
headers: headersToPairs(headers)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetchWithRetry(apiUrl(url), { headers });
|
||||
@@ -214,7 +212,7 @@ export async function downloadGet(url: string, fallbackFilename: string): Promis
|
||||
}
|
||||
|
||||
const filename = parseFilename(response.headers.get('content-disposition')) || fallbackFilename;
|
||||
await saveBlobResponse(response, filename);
|
||||
return await saveBlobResponse(response, filename);
|
||||
}
|
||||
|
||||
async function readError(response: Response): Promise<{ message: string; detail?: ApiErrorDetail }> {
|
||||
@@ -249,7 +247,7 @@ function parseApiError(data: ApiError, fallback: string): { message: string; det
|
||||
};
|
||||
}
|
||||
|
||||
async function saveBlobResponse(response: Response, filename: string): Promise<void> {
|
||||
async function saveBlobResponse(response: Response, filename: string): Promise<boolean> {
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
@@ -257,12 +255,14 @@ async function saveBlobResponse(response: Response, filename: string): Promise<v
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function downloadWithTauri(request: TauriDownloadRequest): Promise<void> {
|
||||
async function downloadWithTauri(request: TauriDownloadRequest): Promise<boolean> {
|
||||
try {
|
||||
const invoke = await getTauriInvoke();
|
||||
await invoke<TauriDownloadResult>('download_to_file', { request });
|
||||
const result = await invoke<TauriDownloadResult>('download_to_file', { request });
|
||||
return result.saved;
|
||||
} catch (error) {
|
||||
throw parseTauriDownloadError(error);
|
||||
}
|
||||
|
||||
@@ -231,6 +231,7 @@ import {
|
||||
|
||||
import { apiGet, apiPost, download } from '../api/client';
|
||||
import type { ApiMessage, DatabaseInfo, TableData, TableInfo } from '../types';
|
||||
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
type RowData = Record<string, unknown>;
|
||||
@@ -483,14 +484,18 @@ async function downloadSelectedCsv() {
|
||||
if (!selectedCsvTable.value) return;
|
||||
if (downloadingFormat.value) return;
|
||||
|
||||
const filename = `${selectedCsvTable.value}.csv`;
|
||||
downloadingFormat.value = 'csv';
|
||||
try {
|
||||
await download(
|
||||
const completed = await download(
|
||||
'/api/download',
|
||||
{ table_name: selectedCsvTable.value, format: 'csv' },
|
||||
`${selectedCsvTable.value}.csv`
|
||||
filename
|
||||
);
|
||||
csvDialogVisible.value = false;
|
||||
if (completed) {
|
||||
csvDialogVisible.value = false;
|
||||
showDownloadCompleteDialog(dialog, filename);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '导出失败');
|
||||
} finally {
|
||||
@@ -503,13 +508,17 @@ async function downloadSelectedXlsx() {
|
||||
if (downloadingFormat.value) return;
|
||||
|
||||
downloadingFormat.value = 'xlsx';
|
||||
const filename = selectedXlsxTables.value.length === 1 ? `${selectedXlsxTables.value[0]}.xlsx` : 'tables.xlsx';
|
||||
try {
|
||||
await download(
|
||||
const completed = await download(
|
||||
'/api/download',
|
||||
{ table_names: selectedXlsxTables.value, format: 'xlsx' },
|
||||
selectedXlsxTables.value.length === 1 ? `${selectedXlsxTables.value[0]}.xlsx` : 'tables.xlsx'
|
||||
filename
|
||||
);
|
||||
xlsxDialogVisible.value = false;
|
||||
if (completed) {
|
||||
xlsxDialogVisible.value = false;
|
||||
showDownloadCompleteDialog(dialog, filename);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '导出失败');
|
||||
} finally {
|
||||
|
||||
@@ -100,6 +100,7 @@ import { CopyOutline, DownloadOutline, FileTrayOutline, RefreshOutline, TrashOut
|
||||
|
||||
import { apiGet, apiPost, download as downloadFile } from '../api/client';
|
||||
import type { CacheSize, HistoryDetail, HistoryRecord } from '../types';
|
||||
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
type LogLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
||||
@@ -184,8 +185,11 @@ async function downloadHistory(record: HistoryRecord) {
|
||||
|
||||
downloadingRecordId.value = record.id;
|
||||
try {
|
||||
await downloadFile('/api/history/download', { record_id: record.id }, `${record.id}.zip`);
|
||||
message.success('历史数据下载已开始');
|
||||
const filename = `${record.id}.zip`;
|
||||
const completed = await downloadFile('/api/history/download', { record_id: record.id }, filename);
|
||||
if (completed) {
|
||||
showDownloadCompleteDialog(dialog, filename);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '下载历史数据失败');
|
||||
} finally {
|
||||
|
||||
@@ -312,11 +312,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
import { useMessage, type SelectOption } from 'naive-ui';
|
||||
import { useDialog, useMessage, type SelectOption } from 'naive-ui';
|
||||
import { CloseOutline, CloudDownloadOutline, CloudUploadOutline } from '@vicons/ionicons5';
|
||||
|
||||
import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
||||
import type { ApiMessage, AppConfig, HistoryRetentionConfig, RemoteDataConfig } from '../types';
|
||||
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
interface ExtractFieldConfig {
|
||||
@@ -327,6 +328,7 @@ interface ExtractFieldConfig {
|
||||
}
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const configInput = ref<HTMLInputElement | null>(null);
|
||||
const configUpdate = ref('');
|
||||
const loading = ref(false);
|
||||
@@ -661,7 +663,11 @@ async function changePassword() {
|
||||
|
||||
async function downloadConfig() {
|
||||
try {
|
||||
await downloadGet('/api/config/download', 'Configure.json');
|
||||
const filename = 'Configure.json';
|
||||
const completed = await downloadGet('/api/config/download', filename);
|
||||
if (completed) {
|
||||
showDownloadCompleteDialog(dialog, filename);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '下载配置失败');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
interface DownloadDialog {
|
||||
success(options: {
|
||||
title: string;
|
||||
content: string;
|
||||
positiveText?: string;
|
||||
closable?: boolean;
|
||||
maskClosable?: boolean;
|
||||
}): unknown;
|
||||
}
|
||||
|
||||
export function showDownloadCompleteDialog(dialog: DownloadDialog, filename?: string): void {
|
||||
const safeName = filename?.trim();
|
||||
|
||||
dialog.success({
|
||||
title: '下载完成',
|
||||
content: safeName ? `文件「${safeName}」已下载完成。` : '文件已下载完成。',
|
||||
positiveText: '知道了',
|
||||
closable: true,
|
||||
maskClosable: true
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user