docs: 新增 AI 代理学习偏好与信用盘术语规范

feat: 优化登录与入口页面加载体验

- 为登录页面添加 Suspense 边界,统一 fallback 样式
- 重构入口守卫逻辑,在布局阶段处理未登录重定向,避免闪烁
- 登录页面支持会话过期提示,并自动清理 URL 参数

feat: 信用盘与钱包盘文案差异化展示

- 底部导航「钱包」标签在信用盘模式下显示为「信用」
- 大厅余额条在信用盘模式下使用「可
This commit is contained in:
2026-06-12 20:48:10 +08:00
parent 20d2befa55
commit 7c283627d3
24 changed files with 347 additions and 92 deletions

View File

@@ -2,9 +2,8 @@
import { Loader2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -20,20 +19,42 @@ import { LotteryApiBizError } from "@/types/api/errors";
const DEPLOY_SITE_CODE = process.env.NEXT_PUBLIC_PLAYER_SITE_CODE?.trim() ?? "";
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 { t } = useTranslation("entry");
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 sessionExpiredHandled = useRef(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
if (sessionExpiredHandled.current) return;
if (searchParams.get("session") !== "expired") return;
sessionExpiredHandled.current = true;
clearBearerToken();
toast.error(t("errors.sessionExpired"));
stripSearchParamFromBrowserUrl("session");
}, [clearBearerToken, searchParams, t]);
async function handleSubmit(event: React.FormEvent): Promise<void> {
event.preventDefault();
if (!username.trim() || !password) {
toast.error(t("login.missingFields", { defaultValue: "请填写账号和密码" }));
toast.error(t("login.missingFields"));
return;
}
@@ -55,7 +76,7 @@ export function PlayerLoginScreen(): React.ReactElement {
}
router.replace("/hall");
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : t("login.failed", { defaultValue: "登录失败" }));
toast.error(e instanceof LotteryApiBizError ? e.message : t("login.failed"));
} finally {
setLoading(false);
}
@@ -63,8 +84,10 @@ export function PlayerLoginScreen(): React.ReactElement {
return (
<div className="relative flex min-h-dvh flex-col bg-white">
<div className="relative h-[32vh] min-h-[220px] bg-red-600">
<Image src="/entry/image1.png" alt="" fill className="object-cover object-center" priority />
<div className="relative h-[45vh] min-h-[320px] shrink-0 overflow-hidden bg-red-600">
<div className="pointer-events-none absolute inset-0">
<Image src="/entry/image1.png" alt="" fill className="object-cover object-center" priority />
</div>
<div className="absolute left-0 right-0 top-0 z-20 flex items-center px-4 py-3">
<LanguageSwitcher variant="header" showFlag={false} />
</div>
@@ -72,15 +95,15 @@ export function PlayerLoginScreen(): React.ReactElement {
<div className="mx-auto w-full max-w-md flex-1 px-4 py-8">
<h1 className="text-xl font-bold text-gray-900">
{t("login.title", { defaultValue: "代理玩家登录" })}
{t("login.title")}
</h1>
<p className="mt-2 text-sm text-muted-foreground">
{t("login.hint", { defaultValue: "使用代理为您开通的账号登录。主站用户请从主站进入。" })}
{t("login.hint")}
</p>
<form className="mt-6 space-y-4" onSubmit={(e) => void handleSubmit(e)}>
<div className="space-y-1">
<Label htmlFor="login-user">{t("login.username", { defaultValue: "登录账号" })}</Label>
<Label htmlFor="login-user">{t("login.username")}</Label>
<Input
id="login-user"
value={username}
@@ -89,7 +112,7 @@ export function PlayerLoginScreen(): React.ReactElement {
/>
</div>
<div className="space-y-1">
<Label htmlFor="login-pass">{t("login.password", { defaultValue: "密码" })}</Label>
<Label htmlFor="login-pass">{t("login.password")}</Label>
<Input
id="login-pass"
type="password"
@@ -100,15 +123,9 @@ export function PlayerLoginScreen(): React.ReactElement {
</div>
<Button type="submit" className="w-full bg-red-600 hover:bg-red-700" disabled={loading}>
{loading ? <Loader2 className="mr-2 size-4 animate-spin" /> : null}
{t("login.submit", { defaultValue: "登录" })}
{t("login.submit")}
</Button>
</form>
<p className="mt-4 text-center text-sm text-muted-foreground">
<Link href="/" className="text-red-600 underline">
{t("login.backEntry", { defaultValue: "返回入口" })}
</Link>
</p>
</div>
</div>
);