import { ApiError } from "./apiClient"; const API_PREFIX = "/api/app"; function joinUrl(baseUrl: string, path: string): string { if (!baseUrl) return path; const b = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl; const p = path.startsWith("/") ? path : `/${path}`; return `${b}${p}`; } function toQueryString(params: Record): string { const qs = new URLSearchParams(); for (const [k, v] of Object.entries(params)) { if (v === undefined || v === null || v === "") continue; if (typeof v === "boolean") { qs.set(k, v ? "true" : "false"); continue; } qs.set(k, String(v)); } const s = qs.toString(); return s ? `?${s}` : ""; } function getTokenForFetch(): string | null { try { return localStorage.getItem("access_token") ?? localStorage.getItem("token") ?? null; } catch { return null; } } function parseFileNameFromContentDisposition(h: string | null): string | null { if (!h) return null; const m = /filename\*?=(?:UTF-8''|)([^;]+)/i.exec(h); if (m?.[1]) return decodeURIComponent(m[1].trim().replace(/^["']|["']$/g, "")); return null; } function getAbpErrorMessage(payload: unknown): string | null { if (!payload || typeof payload !== "object") return null; const p = payload as { error?: { message?: string }; errors?: unknown }; const nested = p.error?.message?.trim(); if (nested) return nested; if (typeof p.errors === "string" && p.errors.trim()) return p.errors.trim(); return null; } function unwrapEnvelope(payload: unknown): T { if (!payload || typeof payload !== "object") return payload as T; const w = payload as Record; if ("data" in w && w.data !== undefined) { if (w.succeeded === false) { const msg = (typeof (w.error as { message?: string } | undefined)?.message === "string" ? (w.error as { message: string }).message.trim() : "") || getAbpErrorMessage(payload) || "Request failed."; throw new ApiError(msg, typeof w.statusCode === "number" ? w.statusCode : 400, payload); } return w.data as T; } return payload as T; } /** * GET 下载二进制(Excel / PDF / 模板),触发浏览器保存。 */ export async function authorizedGetBlobDownload(opts: { path: string; query?: Record; defaultFileName: string; signal?: AbortSignal; }): Promise { const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:19001"; const url = joinUrl(baseUrl, `${API_PREFIX}${opts.path}${toQueryString(opts.query ?? {})}`); const token = getTokenForFetch(); const res = await fetch(url, { method: "GET", headers: token ? { Authorization: `Bearer ${token}` } : {}, signal: opts.signal, }); const ct = res.headers.get("content-type") ?? ""; if (!res.ok) { if (ct.includes("application/json")) { const payload = await res.json().catch(() => null); const msg = getAbpErrorMessage(payload) || "Download failed."; throw new ApiError(msg, res.status, payload); } const t = await res.text().catch(() => ""); throw new ApiError(t || "Download failed.", res.status, t); } if (ct.includes("application/json")) { const payload = await res.json().catch(() => null); const msg = getAbpErrorMessage(payload) || "Download failed."; throw new ApiError(msg, res.status, payload); } const blob = await res.blob(); const name = parseFileNameFromContentDisposition(res.headers.get("content-disposition")) || opts.defaultFileName; const href = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = href; a.download = name; a.click(); URL.revokeObjectURL(href); } /** * POST 下载二进制。ABP 约定控制器里 `Export*` / `Download*` 多为 **POST**; * 若误用 GET,会命中 `GET …/{id}` 把路径段当成 id(如产品导出报「产品不存在」)。 */ export async function authorizedPostBlobDownload(opts: { path: string; query?: Record; defaultFileName: string; signal?: AbortSignal; }): Promise { const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:19001"; const url = joinUrl(baseUrl, `${API_PREFIX}${opts.path}${toQueryString(opts.query ?? {})}`); const token = getTokenForFetch(); const headers: Record = { Accept: "*/*" }; if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(url, { method: "POST", headers, signal: opts.signal, }); const ct = res.headers.get("content-type") ?? ""; if (!res.ok) { if (ct.includes("application/json")) { const payload = await res.json().catch(() => null); const msg = getAbpErrorMessage(payload) || "Download failed."; throw new ApiError(msg, res.status, payload); } const t = await res.text().catch(() => ""); throw new ApiError(t || "Download failed.", res.status, t); } if (ct.includes("application/json")) { const payload = await res.json().catch(() => null); const msg = getAbpErrorMessage(payload) || "Download failed."; throw new ApiError(msg, res.status, payload); } const blob = await res.blob(); const name = parseFileNameFromContentDisposition(res.headers.get("content-disposition")) || opts.defaultFileName; const href = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = href; a.download = name; a.click(); URL.revokeObjectURL(href); } /** * multipart 上传文件,解析 JSON 响应(兼容 ABP 包裹 `{ data, succeeded }`)。 */ export async function authorizedPostMultipartJson(opts: { path: string; fieldName: string; file: File; signal?: AbortSignal; }): Promise { const baseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:19001"; const url = joinUrl(baseUrl, `${API_PREFIX}${opts.path}`); const token = getTokenForFetch(); const fd = new FormData(); fd.append(opts.fieldName, opts.file); const headers: Record = {}; if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(url, { method: "POST", headers, body: fd, signal: opts.signal }); const ct = res.headers.get("content-type") ?? ""; const payload = ct.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => ""); if (!res.ok) { const msg = (typeof payload === "object" && payload && getAbpErrorMessage(payload)) || (typeof payload === "string" && payload.trim()) || "Upload failed."; throw new ApiError(msg, res.status, payload); } if (typeof payload !== "object" || payload === null) { throw new ApiError("Invalid import response.", res.status, payload); } return unwrapEnvelope(payload); }