export async function apiFetch(url: string, options?: RequestInit): Promise { const response = await fetch(url, options); const payload = await response.json().catch(() => null); if (!response.ok || !payload || payload.code !== 0) { throw new Error(payload?.msg || `HTTP ${response.status}`); } return payload.data as T; } export async function downloadBlob(url: string, fallbackName: string): Promise { const response = await fetch(url); if (!response.ok) { const payload = await response.json().catch(() => null); throw new Error(payload?.msg || `HTTP ${response.status}`); } const blob = await response.blob(); const filename = filenameFromDisposition(response.headers.get("Content-Disposition")) || fallbackName; const objectURL = URL.createObjectURL(blob); try { const link = document.createElement("a"); link.href = objectURL; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); } finally { URL.revokeObjectURL(objectURL); } } export function filenameFromDisposition(disposition: string | null): string { if (!disposition) return ""; const utf8 = /filename\*=UTF-8''([^;]+)/i.exec(disposition); if (utf8) return decodeURIComponent(utf8[1]); const quoted = /filename="([^"]+)"/i.exec(disposition); if (quoted) return quoted[1]; return ""; } export function downloadText(filename: string, content: string, type: string): void { const blob = new Blob([content], { type }); const objectURL = URL.createObjectURL(blob); try { const link = document.createElement("a"); link.href = objectURL; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); } finally { URL.revokeObjectURL(objectURL); } }