fix: 修复桌面端下载保存对话框

This commit is contained in:
2026-05-21 17:04:49 +08:00
parent 1288553df8
commit d56eaf5efc
10 changed files with 451 additions and 1 deletions
+11
View File
@@ -8,6 +8,7 @@
"name": "capacity-report-frontend",
"version": "1.0.0",
"dependencies": {
"@tauri-apps/api": "^2.11.0",
"@vicons/ionicons5": "^0.13.0",
"monaco-editor": "^0.55.1",
"naive-ui": "^2.42.0",
@@ -1004,6 +1005,16 @@
"win32"
]
},
"node_modules/@tauri-apps/api": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+1
View File
@@ -9,6 +9,7 @@
"preview": "vite preview --host 0.0.0.0"
},
"dependencies": {
"@tauri-apps/api": "^2.11.0",
"@vicons/ionicons5": "^0.13.0",
"monaco-editor": "^0.55.1",
"naive-ui": "^2.42.0",
+97
View File
@@ -5,7 +5,29 @@ const DESKTOP_API_BASE = 'http://127.0.0.1:9081';
const API_BASE = resolveApiBase();
const API_FETCH_RETRIES = 8;
const API_FETCH_RETRY_DELAY_MS = 500;
const TAURI_HTTP_ERROR_PREFIX = 'CAPAREPORT_HTTP_ERROR:';
let onUnauthorized: (() => void) | null = null;
let cachedTauriInvoke: TauriInvoke | null = null;
type TauriInvoke = <T>(command: string, args?: Record<string, unknown>) => Promise<T>;
interface TauriDownloadHeader {
name: string;
value: string;
}
interface TauriDownloadRequest {
url: string;
method: 'GET' | 'POST';
filename: string;
headers: TauriDownloadHeader[];
body?: string;
}
interface TauriDownloadResult {
saved: boolean;
path?: string;
}
export class ApiRequestError extends Error {
status: number;
@@ -134,6 +156,17 @@ export async function download(url: string, body: unknown, filename: string): Pr
headers.set('Authorization', `Bearer ${token}`);
}
if (isTauriRuntime()) {
await downloadWithTauri({
url: apiUrl(url),
method: 'POST',
filename,
headers: headersToPairs(headers),
body: JSON.stringify(body)
});
return;
}
const response = await fetchWithRetry(apiUrl(url), {
method: 'POST',
headers,
@@ -155,6 +188,16 @@ export async function downloadGet(url: string, fallbackFilename: string): Promis
headers.set('Authorization', `Bearer ${token}`);
}
if (isTauriRuntime()) {
await downloadWithTauri({
url: apiUrl(url),
method: 'GET',
filename: fallbackFilename,
headers: headersToPairs(headers)
});
return;
}
const response = await fetchWithRetry(apiUrl(url), { headers });
if (response.status === 401) {
clearToken();
@@ -216,6 +259,60 @@ async function saveBlobResponse(response: Response, filename: string): Promise<v
URL.revokeObjectURL(objectUrl);
}
async function downloadWithTauri(request: TauriDownloadRequest): Promise<void> {
try {
const invoke = await getTauriInvoke();
await invoke<TauriDownloadResult>('download_to_file', { request });
} catch (error) {
throw parseTauriDownloadError(error);
}
}
async function getTauriInvoke(): Promise<TauriInvoke> {
if (!cachedTauriInvoke) {
const api = await import('@tauri-apps/api/core');
cachedTauriInvoke = api.invoke;
}
return cachedTauriInvoke;
}
function headersToPairs(headers: Headers): TauriDownloadHeader[] {
const pairs: TauriDownloadHeader[] = [];
headers.forEach((value, name) => {
pairs.push({ name, value });
});
return pairs;
}
function parseTauriDownloadError(error: unknown): Error {
const message = error instanceof Error ? error.message : String(error || '下载失败');
if (!message.startsWith(TAURI_HTTP_ERROR_PREFIX)) {
return error instanceof Error ? error : new Error(message);
}
const payload = message.slice(TAURI_HTTP_ERROR_PREFIX.length);
const separator = payload.indexOf(':');
const status = Number(payload.slice(0, separator));
const body = separator >= 0 ? payload.slice(separator + 1) : '';
const parsed = parseApiErrorText(body, `下载失败 (${status || 'HTTP'})`);
if (status === 401) {
clearToken();
onUnauthorized?.();
}
return new ApiRequestError(parsed.message, status || 500, parsed.detail);
}
function parseApiErrorText(text: string, fallback: string): { message: string; detail?: ApiErrorDetail } {
try {
const data = JSON.parse(text) as ApiError;
return parseApiError(data, fallback);
} catch {
return { message: text || fallback };
}
}
async function fetchWithRetry(url: string, init: RequestInit): Promise<Response> {
let lastError: unknown;
for (let attempt = 0; attempt <= API_FETCH_RETRIES; attempt += 1) {