import axios from 'axios'; /** * 公共文件下载工具(blob 请求 + 触发浏览器保存) * * - 成功:解析响应头 Content-Disposition 的 filename(RFC 5987 `filename*=UTF-8''` 优先),兜底用 fallbackName * - 失败兜底:后端业务错误也以 blob 返回(JSON),blob.type 为 application/json 时解析 msg 提示 * - 401:清 token 跳登录页(与 createAxios 行为一致) */ export async function downloadBlob( url: string, params: Record, fallbackName: string ): Promise { const token = localStorage.getItem('token'); let blob: Blob; let disposition = ''; try { const response = await axios.get(url, { baseURL: import.meta.env.VITE_BASE_URL || '', params, responseType: 'blob', headers: token ? { Authorization: `Bearer ${token}` } : {}, }); blob = response.data as Blob; disposition = (response.headers['content-disposition'] as string) || ''; } catch (err: any) { const status = err?.response?.status; const errBlob: Blob | undefined = err?.response?.data; if (status === 401) { window.$message?.error('您未登录,或者登录已经超时,请先登录!'); localStorage.removeItem('token'); localStorage.removeItem('auth-storage'); window.location.href = '/login'; return; } if (errBlob instanceof Blob) { await showBlobError(errBlob); return; } window.$message?.error('下载失败,请稍后重试'); return; } // 后端业务错误以 JSON blob 返回 if (blob.type.includes('application/json')) { await showBlobError(blob); return; } const filename = parseFilename(disposition) || fallbackName; const objectUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = objectUrl; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(objectUrl); } /** * 解析 blob 形式的错误响应并提示 */ async function showBlobError(blob: Blob): Promise { try { const body = JSON.parse(await blob.text()); window.$message?.error(body?.msg || '导出失败'); } catch { window.$message?.error('导出失败'); } } /** * 从 Content-Disposition 解析文件名:RFC 5987 filename*=UTF-8'' 优先,其次 filename="..." */ function parseFilename(disposition: string): string | null { if (!disposition) { return null; } const rfc5987 = disposition.match(/filename\*\s*=\s*(?:UTF-8|utf-8)''([^;]+)/i); if (rfc5987?.[1]) { try { return decodeURIComponent(rfc5987[1].trim()); } catch { return null; } } const plain = disposition.match(/filename\s*=\s*"?([^";]+)"?/i); if (plain?.[1]) { try { return decodeURIComponent(plain[1].trim()); } catch { return plain[1].trim(); } } return null; }