diff --git a/docs/project_context.md b/docs/project_context.md index 8746bea..e59ded5 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -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,不参与用户机器运行。 diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 7adb15b..132caa0 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -149,7 +149,7 @@ export function upload( }); } -export async function download(url: string, body: unknown, filename: string): Promise { +export async function download(url: string, body: unknown, filename: string): Promise { 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 { +export async function downloadGet(url: string, fallbackFilename: string): Promise { 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 { +async function saveBlobResponse(response: Response, filename: string): Promise { 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 { +async function downloadWithTauri(request: TauriDownloadRequest): Promise { try { const invoke = await getTauriInvoke(); - await invoke('download_to_file', { request }); + const result = await invoke('download_to_file', { request }); + return result.saved; } catch (error) { throw parseTauriDownloadError(error); } diff --git a/frontend/src/components/DatabasePanel.vue b/frontend/src/components/DatabasePanel.vue index d7393ec..3bc1ae6 100644 --- a/frontend/src/components/DatabasePanel.vue +++ b/frontend/src/components/DatabasePanel.vue @@ -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; @@ -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 { diff --git a/frontend/src/components/HistoryPanel.vue b/frontend/src/components/HistoryPanel.vue index 1330f0c..3140c87 100644 --- a/frontend/src/components/HistoryPanel.vue +++ b/frontend/src/components/HistoryPanel.vue @@ -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 { diff --git a/frontend/src/components/SettingsPanel.vue b/frontend/src/components/SettingsPanel.vue index a7dc825..20883ba 100644 --- a/frontend/src/components/SettingsPanel.vue +++ b/frontend/src/components/SettingsPanel.vue @@ -312,11 +312,12 @@