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:排查离线机器桌面端白屏
|
## 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,不参与用户机器运行。
|
- 运行时外链扫描确认:`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 headers = new Headers({ 'Content-Type': 'application/json' });
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -157,14 +157,13 @@ export async function download(url: string, body: unknown, filename: string): Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTauriRuntime()) {
|
if (isTauriRuntime()) {
|
||||||
await downloadWithTauri({
|
return await downloadWithTauri({
|
||||||
url: apiUrl(url),
|
url: apiUrl(url),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
filename,
|
filename,
|
||||||
headers: headersToPairs(headers),
|
headers: headersToPairs(headers),
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetchWithRetry(apiUrl(url), {
|
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);
|
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 headers = new Headers();
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -189,13 +188,12 @@ export async function downloadGet(url: string, fallbackFilename: string): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isTauriRuntime()) {
|
if (isTauriRuntime()) {
|
||||||
await downloadWithTauri({
|
return await downloadWithTauri({
|
||||||
url: apiUrl(url),
|
url: apiUrl(url),
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
filename: fallbackFilename,
|
filename: fallbackFilename,
|
||||||
headers: headersToPairs(headers)
|
headers: headersToPairs(headers)
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetchWithRetry(apiUrl(url), { headers });
|
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;
|
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 }> {
|
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 blob = await response.blob();
|
||||||
const objectUrl = URL.createObjectURL(blob);
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@@ -257,12 +255,14 @@ async function saveBlobResponse(response: Response, filename: string): Promise<v
|
|||||||
link.download = filename;
|
link.download = filename;
|
||||||
link.click();
|
link.click();
|
||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadWithTauri(request: TauriDownloadRequest): Promise<void> {
|
async function downloadWithTauri(request: TauriDownloadRequest): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const invoke = await getTauriInvoke();
|
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) {
|
} catch (error) {
|
||||||
throw parseTauriDownloadError(error);
|
throw parseTauriDownloadError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ import {
|
|||||||
|
|
||||||
import { apiGet, apiPost, download } from '../api/client';
|
import { apiGet, apiPost, download } from '../api/client';
|
||||||
import type { ApiMessage, DatabaseInfo, TableData, TableInfo } from '../types';
|
import type { ApiMessage, DatabaseInfo, TableData, TableInfo } from '../types';
|
||||||
|
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
||||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||||
|
|
||||||
type RowData = Record<string, unknown>;
|
type RowData = Record<string, unknown>;
|
||||||
@@ -483,14 +484,18 @@ async function downloadSelectedCsv() {
|
|||||||
if (!selectedCsvTable.value) return;
|
if (!selectedCsvTable.value) return;
|
||||||
if (downloadingFormat.value) return;
|
if (downloadingFormat.value) return;
|
||||||
|
|
||||||
|
const filename = `${selectedCsvTable.value}.csv`;
|
||||||
downloadingFormat.value = 'csv';
|
downloadingFormat.value = 'csv';
|
||||||
try {
|
try {
|
||||||
await download(
|
const completed = await download(
|
||||||
'/api/download',
|
'/api/download',
|
||||||
{ table_name: selectedCsvTable.value, format: 'csv' },
|
{ table_name: selectedCsvTable.value, format: 'csv' },
|
||||||
`${selectedCsvTable.value}.csv`
|
filename
|
||||||
);
|
);
|
||||||
csvDialogVisible.value = false;
|
if (completed) {
|
||||||
|
csvDialogVisible.value = false;
|
||||||
|
showDownloadCompleteDialog(dialog, filename);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '导出失败');
|
message.error(error instanceof Error ? error.message : '导出失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -503,13 +508,17 @@ async function downloadSelectedXlsx() {
|
|||||||
if (downloadingFormat.value) return;
|
if (downloadingFormat.value) return;
|
||||||
|
|
||||||
downloadingFormat.value = 'xlsx';
|
downloadingFormat.value = 'xlsx';
|
||||||
|
const filename = selectedXlsxTables.value.length === 1 ? `${selectedXlsxTables.value[0]}.xlsx` : 'tables.xlsx';
|
||||||
try {
|
try {
|
||||||
await download(
|
const completed = await download(
|
||||||
'/api/download',
|
'/api/download',
|
||||||
{ table_names: selectedXlsxTables.value, format: 'xlsx' },
|
{ 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) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '导出失败');
|
message.error(error instanceof Error ? error.message : '导出失败');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ import { CopyOutline, DownloadOutline, FileTrayOutline, RefreshOutline, TrashOut
|
|||||||
|
|
||||||
import { apiGet, apiPost, download as downloadFile } from '../api/client';
|
import { apiGet, apiPost, download as downloadFile } from '../api/client';
|
||||||
import type { CacheSize, HistoryDetail, HistoryRecord } from '../types';
|
import type { CacheSize, HistoryDetail, HistoryRecord } from '../types';
|
||||||
|
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
||||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||||
|
|
||||||
type LogLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
type LogLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
||||||
@@ -184,8 +185,11 @@ async function downloadHistory(record: HistoryRecord) {
|
|||||||
|
|
||||||
downloadingRecordId.value = record.id;
|
downloadingRecordId.value = record.id;
|
||||||
try {
|
try {
|
||||||
await downloadFile('/api/history/download', { record_id: record.id }, `${record.id}.zip`);
|
const filename = `${record.id}.zip`;
|
||||||
message.success('历史数据下载已开始');
|
const completed = await downloadFile('/api/history/download', { record_id: record.id }, filename);
|
||||||
|
if (completed) {
|
||||||
|
showDownloadCompleteDialog(dialog, filename);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '下载历史数据失败');
|
message.error(error instanceof Error ? error.message : '下载历史数据失败');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -312,11 +312,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
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 { CloseOutline, CloudDownloadOutline, CloudUploadOutline } from '@vicons/ionicons5';
|
||||||
|
|
||||||
import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
||||||
import type { ApiMessage, AppConfig, HistoryRetentionConfig, RemoteDataConfig } from '../types';
|
import type { ApiMessage, AppConfig, HistoryRetentionConfig, RemoteDataConfig } from '../types';
|
||||||
|
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
||||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||||
|
|
||||||
interface ExtractFieldConfig {
|
interface ExtractFieldConfig {
|
||||||
@@ -327,6 +328,7 @@ interface ExtractFieldConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
const configInput = ref<HTMLInputElement | null>(null);
|
const configInput = ref<HTMLInputElement | null>(null);
|
||||||
const configUpdate = ref('');
|
const configUpdate = ref('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -661,7 +663,11 @@ async function changePassword() {
|
|||||||
|
|
||||||
async function downloadConfig() {
|
async function downloadConfig() {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '下载配置失败');
|
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