From f1a8b69c2aadca58f1e2ea922964cc1403bf4a91 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Mon, 1 Jun 2026 20:54:12 +0800 Subject: [PATCH] fix: unify frontend API client - add blob support to fetchWithAuth, eliminate raw fetch calls - Extend fetchWithAuth with responseType parameter ('json' | 'blob') - reports.ts: PDF/CSV export now uses fetchWithAuth blob mode - reports/page.tsx: remove duplicate API_BASE, use fetchWithAuth for CSV export - lifecycle/new/page.tsx: replace raw fetch with fetchWithAuth for quick-start POST --- .../dashboard/lifecycle/new/page.tsx | 32 ++++--------------- .../(dashboard)/dashboard/reports/page.tsx | 13 ++------ frontend/lib/api/client.ts | 11 +++++-- frontend/lib/api/reports.ts | 10 ++---- 4 files changed, 20 insertions(+), 46 deletions(-) diff --git a/frontend/app/(dashboard)/dashboard/lifecycle/new/page.tsx b/frontend/app/(dashboard)/dashboard/lifecycle/new/page.tsx index 745af14..e126e07 100644 --- a/frontend/app/(dashboard)/dashboard/lifecycle/new/page.tsx +++ b/frontend/app/(dashboard)/dashboard/lifecycle/new/page.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { AlertCard } from "@/components/business/alert-card"; +import { fetchWithAuth } from "@/lib/api/client"; import { Check } from "lucide-react"; interface ProjectResponse { @@ -57,39 +58,20 @@ export default function NewProjectPage() { const token = session?.accessToken as string | undefined; try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/api/v1/lifecycle/projects/quick-start`, + const data = await fetchWithAuth( + "/api/v1/lifecycle/projects/quick-start", { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token || ""}`, - }, body: JSON.stringify({ brand_name: brandName.trim(), brand_url: brandUrl.trim(), description: description.trim() || undefined, }), - } - ); - - if (!response.ok) { - let errorMessage = "请求失败,请稍后重试"; - try { - const errData = await response.json(); - errorMessage = errData.detail || errData.message || errorMessage; - } catch { - // ignore parse error - } - throw new Error(errorMessage); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _data = (await response.json()) as { - project: ProjectResponse; - message: string; - }; + }, + token + ) as { project: ProjectResponse; message: string }; + void data; animateSteps(); } catch (err) { setSubmitting(false); diff --git a/frontend/app/(dashboard)/dashboard/reports/page.tsx b/frontend/app/(dashboard)/dashboard/reports/page.tsx index 198d01c..20a96ba 100644 --- a/frontend/app/(dashboard)/dashboard/reports/page.tsx +++ b/frontend/app/(dashboard)/dashboard/reports/page.tsx @@ -22,6 +22,7 @@ import { TableRow, } from "@/components/ui/table"; import { reportsApi } from "@/lib/api/reports"; +import { fetchWithAuth } from "@/lib/api/client"; import type { QueryListResponse } from "@/lib/api/queries"; import type { CitationListResponse, CitationStats } from "@/lib/api/citations"; import { useApi } from "@/lib/hooks/use-api"; @@ -39,8 +40,6 @@ import { BarChart3, } from "lucide-react"; -const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; - export default function ReportsPage() { const { data: session } = useSession(); const [selectedQuery, setSelectedQuery] = useState(""); @@ -87,15 +86,7 @@ export default function ReportsPage() { if (format === "csv") { const query = `?query_id=${queryId}`; - const url = `${API_BASE}/api/v1/reports/export/csv${query}`; - const res = await fetch(url, { - headers: { Authorization: `Bearer ${session.accessToken}` }, - }); - if (!res.ok) { - const errorData = await res.json().catch(() => ({ detail: "导出失败" })); - throw new Error(errorData.detail || `HTTP ${res.status}`); - } - blob = await res.blob(); + blob = await fetchWithAuth(`/api/v1/reports/export/csv${query}`, {}, session.accessToken, "blob") as unknown as Blob; filename = `report_${queryId}_${new Date().toISOString().split("T")[0]}.csv`; } else { blob = await reportsApi.exportPDF(session.accessToken, queryId); diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index d214045..a82aa50 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -7,12 +7,14 @@ export function getApiUrl(path: string): string { return `${API_BASE}${path}`; } +export type ResponseType = "json" | "blob"; + export async function fetchWithAuth( url: string, options: RequestInit = {}, - token?: string + token?: string, + responseType: ResponseType = "json" ) { - // 如果没有显式传入 token,尝试从 NextAuth session 获取 let authToken = token; if (!authToken && typeof window !== "undefined") { try { @@ -33,7 +35,6 @@ export async function fetchWithAuth( const res = await fetch(`${API_BASE}${url}`, { ...options, headers }); if (res.status === 401) { - // 不要自动跳转登录页,让页面组件/layout自行处理认证状态 throw new Error("登录已过期,请重新登录"); } @@ -52,5 +53,9 @@ export async function fetchWithAuth( return null; } + if (responseType === "blob") { + return res.blob(); + } + return res.json(); } diff --git a/frontend/lib/api/reports.ts b/frontend/lib/api/reports.ts index ba2dd7d..ea45a98 100644 --- a/frontend/lib/api/reports.ts +++ b/frontend/lib/api/reports.ts @@ -1,16 +1,12 @@ -import { API_BASE, fetchWithAuth } from "./client"; +import { fetchWithAuth } from "./client"; export const reportsApi = { exportCSV: (token: string, queryId?: string) => { const query = queryId ? `?query_id=${queryId}` : ""; - return fetchWithAuth(`/api/v1/reports/export/csv${query}`, {}, token); + return fetchWithAuth(`/api/v1/reports/export/csv${query}`, {}, token, "blob"); }, exportPDF: async (token: string, queryId?: string) => { const query = queryId ? `?query_id=${queryId}` : ""; - const res = await fetch(`${API_BASE}/api/v1/reports/export/pdf${query}`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (!res.ok) throw new Error("导出失败"); - return res.blob(); + return fetchWithAuth(`/api/v1/reports/export/pdf${query}`, {}, token, "blob"); }, };