101 lines
2.8 KiB
TypeScript
101 lines
2.8 KiB
TypeScript
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<string, unknown>,
|
||
fallbackName: string
|
||
): Promise<void> {
|
||
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<void> {
|
||
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;
|
||
}
|