batchFileHttp.ts 6.62 KB
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, unknown>): 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<T>(payload: unknown): T {
  if (!payload || typeof payload !== "object") return payload as T;
  const w = payload as Record<string, unknown>;
  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<string, unknown>;
  defaultFileName: string;
  signal?: AbortSignal;
}): Promise<void> {
  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<string, unknown>;
  defaultFileName: string;
  signal?: AbortSignal;
}): Promise<void> {
  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<string, string> = { 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<T>(opts: {
  path: string;
  fieldName: string;
  file: File;
  signal?: AbortSignal;
}): Promise<T> {
  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<string, string> = {};
  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<T>(payload);
}