feat(web): integrate session management and improve API error handling

This commit is contained in:
2026-03-11 17:19:14 +08:00
parent f1932676be
commit 168a5b5613
5 changed files with 88 additions and 76 deletions

View File

@@ -1,12 +1,67 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { toast } from "@heroui/react";
import { signOut, useSession } from "@/lib/auth-client";
import { APIError } from "@/lib/api";
/** 监听 better-auth 会话变为 null服务端过期/异地登出等场景)*/
function SessionMonitor({ onExpired }: { onExpired: () => void }) {
const { data: session, isPending } = useSession();
const wasLoggedIn = useRef(false);
useEffect(() => {
if (isPending) return;
if (session) {
wasLoggedIn.current = true;
} else if (wasLoggedIn.current) {
wasLoggedIn.current = false;
onExpired();
}
}, [session, isPending, onExpired]);
return null;
}
export function ReactQueryProvider({ children }: { children: React.ReactNode }) {
const router = useRouter();
const isHandling = useRef(false);
const handleExpired = useCallback(async () => {
if (isHandling.current) return;
isHandling.current = true;
try {
await signOut({ fetchOptions: { credentials: "include" } });
} catch {
// ignore sign-out errors
}
toast.warning("登录已过期,请重新登录");
router.push("/login");
}, [router]);
// Stable ref so the QueryClient (created once) always calls the latest handler
const handleExpiredRef = useRef(handleExpired);
handleExpiredRef.current = handleExpired;
const [queryClient] = useState(
() =>
new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
if (error instanceof APIError && error.status === 401) {
handleExpiredRef.current();
}
},
}),
mutationCache: new MutationCache({
onError: (error) => {
if (error instanceof APIError && error.status === 401) {
handleExpiredRef.current();
}
},
}),
defaultOptions: {
queries: {
staleTime: 10_000,
@@ -14,5 +69,11 @@ export function ReactQueryProvider({ children }: { children: React.ReactNode })
},
}),
);
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
return (
<QueryClientProvider client={queryClient}>
<SessionMonitor onExpired={handleExpired} />
{children}
</QueryClientProvider>
);
}