"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(null); const [siteLogoDark, setSiteLogoDark] = useState(null); const [isDingTalkEnabled, setIsDingTalkEnabled] = useState(false); const [dingTalkAppKey, setDingTalkAppKey] = useState(null); const [dingTalkOrganizations, setDingTalkOrganizations] = useState([]); 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 [notification, setNotification] = useState<{ enabled: boolean; title: string; content: string } | null>(null); const [showNotification, setShowNotification] = useState(false); const iframeRef = useRef(null); // 声明 formAction 在前面 const [state, formAction] = useActionState( login, { status: "idle", } ); // 使用 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?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.dingTalkAppKey) setDingTalkAppKey(data.dingTalkAppKey); if (Array.isArray(data.organizations)) { setDingTalkOrganizations( data.organizations .map((org: { name?: string }) => org.name) .filter((name: string | undefined): name is string => !!name), ); } if (data.isRegistrationEnabled !== undefined) { setIsRegistrationEnabled(data.isRegistrationEnabled); } }) .catch(console.error); }, []); // 检查 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 (state.status === "failed") { setIsHandlingToken(false); toast({ type: "error", description: "邮箱或密码不正确", }); } else if (state.status === "invalid_data") { setIsHandlingToken(false); toast({ type: "error", description: "提交的信息校验失败", }); } else if (state.status === "success") { setIsSuccessful(true); window.location.href = "/"; } }, [state.status]); const handleSubmit = (formData: FormData) => { setEmail(formData.get("email") as string); formAction(formData); }; // 根据组织名称启动钉钉登录(在 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 = `/api/auth/dingtalk/login?org=${encodeURIComponent( orgName, )}`; console.log("[DingTalk] Opening org login:", orgName, loginUrl); setDingTalkIframeSrc(loginUrl); setShowDingTalkModal(true); setShowOrgMenu(false); }; // 钉钉登录 - 支持多组织 const handleDingTalkLogin = () => { if (!isDingTalkEnabled) { toast({ type: "error", description: "钉钉登录未启用" }); return; } // 没有配置多组织:回退到单一 appKey 流程 if (dingTalkOrganizations.length === 0) { if (!dingTalkAppKey) { toast({ type: "error", description: "钉钉登录未正确配置" }); return; } // 兼容:直接使用旧的方式在 iframe 中打开 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 param = { redirect_uri: encodeURIComponent( window.location.origin + "/dingtalk-callback.html", ), client_id: dingTalkAppKey, scope: "openid", response_type: "code", state: "default", prompt: "consent", }; const redirectUrl = "https://login.dingtalk.com/oauth2/auth?" + Object.keys(param) .map((key) => key + "=" + (param as any)[key]) .join("&"); console.log("[DingTalk] Opening OAuth URL (legacy):", redirectUrl); setDingTalkIframeSrc(redirectUrl); setShowDingTalkModal(true); return; } // 只有一个组织:直接使用该组织登录 if (dingTalkOrganizations.length === 1) { startDingTalkLoginForOrg(dingTalkOrganizations[0] as string); return; } // 多组织:点击时展开/收起选择菜单(hover 时也会展开) setShowOrgMenu((prev) => !prev); }; // 关闭钉钉登录弹窗 const closeDingTalkModal = () => { setShowDingTalkModal(false); setDingTalkIframeSrc(""); }; // 决定显示哪个 LOGO const displayLogo = mounted ? (resolvedTheme === "dark" && siteLogoDark ? siteLogoDark : siteLogo) : siteLogo; return (
{/* Antigravity 动画背景 */} {/* 主题切换按钮 */}
{displayLogo ? ( // eslint-disable-next-line @next/next/no-img-element {siteName} ) : null}

登录 {siteName}

使用邮箱与密码进入系统

登录 {isDingTalkEnabled && (dingTalkAppKey || dingTalkOrganizations.length > 0) && (
或使用第三方登录
{/* 多组织下的小型选择框(悬浮在按钮附近) */} {dingTalkOrganizations.length > 1 && showOrgMenu && (
{dingTalkOrganizations.map((org) => ( ))}
)}
)} {isRegistrationEnabled && (

还没有账号? 立即注册 ,即可免费体验。

)}
{/* 钉钉登录 iframe 弹窗 / 多组织选择 */} {showDingTalkModal && (
{ if (e.target === e.currentTarget) closeDingTalkModal(); }} >

钉钉登录

{/* 多组织选择 */} {dingTalkOrganizations.length > 1 && !dingTalkIframeSrc && (

请选择要使用的钉钉组织进行登录:

{dingTalkOrganizations.map((org) => ( ))}

选择后将在下方加载钉钉登录页面。

)} {/* 登录 iframe */} {dingTalkIframeSrc && (