Your Name 40848ffdb5
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: stabilize dingtalk login and image sizing
2026-05-11 14:24:34 +08:00

704 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { useActionState, useEffect, useState, useRef, useCallback } from "react";
import { useTheme } from "next-themes";
import { Moon, Sun, X } from "lucide-react";
import { signIn } from "next-auth/react"; // Import signIn
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { AuthForm } from "@/components/auth-form";
import { SubmitButton } from "@/components/submit-button";
import { toast } from "@/components/toast";
import { type LoginActionState, login } from "../actions";
import { Button } from "@/components/ui/button";
import { AntigravityBackground } from "@/components/antigravity-background";
export default function Page() {
const [email, setEmail] = useState("");
const [isSuccessful, setIsSuccessful] = useState(false);
const [siteName, setSiteName] = useState("AI Chatbot");
const [siteLogo, setSiteLogo] = useState<string | null>(null);
const [siteLogoDark, setSiteLogoDark] = useState<string | null>(null);
const [isDingTalkEnabled, setIsDingTalkEnabled] = useState(false);
const [isGoogleEnabled, setIsGoogleEnabled] = useState(false);
const [isGoogleConfigured, setIsGoogleConfigured] = useState(false);
const [dingTalkAppKey, setDingTalkAppKey] = useState<string | null>(null);
const [dingTalkOrganizations, setDingTalkOrganizations] = useState<string[]>([]);
const [isRegistrationEnabled, setIsRegistrationEnabled] = useState(true);
const [showDingTalkModal, setShowDingTalkModal] = useState(false);
const [dingTalkIframeSrc, setDingTalkIframeSrc] = useState("");
const [showOrgMenu, setShowOrgMenu] = useState(false);
const { setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const [isHandlingToken, setIsHandlingToken] = useState(false);
const [isCheckingExistingSession, setIsCheckingExistingSession] = useState(true);
// 系统通知状态
const [notification, setNotification] = useState<{ enabled: boolean; title: string; content: string } | null>(null);
const [showNotification, setShowNotification] = useState(false);
const loginRedirectTimerRef = useRef<number | null>(null);
const loginRedirectFallbackTimerRef = useRef<number | null>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
// 声明 formAction 在前面
const [state, formAction] = useActionState<LoginActionState, FormData>(
login,
{
status: "idle",
}
);
useEffect(() => {
let cancelled = false;
fetch("/api/auth/validate-session", {
cache: "no-store",
credentials: "include",
})
.then((res) => res.json())
.then((data) => {
if (cancelled) return;
if (data?.valid === true && data?.userType !== "guest") {
window.location.replace("/");
return;
}
setIsCheckingExistingSession(false);
})
.catch(() => {
if (!cancelled) {
setIsCheckingExistingSession(false);
}
});
return () => {
cancelled = true;
};
}, []);
// 使用 token 完成钉钉登录
const completeDingTalkLogin = useCallback(async (token: string) => {
console.log("[DingTalk] Completing login with token:", token.substring(0, 10) + "...");
setIsHandlingToken(true);
try {
// 获取 CSRF token
const csrfRes = await fetch("/api/auth/csrf");
const { csrfToken } = await csrfRes.json();
console.log("[DingTalk] Got CSRF token");
// 手动构造登录请求
const loginRes = await fetch("/api/auth/callback/credentials", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken,
token,
callbackUrl: "/",
}),
credentials: "include", // 重要:确保 cookie 被发送和接收
});
console.log("[DingTalk] Login response status:", loginRes.status);
console.log("[DingTalk] Login response headers:", Object.fromEntries(loginRes.headers.entries()));
// 检查响应类型
const contentType = loginRes.headers.get("content-type");
console.log("[DingTalk] Content-Type:", contentType);
if (loginRes.redirected || loginRes.status === 302) {
// 登录成功,被重定向
console.log("[DingTalk] Login successful (redirected to:", loginRes.url, ")");
// 等待一小段时间确保 cookie 被设置
await new Promise(resolve => setTimeout(resolve, 300));
// 验证 session 是否创建成功
const sessionRes = await fetch("/api/auth/session");
const session = await sessionRes.json();
console.log("[DingTalk] Session after login:", session);
if (session && session.user) {
console.log("[DingTalk] Session validated, redirecting to home...");
setIsSuccessful(true);
// 使用 replace 而不是 href,避免在历史记录中留下登录页
// 增加短暂延迟确保 cookie 完全同步
setTimeout(() => {
window.location.replace("/");
}, 200);
} else {
console.error("[DingTalk] Session not created after login");
setIsHandlingToken(false);
toast({ type: "error", description: "登录失败:无法创建会话" });
}
} else if (contentType?.includes("application/json")) {
const result = await loginRes.json();
console.log("[DingTalk] Login result:", result);
if (result.url || loginRes.ok) {
// 登录成功
console.log("[DingTalk] Login successful");
// 等待一小段时间确保 cookie 被设置
await new Promise(resolve => setTimeout(resolve, 300));
// 验证 session
const sessionRes = await fetch("/api/auth/session");
const session = await sessionRes.json();
console.log("[DingTalk] Session after login:", session);
if (session && session.user) {
console.log("[DingTalk] Session validated, redirecting...");
setIsSuccessful(true);
// 使用 replace 而不是 href,避免在历史记录中留下登录页
// 增加短暂延迟确保 cookie 完全同步
setTimeout(() => {
window.location.replace(result.url || "/");
}, 200);
} else {
console.error("[DingTalk] Session not created");
setIsHandlingToken(false);
toast({ type: "error", description: "登录失败:无法创建会话" });
}
} else {
// 登录失败
console.error("[DingTalk] Login failed:", result);
setIsHandlingToken(false);
toast({ type: "error", description: result.error || "登录失败,请重试" });
}
} else {
// 未知响应
const responseText = await loginRes.text();
console.error("[DingTalk] Unexpected response type. Body:", responseText.substring(0, 200));
setIsHandlingToken(false);
toast({ type: "error", description: "登录失败:服务器响应异常" });
}
} catch (error) {
console.error("[DingTalk] Login error:", error);
setIsHandlingToken(false);
toast({ type: "error", description: "登录发生错误:" + (error instanceof Error ? error.message : String(error)) });
}
}, []);
// Chunk 加载错误自动恢复
useEffect(() => {
const handleChunkError = (event: ErrorEvent) => {
const target = event.target as any;
if (target?.src?.includes("/_next/static/chunks/") || event.message?.includes("Failed to load chunk")) {
console.error("[ChunkError] 检测到chunk加载失败,3秒后自动刷新页面");
// 3秒后自动刷新页面(清除缓存)
setTimeout(() => {
window.location.reload();
}, 3000);
}
};
window.addEventListener("error", handleChunkError, true);
return () => window.removeEventListener("error", handleChunkError, true);
}, []);
// 初始化设置
useEffect(() => {
setMounted(true);
// 测试标记 - 确认代码已更新
console.log("=".repeat(50));
console.log("🔔 登录页面代码版本: 2024-12-10 v3.0");
console.log("=".repeat(50));
// 获取网站设置(添加时间戳防止缓存)
fetch(`/api/site-settings?scope=auth&t=${Date.now()}`)
.then((res) => res.json())
.then((data) => {
console.log("[Login] Site settings data:", data);
if (data.siteName) setSiteName(data.siteName);
if (data.siteLogo) setSiteLogo(data.siteLogo);
if (data.siteLogoDark) setSiteLogoDark(data.siteLogoDark);
// 处理系统通知
console.log("[Login] Notification data:", data.notification);
if (data.notification && data.notification.enabled) {
console.log("[Login] Setting notification and showing dialog");
setNotification(data.notification);
// 每次访问登录页都显示通知(重要公告)
setShowNotification(true);
} else {
console.log("[Login] Notification not enabled or missing");
}
})
.catch((err) => {
console.error("[Login] Failed to fetch site settings:", err);
});
// 获取认证设置
fetch("/api/auth-settings")
.then((res) => res.json())
.then((data) => {
if (data.isDingTalkEnabled) setIsDingTalkEnabled(true);
if (data.isGoogleEnabled) setIsGoogleEnabled(true);
if (data.isGoogleConfigured) setIsGoogleConfigured(true);
if (data.dingTalkAppKey) setDingTalkAppKey(data.dingTalkAppKey);
const rawOrganizations = Array.isArray(data.organizations)
? data.organizations
: Array.isArray(data.dingTalkOrganizations)
? data.dingTalkOrganizations
: [];
if (rawOrganizations.length > 0) {
setDingTalkOrganizations(
rawOrganizations
.map((org: { name?: string } | string) =>
typeof org === "string" ? org : org.name,
)
.filter((name: string | undefined): name is string => Boolean(name?.trim()))
.map((name: string) => name.trim()),
);
}
if (data.isRegistrationEnabled !== undefined) {
setIsRegistrationEnabled(data.isRegistrationEnabled);
}
})
.catch(console.error);
}, []);
const handleGoogleLogin = async () => {
if (!isGoogleEnabled) {
toast({ type: "error", description: "Google 登录未启用" });
return;
}
if (!isGoogleConfigured) {
toast({ type: "error", description: "Google 登录未完成服务器配置" });
return;
}
await signIn("google", { callbackUrl: "/" });
};
// 检查 URL 中是否有钉钉 Token(页面加载时自动登录)
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const token = params.get("dingtalk_token");
if (token) {
completeDingTalkLogin(token);
}
}, [completeDingTalkLogin]);
// 监听来自钉钉回调页面的消息
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
console.log("[DingTalk] Received message:", event.data);
if (event.data && event.data.type === "dingtalk_login_success") {
setShowDingTalkModal(false);
setDingTalkIframeSrc("");
// 使用返回的 token 完成 Auth.js 登录
if (event.data.token) {
toast({ type: "success", description: "钉钉认证成功,正在登录..." });
completeDingTalkLogin(event.data.token);
} else {
toast({ type: "error", description: "钉钉登录失败:未获取到登录凭证" });
}
} else if (event.data && event.data.type === "dingtalk_login_error") {
setShowDingTalkModal(false);
setDingTalkIframeSrc("");
toast({ type: "error", description: "钉钉登录失败:" + (event.data.message || "未知错误") });
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [completeDingTalkLogin]);
// 处理登录状态变化
useEffect(() => {
if (loginRedirectTimerRef.current) {
window.clearTimeout(loginRedirectTimerRef.current);
loginRedirectTimerRef.current = null;
}
if (loginRedirectFallbackTimerRef.current) {
window.clearTimeout(loginRedirectFallbackTimerRef.current);
loginRedirectFallbackTimerRef.current = null;
}
if (state.status === "failed") {
setIsSuccessful(false);
setIsHandlingToken(false);
toast({
type: "error",
description: "邮箱或密码不正确",
});
} else if (state.status === "invalid_data") {
setIsSuccessful(false);
setIsHandlingToken(false);
toast({
type: "error",
description: "提交的信息校验失败",
});
} else if (state.status === "success") {
toast({ type: "success", description: "登录成功,正在进入首页" });
setIsSuccessful(true);
loginRedirectTimerRef.current = window.setTimeout(() => {
window.location.replace("/");
}, 120);
loginRedirectFallbackTimerRef.current = window.setTimeout(() => {
window.location.assign("/");
}, 1600);
}
}, [state.status]);
const handleSubmit = (formData: FormData) => {
setIsSuccessful(false);
setEmail(formData.get("email") as string);
formAction(formData);
};
useEffect(() => {
return () => {
if (loginRedirectTimerRef.current) {
window.clearTimeout(loginRedirectTimerRef.current);
}
if (loginRedirectFallbackTimerRef.current) {
window.clearTimeout(loginRedirectFallbackTimerRef.current);
}
};
}, []);
const getSafeLoginCallbackUrl = () => {
if (typeof window === "undefined") return "/";
const callbackUrl = new URLSearchParams(window.location.search).get(
"callbackUrl",
);
if (
callbackUrl &&
callbackUrl.startsWith("/") &&
!callbackUrl.startsWith("//")
) {
return callbackUrl;
}
return "/";
};
const buildDingTalkLoginUrl = (orgName?: string) => {
const params = new URLSearchParams();
if (orgName) params.set("org", orgName);
const callbackUrl = getSafeLoginCallbackUrl();
if (callbackUrl !== "/") {
params.set("callbackUrl", callbackUrl);
}
const query = params.toString();
return `/api/auth/dingtalk/login${query ? `?${query}` : ""}`;
};
// 根据组织名称启动钉钉登录。钉钉授权页禁止第三方 iframe 嵌入,必须使用顶层跳转。
const startDingTalkLoginForOrg = (orgName: string) => {
// 清除 URL 中的 token 参数,避免干扰
if (
typeof window !== "undefined" &&
window.location.search.includes("dingtalk_token")
) {
const newUrl = window.location.pathname;
window.history.replaceState({}, "", newUrl);
}
const loginUrl = buildDingTalkLoginUrl(orgName);
console.log("[DingTalk] Redirecting org login:", orgName, loginUrl);
setShowDingTalkModal(true);
setShowOrgMenu(false);
window.location.assign(loginUrl);
};
// 钉钉登录 - 支持多组织
const handleDingTalkLogin = () => {
if (!isDingTalkEnabled) {
toast({ type: "error", description: "钉钉登录未启用" });
return;
}
// 没有配置多组织:回退到单一 appKey 流程
if (dingTalkOrganizations.length === 0) {
if (!dingTalkAppKey) {
toast({ type: "error", description: "钉钉登录未正确配置" });
return;
}
if (
typeof window === "undefined" ||
typeof window.location === "undefined"
) {
toast({ type: "error", description: "当前环境不支持钉钉登录" });
return;
}
// 清除 URL 中的 token 参数,避免干扰
if (window.location.search.includes("dingtalk_token")) {
const newUrl = window.location.pathname;
window.history.replaceState({}, "", newUrl);
}
const loginUrl = buildDingTalkLoginUrl();
console.log("[DingTalk] Redirecting OAuth login:", loginUrl);
window.location.assign(loginUrl);
return;
}
// 只有一个组织:直接使用该组织登录
if (dingTalkOrganizations.length === 1) {
startDingTalkLoginForOrg(dingTalkOrganizations[0] as string);
return;
}
// 多组织:直接打开弹窗中的组织列表,避免悬浮菜单被页面层级覆盖
setDingTalkIframeSrc("");
setShowDingTalkModal(true);
setShowOrgMenu(false);
};
// 关闭钉钉登录弹窗
const closeDingTalkModal = () => {
setShowDingTalkModal(false);
setDingTalkIframeSrc("");
};
// 决定显示哪个 LOGO
const displayLogo = mounted
? (resolvedTheme === "dark" && siteLogoDark ? siteLogoDark : siteLogo)
: siteLogo;
const shouldInvertLogo =
mounted &&
resolvedTheme === "dark" &&
!siteLogoDark?.trim() &&
Boolean(siteLogo?.trim());
const hasThirdPartyLogin =
(isDingTalkEnabled && (Boolean(dingTalkAppKey) || dingTalkOrganizations.length > 0)) ||
(isGoogleEnabled && isGoogleConfigured);
if (isCheckingExistingSession) {
return (
<div className="flex min-h-screen items-center justify-center text-muted-foreground text-sm">
正在检查登录状态...
</div>
);
}
return (
<div className="flex h-dvh w-screen items-start justify-center bg-background pt-12 md:items-center md:pt-0 relative overflow-hidden">
{/* Antigravity 动画背景 */}
<AntigravityBackground />
{/* 主题切换按钮 */}
<div className="absolute top-4 right-4 z-10">
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
>
{mounted && resolvedTheme === "dark" ? (
<Moon className="h-5 w-5" />
) : (
<Sun className="h-5 w-5" />
)}
</Button>
</div>
<div className="flex w-full max-w-md flex-col gap-12 overflow-hidden rounded-2xl relative z-10 pointer-events-auto">
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16 backdrop-blur-sm bg-background/80 rounded-2xl p-6 pointer-events-auto">
{displayLogo ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={displayLogo}
alt={siteName}
className={`mb-2 h-12 w-auto object-contain ${shouldInvertLogo ? "brightness-0 invert" : ""}`}
/>
) : null}
<h3 className="font-semibold text-xl dark:text-zinc-50">登录 {siteName}</h3>
<p className="text-gray-500 text-sm dark:text-zinc-400">
使用邮箱与密码进入系统
</p>
</div>
<AuthForm action={handleSubmit} defaultEmail={email} className="pointer-events-auto">
<SubmitButton isSuccessful={isSuccessful}>登录</SubmitButton>
{hasThirdPartyLogin && (
<div className="mt-4 w-full relative">
<div className="relative mb-4">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
或使用第三方登录
</span>
</div>
</div>
<div className="space-y-2">
{isDingTalkEnabled && (dingTalkAppKey || dingTalkOrganizations.length > 0) ? (
<button
type="button"
onClick={handleDingTalkLogin}
className="flex w-full items-center justify-center gap-2 rounded-md border bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
<svg style={{ width: "20px", height: "20px" }} viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path
d="M512 2C230.2 2 2 230.2 2 512s228.2 510 510 510 510-228.2 510-510S793.3 2 512 2z m235.9 442c-1 4.6-3.6 10.8-7.2 19.1l-0.5 0.5c-21.6 45.8-77.3 135.5-77.3 135.5l-0.5-0.5-16.5 28.3h78.8L574.3 826.8l34-136h-61.8l21.6-90.2c-17.5 4.1-38.1 9.8-62.3 18 0 0-33 19.1-94.8-37.1 0 0-41.7-37.1-17.5-45.8 10.3-4.1 50-8.8 81.4-12.9 42.2-5.7 68.5-8.8 68.5-8.8s-130.3 2.1-161.2-3.1c-30.9-4.6-70.1-56.7-78.3-102 0 0-12.9-24.7 27.8-12.9 40.2 11.8 209.2 45.8 209.2 45.8S321.4 375 307 358.5c-14.4-16.5-42.8-89.6-39.2-134.5 0 0 1.5-11.3 12.9-8.2 0 0 161.8 74.2 272.5 114.4C664.5 371.4 760.8 392 747.9 444z"
fill="#3296FA"
/>
</svg>
钉钉登录
</button>
) : null}
{isGoogleEnabled && isGoogleConfigured ? (
<button
type="button"
onClick={handleGoogleLogin}
className="flex w-full items-center justify-center gap-2 rounded-md border bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
<path fill="#EA4335" d="M12 10.2v3.9h5.5c-.2 1.2-1.4 3.5-5.5 3.5-3.3 0-6-2.7-6-6s2.7-6 6-6c1.9 0 3.2.8 3.9 1.5l2.7-2.6C16.9 2.9 14.6 2 12 2 6.5 2 2 6.5 2 12s4.5 10 10 10c5.8 0 9.6-4.1 9.6-9.8 0-.7-.1-1.2-.2-1.7H12z" />
</svg>
Google 登录
</button>
) : null}
</div>
{/* 多组织下的小型选择框(悬浮在按钮附近) */}
{dingTalkOrganizations.length > 1 && showOrgMenu && (
<div className="absolute left-0 right-0 -top-2 -translate-y-full rounded-md border bg-background shadow-lg z-20 max-h-60 overflow-auto text-sm">
{dingTalkOrganizations.map((org) => (
<button
key={org}
type="button"
onClick={() => startDingTalkLoginForOrg(org)}
className="flex w-full items-center justify-between px-3 py-2 text-left hover:bg-muted"
>
<span>{org}</span>
</button>
))}
</div>
)}
</div>
)}
{isRegistrationEnabled && (
<p className="mt-4 text-center text-gray-600 text-sm dark:text-zinc-400">
还没有账号?
<Link
className="font-semibold text-gray-800 hover:underline dark:text-zinc-200"
href="/register"
>
立即注册
</Link>
,即可免费体验。
</p>
)}
</AuthForm>
</div>
{/* 钉钉登录 iframe 弹窗 / 多组织选择 */}
{showDingTalkModal && (
<div
className="fixed inset-0 bg-black/85 flex items-center justify-center z-50 backdrop-blur-sm"
onClick={(e) => {
if (e.target === e.currentTarget) closeDingTalkModal();
}}
>
<div className="w-[90%] max-w-[500px] h-[720px] bg-white dark:bg-zinc-900 rounded-xl overflow-hidden flex flex-col shadow-2xl">
<div className="flex justify-between items-center p-4 bg-gray-50 dark:bg-zinc-800 border-b dark:border-zinc-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
钉钉登录
</h3>
<button
onClick={closeDingTalkModal}
className="p-2 hover:bg-gray-200 dark:hover:bg-zinc-700 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-gray-500 dark:text-gray-400" />
</button>
</div>
{/* 多组织选择 */}
{dingTalkOrganizations.length > 1 && !dingTalkIframeSrc && (
<div className="p-4 space-y-3">
<p className="text-sm text-muted-foreground">
请选择要使用的钉钉组织进行登录:
</p>
<div className="space-y-2">
{dingTalkOrganizations.map((org) => (
<button
key={org}
type="button"
onClick={() => startDingTalkLoginForOrg(org)}
className="flex w-full items-center justify-between rounded-md border bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
<span>{org}</span>
</button>
))}
</div>
<p className="text-xs text-muted-foreground">
选择后将跳转到钉钉完成授权。
</p>
</div>
)}
{/* 登录 iframe */}
{dingTalkIframeSrc && (
<iframe
ref={iframeRef}
src={dingTalkIframeSrc}
className="flex-1 w-full border-0"
title="钉钉登录"
/>
)}
<div className="p-4 bg-gray-50 dark:bg-zinc-800 border-t dark:border-zinc-700 flex justify-center">
<button
onClick={closeDingTalkModal}
className="px-4 py-2 bg-gray-200 dark:bg-zinc-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-zinc-600 transition-colors"
>
返回登录
</button>
</div>
</div>
</div>
)}
{/* 系统通知弹窗 */}
<Dialog open={showNotification} onOpenChange={setShowNotification}>
<DialogContent className="sm:max-w-[425px] bg-background">
<DialogHeader>
<DialogTitle>{notification?.title || "系统通知"}</DialogTitle>
</DialogHeader>
<div className="whitespace-pre-wrap text-sm text-muted-foreground mt-4 max-h-[60vh] overflow-y-auto">
{notification?.content}
</div>
</DialogContent>
</Dialog>
{/* 登录中 Loading 状态 */}
{isHandlingToken && (
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center backdrop-blur-sm">
<div className="bg-white dark:bg-zinc-800 p-8 rounded-xl shadow-2xl flex flex-col items-center">
<div className="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mb-4"></div>
<p className="text-lg font-medium text-gray-900 dark:text-white">正在完成登录...</p>
</div>
</div>
)}
</div>
);
}