"use client"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { useI18nHydrated } from "@/components/i18n-hydration-provider"; import { useHydrationSafeEntryT } from "@/i18n/hydration-t"; import { toast } from "sonner"; import { getPlayerMe } from "@/api/player"; import { getPlayerAuthCaptcha, postPlayerAuthLogin } from "@/api/player-auth"; import { getPublicCurrencies } from "@/api/currency"; import { LanguageSwitcher } from "@/components/language-switcher"; import { EntryHeroBanner } from "@/features/player/entry-hero-banner"; import { PlayerLoginDesktop } from "@/features/player/player-login-desktop"; import { PlayerLoginForm } from "@/features/player/player-login-form"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import { validatePlayerLoginPassword, validatePlayerLoginUsername, } from "@/lib/player-input-validation"; import { LotteryApiBizError } from "@/types/api/errors"; import "@/i18n"; function stripSearchParamFromBrowserUrl(name: string): void { if (typeof window === "undefined") return; const url = new URL(window.location.href); if (!url.searchParams.has(name)) return; url.searchParams.delete(name); const next = `${url.pathname}${url.search}${url.hash}`; window.history.replaceState(null, "", next); } export function PlayerLoginScreen(): React.ReactElement { const i18nHydrated = useI18nHydrated(); const t = useHydrationSafeEntryT(); const tRef = useRef(t); useEffect(() => { tRef.current = t; }, [t]); const router = useRouter(); const searchParams = useSearchParams(); const setBearerToken = usePlayerSessionStore((s) => s.setBearerToken); const setProfile = usePlayerSessionStore((s) => s.setProfile); const clearBearerToken = usePlayerSessionStore((s) => s.clearBearerToken); const sessionStatusHandled = useRef(false); const passwordChangedHandled = useRef(false); const captchaInitialized = useRef(false); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [captchaCode, setCaptchaCode] = useState(""); const [captchaKey, setCaptchaKey] = useState(null); const [captchaSrc, setCaptchaSrc] = useState(null); const [loadingCaptcha, setLoadingCaptcha] = useState(false); const [loading, setLoading] = useState(false); const loadCaptcha = useCallback(async () => { setLoadingCaptcha(true); try { const data = await getPlayerAuthCaptcha(); setCaptchaKey(data.captcha_key); setCaptchaSrc(`data:image/svg+xml;base64,${data.image_base64}`); setCaptchaCode(""); } catch { toast.error(tRef.current("login.captchaLoadFailed")); setCaptchaKey(null); setCaptchaSrc(null); } finally { setLoadingCaptcha(false); } }, []); useEffect(() => { if (captchaInitialized.current) return; captchaInitialized.current = true; void loadCaptcha(); }, [loadCaptcha]); useEffect(() => { if (!i18nHydrated) return; if (sessionStatusHandled.current) return; const sessionStatus = searchParams.get("session"); if (sessionStatus !== "expired" && sessionStatus !== "replaced") return; sessionStatusHandled.current = true; clearBearerToken(); toast.error( t( sessionStatus === "replaced" ? "errors.sessionReplaced" : "errors.sessionExpired", ), ); stripSearchParamFromBrowserUrl("session"); }, [clearBearerToken, i18nHydrated, searchParams, t]); useEffect(() => { if (passwordChangedHandled.current) return; if (searchParams.get("password") !== "changed") return; passwordChangedHandled.current = true; toast.success(t("login.passwordChanged")); stripSearchParamFromBrowserUrl("password"); }, [searchParams, t]); async function handleSubmit(event: React.FormEvent): Promise { event.preventDefault(); if (!username.trim() || !password) { toast.error(t("login.missingFields")); return; } if (!captchaKey || !captchaSrc) { toast.error(t("login.captchaRequired")); void loadCaptcha(); return; } if (!captchaCode.trim()) { toast.error(t("login.captchaCodeRequired")); return; } const usernameIssue = validatePlayerLoginUsername(username); if (usernameIssue === "invalid_charset") { toast.error(t("login.usernameInvalidCharset")); return; } const passwordIssue = validatePlayerLoginPassword(password); if (passwordIssue === "too_short") { toast.error(t("login.passwordMinLength")); return; } setLoading(true); try { const data = await postPlayerAuthLogin({ username: username.trim(), password, captcha_key: captchaKey, captcha_code: captchaCode.trim(), }); setBearerToken(data.access_token); const me = await getPlayerMe(); setProfile(me); try { const currencies = await getPublicCurrencies(); usePlayerSessionStore.getState().setCurrencies(currencies.items); } catch { // 不阻断登录 } router.replace("/hall"); } catch (e) { toast.error(e instanceof LotteryApiBizError ? e.message : t("login.failed")); void loadCaptcha(); } finally { setLoading(false); } } const formProps = { t, username, password, captchaCode, captchaSrc, loading, loadingCaptcha, onUsernameChange: setUsername, onPasswordChange: setPassword, onCaptchaCodeChange: setCaptchaCode, onRefreshCaptcha: () => void loadCaptcha(), onSubmit: (event: React.FormEvent) => void handleSubmit(event), }; return ( <>
); }