统一盲盒前端 API 调用层并收敛错误处理
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||
import { requestJson } from "@/lib/fetcher";
|
||||
import AuthModal from "@/components/AuthModal";
|
||||
import Button from "@/components/Button";
|
||||
import Input from "@/components/Input";
|
||||
@@ -230,13 +231,10 @@ export default function BlindboxLobbyPage() {
|
||||
setCreating(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/room", {
|
||||
const data = await requestJson<{ code: string }, { name?: string }>("/api/blindbox/room", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: name || undefined }),
|
||||
body: { name: name || undefined },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
router.push(`/blindbox/${data.code}`);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "创建失败");
|
||||
@@ -251,13 +249,10 @@ export default function BlindboxLobbyPage() {
|
||||
setJoining(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/room/join", {
|
||||
const data = await requestJson<{ code: string }, { code: string }>("/api/blindbox/room/join", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ code }),
|
||||
body: { code },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error);
|
||||
router.push(`/blindbox/${data.code}`);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "加入失败");
|
||||
@@ -270,14 +265,9 @@ export default function BlindboxLobbyPage() {
|
||||
if (deletingId || !profile) return;
|
||||
setDeletingId(room.id);
|
||||
try {
|
||||
const res = await fetch(`/api/blindbox/room/${room.code}`, {
|
||||
await requestJson<{ action: "deleted" | "left" }>(`/api/blindbox/room/${room.code}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "操作失败");
|
||||
}
|
||||
mutateRooms((prev) => prev ? { rooms: prev.rooms.filter((r) => r.id !== room.id) } : prev, false);
|
||||
setConfirmDeleteId(null);
|
||||
toast.show(room.creatorId === profile.id ? "房间已删除" : "已退出房间");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useAnimation } from "framer-motion";
|
||||
import confetti from "canvas-confetti";
|
||||
import { requestJson } from "@/lib/fetcher";
|
||||
import type { DrawnIdea } from "@/components/BlindboxDrawnHistory";
|
||||
import type { RoomInfo } from "@/hooks/useBlindboxRoom";
|
||||
import type { UserProfile } from "@/types";
|
||||
@@ -78,18 +79,10 @@ export function useBlindboxDraw(
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/draw", {
|
||||
const idea = await requestJson<DrawnIdea, { roomId: string }>("/api/blindbox/draw", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ roomId: room.id }),
|
||||
body: { roomId: room.id },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "抽取失败");
|
||||
}
|
||||
|
||||
const idea = await res.json();
|
||||
setRevealedIdea(idea);
|
||||
setPhase("reveal");
|
||||
setPoolCount((c) => Math.max(0, c - 1));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { useAnimation } from "framer-motion";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { requestJson } from "@/lib/fetcher";
|
||||
import type { MyIdea } from "@/components/BlindboxMyIdeas";
|
||||
import type { DrawnIdea } from "@/components/BlindboxDrawnHistory";
|
||||
import type { RoomInfo } from "@/hooks/useBlindboxRoom";
|
||||
@@ -34,6 +35,30 @@ function pickRandom<T>(arr: T[], n: number): T[] {
|
||||
return shuffled.slice(0, n);
|
||||
}
|
||||
|
||||
interface IdeasResponse {
|
||||
poolCount?: number;
|
||||
myIdeas?: MyIdea[];
|
||||
drawn?: DrawnIdea[];
|
||||
}
|
||||
|
||||
interface SuggestionsResponse {
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
interface IdeaMutationResponse {
|
||||
id: string;
|
||||
tags?: {
|
||||
category: string;
|
||||
timeSlot: string;
|
||||
estimatedMinutes: number;
|
||||
costLevel: string;
|
||||
intensity: string;
|
||||
needsBooking: boolean;
|
||||
searchQuery: string;
|
||||
searchType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useBlindboxIdeas(room: RoomInfo | null, profile: UserProfile | null) {
|
||||
const toast = useToast();
|
||||
const boxControls = useAnimation();
|
||||
@@ -62,13 +87,10 @@ export function useBlindboxIdeas(room: RoomInfo | null, profile: UserProfile | n
|
||||
const fetchIdeas = useCallback(async () => {
|
||||
if (!room || !profile) return;
|
||||
try {
|
||||
const res = await fetch(`/api/blindbox?roomId=${room.id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setPoolCount(data.poolCount ?? 0);
|
||||
setMyIdeas(data.myIdeas ?? []);
|
||||
setDrawnHistory(data.drawn ?? []);
|
||||
}
|
||||
const data = await requestJson<IdeasResponse>(`/api/blindbox?roomId=${room.id}`);
|
||||
setPoolCount(data.poolCount ?? 0);
|
||||
setMyIdeas(data.myIdeas ?? []);
|
||||
setDrawnHistory(data.drawn ?? []);
|
||||
} catch (e) { console.error("fetchIdeas failed:", e); }
|
||||
}, [room, profile]);
|
||||
|
||||
@@ -76,15 +98,13 @@ export function useBlindboxIdeas(room: RoomInfo | null, profile: UserProfile | n
|
||||
if (!room || !profile) return;
|
||||
setSuggestionsLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/blindbox/suggest?roomId=${room.id}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.suggestions?.length > 0) {
|
||||
setSuggestions(data.suggestions);
|
||||
setSuggestionsSource("ai");
|
||||
setSuggestionsLoading(false);
|
||||
return;
|
||||
}
|
||||
const data = await requestJson<SuggestionsResponse>(`/api/blindbox/suggest?roomId=${room.id}`);
|
||||
const suggested = data.suggestions ?? [];
|
||||
if (suggested.length > 0) {
|
||||
setSuggestions(suggested);
|
||||
setSuggestionsSource("ai");
|
||||
setSuggestionsLoading(false);
|
||||
return;
|
||||
}
|
||||
} catch (e) { console.error("fetchSuggestions failed:", e); }
|
||||
setSuggestions(pickRandom(IDEA_INSPIRATIONS, 4));
|
||||
@@ -106,16 +126,10 @@ export function useBlindboxIdeas(room: RoomInfo | null, profile: UserProfile | n
|
||||
setSubmitting(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch("/api/blindbox", {
|
||||
const data = await requestJson<IdeaMutationResponse, { roomId: string; content: string }>("/api/blindbox", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ roomId: room.id, content: text }),
|
||||
body: { roomId: room.id, content: text },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "提交失败");
|
||||
}
|
||||
const data = await res.json();
|
||||
setInput("");
|
||||
setPoolCount((c) => c + 1);
|
||||
setMyIdeas((prev) => [{
|
||||
@@ -153,16 +167,10 @@ export function useBlindboxIdeas(room: RoomInfo | null, profile: UserProfile | n
|
||||
const trimmed = newContent.trim();
|
||||
if (!trimmed || trimmed.length > 200) return;
|
||||
try {
|
||||
const res = await fetch("/api/blindbox", {
|
||||
const data = await requestJson<IdeaMutationResponse, { ideaId: string; content: string }>("/api/blindbox", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ideaId, content: trimmed }),
|
||||
body: { ideaId, content: trimmed },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "编辑失败");
|
||||
}
|
||||
const data = await res.json();
|
||||
setMyIdeas((prev) => prev.map((i) => (i.id === ideaId ? {
|
||||
...i,
|
||||
content: trimmed,
|
||||
@@ -185,15 +193,10 @@ export function useBlindboxIdeas(room: RoomInfo | null, profile: UserProfile | n
|
||||
const handleDeleteIdea = useCallback(async (ideaId: string) => {
|
||||
if (!profile) return;
|
||||
try {
|
||||
const res = await fetch("/api/blindbox", {
|
||||
await requestJson<{ deleted: boolean }, { ideaId: string }>("/api/blindbox", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ideaId }),
|
||||
body: { ideaId },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "删除失败");
|
||||
}
|
||||
setMyIdeas((prev) => prev.filter((i) => i.id !== ideaId));
|
||||
setPoolCount((c) => Math.max(0, c - 1));
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { requestJson } from "@/lib/fetcher";
|
||||
import type { RoomInfo } from "@/hooks/useBlindboxRoom";
|
||||
import type { WeekendPlanData, UserProfile } from "@/types";
|
||||
import type { PendingContract } from "@/components/ContractCompletionModal";
|
||||
@@ -15,6 +16,28 @@ const PLAN_STATUS_STEPS = [
|
||||
"快好了...",
|
||||
];
|
||||
|
||||
interface LatestPlanResponse {
|
||||
plan?: {
|
||||
id: string;
|
||||
days: WeekendPlanData[];
|
||||
endTime?: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface PendingPlansResponse {
|
||||
pending?: PendingContract[];
|
||||
}
|
||||
|
||||
interface GeneratedPlanResponse {
|
||||
id: string;
|
||||
days: WeekendPlanData[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface AcceptPlanResponse {
|
||||
endTime?: string | null;
|
||||
}
|
||||
|
||||
export function useBlindboxPlan(
|
||||
room: RoomInfo | null,
|
||||
profile: UserProfile | null,
|
||||
@@ -46,9 +69,7 @@ export function useBlindboxPlan(
|
||||
const fetchAcceptedPlan = useCallback(async () => {
|
||||
if (!room || !profile) return;
|
||||
try {
|
||||
const res = await fetch(`/api/blindbox/plan?mode=latest&roomId=${room.id}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const data = await requestJson<LatestPlanResponse>(`/api/blindbox/plan?mode=latest&roomId=${room.id}`);
|
||||
if (data.plan) {
|
||||
setActiveContract({
|
||||
id: data.plan.id,
|
||||
@@ -64,9 +85,7 @@ export function useBlindboxPlan(
|
||||
if (!profile) return;
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/plan?mode=pending");
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const data = await requestJson<PendingPlansResponse>("/api/blindbox/plan?mode=pending");
|
||||
if (data.pending?.length) setPendingContracts(data.pending);
|
||||
} catch (e) { console.error("fetchPendingContracts failed:", e); }
|
||||
})();
|
||||
@@ -92,8 +111,7 @@ export function useBlindboxPlan(
|
||||
});
|
||||
n.onclick = () => { window.focus(); n.close(); };
|
||||
}
|
||||
fetch("/api/blindbox/plan?mode=pending")
|
||||
.then((r) => r.json())
|
||||
requestJson<PendingPlansResponse>("/api/blindbox/plan?mode=pending")
|
||||
.then((d) => { if (d.pending?.length) setPendingContracts(d.pending); })
|
||||
.catch((e) => { console.error("refreshPendingContracts failed:", e); });
|
||||
}, ms);
|
||||
@@ -155,16 +173,10 @@ export function useBlindboxPlan(
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/plan", {
|
||||
const data = await requestJson<GeneratedPlanResponse, typeof payload>("/api/blindbox/plan", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
body: payload,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || "生成失败");
|
||||
}
|
||||
const data = await res.json();
|
||||
setPlanId(data.id);
|
||||
setPlanDays(data.days);
|
||||
setPlanAccepted(false);
|
||||
@@ -188,12 +200,13 @@ export function useBlindboxPlan(
|
||||
setActiveContract((prev) => prev ? { ...prev, days: newDays } : prev);
|
||||
}
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/plan", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ planId, action: "update_plan", days: newDays }),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "保存失败");
|
||||
await requestJson<unknown, { planId: string; action: string; days: WeekendPlanData[] }>(
|
||||
"/api/blindbox/plan",
|
||||
{
|
||||
method: "PATCH",
|
||||
body: { planId, action: "update_plan", days: newDays },
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
setPlanDays(prevDays);
|
||||
if (planAccepted) setActiveContract((prev) => prev ? { ...prev, days: prevDays } : prev);
|
||||
@@ -205,13 +218,13 @@ export function useBlindboxPlan(
|
||||
if (!profile || !planDays.length) return;
|
||||
const prevDays = planDays;
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/plan/refine", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ instruction, days: planDays }),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "AI 调整失败");
|
||||
const data = await res.json();
|
||||
const data = await requestJson<{ days: WeekendPlanData[] }, { instruction: string; days: WeekendPlanData[] }>(
|
||||
"/api/blindbox/plan/refine",
|
||||
{
|
||||
method: "POST",
|
||||
body: { instruction, days: planDays },
|
||||
},
|
||||
);
|
||||
await handlePlanDaysChange(data.days);
|
||||
} catch (e) {
|
||||
setPlanDays(prevDays);
|
||||
@@ -226,15 +239,13 @@ export function useBlindboxPlan(
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/plan", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ planId, action: "accept" }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || "接受契约失败");
|
||||
}
|
||||
const data = await requestJson<AcceptPlanResponse, { planId: string; action: string }>(
|
||||
"/api/blindbox/plan",
|
||||
{
|
||||
method: "PATCH",
|
||||
body: { planId, action: "accept" },
|
||||
},
|
||||
);
|
||||
|
||||
setPlanAccepted(true);
|
||||
setActiveContract({ id: planId, days: planDays, endTime: data.endTime ?? null });
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||
import { requestJson } from "@/lib/fetcher";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useShare } from "@/hooks/useShare";
|
||||
import type { UserProfile } from "@/types";
|
||||
@@ -55,9 +56,7 @@ export function useBlindboxRoom(code: string) {
|
||||
const fetchRoom = useCallback(async () => {
|
||||
if (!code) return;
|
||||
try {
|
||||
const res = await fetch(`/api/blindbox/room/${code}`);
|
||||
if (!res.ok) { router.replace("/blindbox"); return; }
|
||||
const data: RoomInfo = await res.json();
|
||||
const data = await requestJson<RoomInfo>(`/api/blindbox/room/${code}`);
|
||||
setRoom(data);
|
||||
const p = getCachedProfile();
|
||||
setIsMember(data.members.some((m) => m.id === p?.id));
|
||||
@@ -74,12 +73,12 @@ export function useBlindboxRoom(code: string) {
|
||||
if (joiningRoom || !profile || !room) return;
|
||||
setJoiningRoom(true);
|
||||
try {
|
||||
const res = await fetch("/api/blindbox/room/join", {
|
||||
await requestJson<{ id: string; code: string; name: string }>("/api/blindbox/room/join", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ code }),
|
||||
body: { code },
|
||||
});
|
||||
if (res.ok) { setIsMember(true); fetchRoom(); }
|
||||
setIsMember(true);
|
||||
fetchRoom();
|
||||
} catch (e) { console.error("handleJoinRoom failed:", e); }
|
||||
finally { setJoiningRoom(false); }
|
||||
};
|
||||
@@ -92,16 +91,15 @@ export function useBlindboxRoom(code: string) {
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 10000 }),
|
||||
);
|
||||
const { latitude: lat, longitude: lng } = pos.coords;
|
||||
const regeoRes = await fetch(`/api/location/regeo?lat=${lat}&lng=${lng}`);
|
||||
const regeo = regeoRes.ok ? await regeoRes.json() : {};
|
||||
const regeo = await requestJson<{ name?: string; formatted?: string }>(
|
||||
`/api/location/regeo?lat=${lat}&lng=${lng}`,
|
||||
);
|
||||
const cityName = regeo.name || "未知位置";
|
||||
const addressLabel = regeo.formatted || cityName;
|
||||
const patchRes = await fetch(`/api/blindbox/room/${room.code}`, {
|
||||
await requestJson(`/api/blindbox/room/${room.code}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ city: cityName, address: addressLabel, lat, lng }),
|
||||
body: { city: cityName, address: addressLabel, lat, lng },
|
||||
});
|
||||
if (!patchRes.ok) throw new Error("保存位置失败");
|
||||
setRoom((prev) => prev ? { ...prev, city: cityName, address: addressLabel, lat, lng } : prev);
|
||||
toast.show("位置已设置");
|
||||
} catch {
|
||||
@@ -120,14 +118,9 @@ export function useBlindboxRoom(code: string) {
|
||||
if (leaving || !profile || !room) return;
|
||||
setLeaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/blindbox/room/${room.code}`, {
|
||||
await requestJson<{ action: "deleted" | "left" }>(`/api/blindbox/room/${room.code}`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "操作失败");
|
||||
}
|
||||
router.push("/blindbox");
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
|
||||
@@ -12,6 +12,89 @@ export class FetchError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public payload?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiRequestError";
|
||||
}
|
||||
}
|
||||
|
||||
function extractErrorMessage(payload: unknown): string | null {
|
||||
if (
|
||||
payload &&
|
||||
typeof payload === "object" &&
|
||||
"error" in payload &&
|
||||
typeof payload.error === "string"
|
||||
) {
|
||||
return payload.error;
|
||||
}
|
||||
if (typeof payload === "string" && payload.trim()) {
|
||||
return payload;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function parsePayload(res: Response): Promise<unknown> {
|
||||
if (res.status === 204) return undefined;
|
||||
if (typeof res.text === "function") {
|
||||
const text = await res.text();
|
||||
if (!text) return undefined;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
if (typeof res.json === "function") {
|
||||
try {
|
||||
return await res.json();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type RequestJsonInit<TBody> = Omit<RequestInit, "body"> & {
|
||||
body?: TBody;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared JSON requester for imperative client-side API calls.
|
||||
* - Parses JSON/text payloads automatically
|
||||
* - Throws ApiRequestError with normalized message on non-2xx
|
||||
*/
|
||||
export async function requestJson<TResponse = unknown, TBody = unknown>(
|
||||
url: string,
|
||||
init: RequestJsonInit<TBody> = {},
|
||||
): Promise<TResponse> {
|
||||
const { body, headers, ...rest } = init;
|
||||
const hasBody = body !== undefined;
|
||||
|
||||
const mergedHeaders = new Headers(headers);
|
||||
if (hasBody && !mergedHeaders.has("Content-Type")) {
|
||||
mergedHeaders.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
...rest,
|
||||
headers: mergedHeaders,
|
||||
body: hasBody ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const payload = await parsePayload(res);
|
||||
if (!res.ok) {
|
||||
const message = extractErrorMessage(payload) ?? "请求失败";
|
||||
throw new ApiRequestError(message, res.status, payload);
|
||||
}
|
||||
|
||||
return payload as TResponse;
|
||||
}
|
||||
|
||||
export async function fetcher<T = unknown>(url: string): Promise<T> {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
|
||||
Reference in New Issue
Block a user