diff --git a/src/components/language-switcher.tsx b/src/components/language-switcher.tsx index 71dc6db..6c70e87 100644 --- a/src/components/language-switcher.tsx +++ b/src/components/language-switcher.tsx @@ -12,12 +12,14 @@ import { playerViewportPopoverMaxClass } from "@/lib/player-viewport"; import { cn } from "@/lib/utils"; interface LanguageSwitcherProps { - variant?: "default" | "header" | "minimal"; + variant?: "default" | "header" | "minimal" | "pill"; /** 下拉相对触发器水平对齐:`start`=左对齐(适合左上角触发器),`end`=右对齐(适合顶栏右侧) */ menuAlign?: "start" | "end"; className?: string; showFlag?: boolean; showLabel?: boolean; + /** 显示完整语言名(如「简体中文」),而非缩写 */ + useFullLabel?: boolean; } export function LanguageSwitcher({ @@ -26,6 +28,7 @@ export function LanguageSwitcher({ className, showFlag = true, showLabel = true, + useFullLabel = false, }: LanguageSwitcherProps) { const { i18n, t } = useTranslation("common"); const active = normalizeLanguage(i18n.language) as AppLanguage; @@ -46,7 +49,9 @@ export function LanguageSwitcher({ setIsOpen(false); } - const currentLabel = options.find((o) => o.code === active)?.short ?? active.toUpperCase(); + const currentLabel = useFullLabel + ? (options.find((o) => o.code === active)?.label ?? active.toUpperCase()) + : (options.find((o) => o.code === active)?.short ?? active.toUpperCase()); const currentFlag = options.find((o) => o.code === active)?.flag ?? ""; const variantStyles = { @@ -68,12 +73,20 @@ export function LanguageSwitcher({ item: "text-gray-800 hover:bg-gray-100", activeItem: "bg-red-50 text-red-600", }, + pill: { + button: + "rounded-full border border-gray-200 bg-white px-3 py-1.5 text-gray-600 shadow-sm hover:bg-gray-50", + dropdown: "border border-gray-200 bg-white shadow-lg", + item: "text-gray-800 hover:bg-gray-100", + activeItem: "bg-red-50 text-red-600", + }, } as const; const styles = variantStyles[variant]; const align = - menuAlign ?? (variant === "header" || variant === "default" ? "start" : "end"); + menuAlign ?? + (variant === "pill" ? "end" : variant === "header" || variant === "default" ? "start" : "end"); return ( diff --git a/src/components/layout/player-desktop-header.tsx b/src/components/layout/player-desktop-header.tsx index 5ddd15c..bbaff4a 100644 --- a/src/components/layout/player-desktop-header.tsx +++ b/src/components/layout/player-desktop-header.tsx @@ -4,14 +4,13 @@ import Image from "next/image"; import Link from "next/link"; import { LanguageSwitcher } from "@/components/language-switcher"; -import { PlayerNotificationBell } from "@/components/layout/player-notification-bell"; import { cn } from "@/lib/utils"; type PlayerDesktopHeaderProps = { className?: string; }; -/** 全局顶栏:移动显示 Logo;桌面 Logo 在侧栏顶部,此处仅语言 + 通知 */ +/** 全局顶栏:移动显示 Logo;桌面 Logo 在侧栏顶部,此处仅语言切换 */ export function PlayerDesktopHeader({ className }: PlayerDesktopHeaderProps) { return (
-
); -} \ No newline at end of file +} diff --git a/src/features/hall/hall-screen.tsx b/src/features/hall/hall-screen.tsx index b8f4c0b..94f801c 100644 --- a/src/features/hall/hall-screen.tsx +++ b/src/features/hall/hall-screen.tsx @@ -9,9 +9,7 @@ import { HallWalletStrip } from "@/features/hall/hall-wallet-strip"; import { JackpotBurstOverlay } from "@/features/hall/jackpot-burst-overlay"; import { useHallDrawLive } from "@/features/hall/use-hall-draw-live"; import { useJackpotBurstLive } from "@/features/hall/use-jackpot-burst-live"; -import { playerPageInset } from "@/lib/player-spacing"; -import { playerContentColumnClass } from "@/lib/player-viewport"; -import { cn } from "@/lib/utils"; +import { PlayerPanel } from "@/components/layout/player-panel"; /** * 下注大厅:钱包条 §4 + 当期期号 §4.2(封盘置灰 / 倒计时错误色 / WS+轮询);玩法目录 §12.3;下注表格 §13.3。 @@ -23,20 +21,13 @@ export function HallScreen() { const { burstEvent, clearBurstEvent } = useJackpotBurstLive(tp); return ( -
-
+ <> + - - -
+ -
+ ); } \ No newline at end of file diff --git a/src/features/player/entry-gate.tsx b/src/features/player/entry-gate.tsx index d63200c..fdcdc64 100644 --- a/src/features/player/entry-gate.tsx +++ b/src/features/player/entry-gate.tsx @@ -26,6 +26,7 @@ import { getPublicCurrencies } from "@/api/currency"; import { getPlayerMe, getPlayerPing } from "@/api/player"; import { isInIframe } from "@/components/iframe-bridge"; import { LanguageSwitcher } from "@/components/language-switcher"; +import { EntryHeroBanner } from "@/features/player/entry-hero-banner"; import { Button, buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { usePlayerSessionStore } from "@/stores/player-session-store"; @@ -431,24 +432,21 @@ export function EntryGate() { return (
-
-
- {t("header.backgroundAlt")} -
- -
- -
+
+
-
+ +
+ +
+
+ +
{phase === "loading" || waitingForEmbeddedToken ? (
diff --git a/src/features/player/entry-hero-banner.tsx b/src/features/player/entry-hero-banner.tsx new file mode 100644 index 0000000..75a7c1b --- /dev/null +++ b/src/features/player/entry-hero-banner.tsx @@ -0,0 +1,46 @@ +"use client"; + +import Image from "next/image"; +import type { ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +type EntryHeroBannerProps = { + src: string; + alt?: string; + className?: string; + imageClassName?: string; + children?: ReactNode; +}; + +/** 登录/入场页插画:仅移动端顶部横幅。 */ +export function EntryHeroBanner({ + src, + alt = "", + className, + imageClassName, + children, +}: EntryHeroBannerProps) { + return ( +
+
+ {alt} +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/src/features/player/player-login-desktop.tsx b/src/features/player/player-login-desktop.tsx new file mode 100644 index 0000000..f551b27 --- /dev/null +++ b/src/features/player/player-login-desktop.tsx @@ -0,0 +1,94 @@ +"use client"; + +import type { TFunction } from "i18next"; + +import { LanguageSwitcher } from "@/components/language-switcher"; +import { PlayerLoginForm } from "@/features/player/player-login-form"; + +type PlayerLoginDesktopProps = { + t: TFunction<"entry">; + username: string; + password: string; + captchaCode: string; + captchaSrc: string | null; + loading: boolean; + loadingCaptcha: boolean; + onUsernameChange: (value: string) => void; + onPasswordChange: (value: string) => void; + onCaptchaCodeChange: (value: string) => void; + onRefreshCaptcha: () => void; + onSubmit: (event: React.FormEvent) => void; +}; + +/** PC 端登录:左右分栏卡片,左侧插画占位。 */ +export function PlayerLoginDesktop({ + t, + username, + password, + captchaCode, + captchaSrc, + loading, + loadingCaptcha, + onUsernameChange, + onPasswordChange, + onCaptchaCodeChange, + onRefreshCaptcha, + onSubmit, +}: PlayerLoginDesktopProps) { + return ( +
+
+ +
+ {/* 左侧插画:图片待替换 */} +
+ +
+
+ +
+ +
+

+ {t("login.title")} +

+

+ {t("login.hint")} +

+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/features/player/player-login-form.tsx b/src/features/player/player-login-form.tsx new file mode 100644 index 0000000..ee90b5f --- /dev/null +++ b/src/features/player/player-login-form.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { Eye, EyeOff, KeyRound, Loader2, Lock, User } from "lucide-react"; +import { useState } from "react"; +import type { TFunction } from "i18next"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; + +type PlayerLoginFormProps = { + variant: "mobile" | "desktop"; + t: TFunction<"entry">; + username: string; + password: string; + captchaCode: string; + captchaSrc: string | null; + loading: boolean; + loadingCaptcha: boolean; + onUsernameChange: (value: string) => void; + onPasswordChange: (value: string) => void; + onCaptchaCodeChange: (value: string) => void; + onRefreshCaptcha: () => void; + onSubmit: (event: React.FormEvent) => void; +}; + +export function PlayerLoginForm({ + variant, + t, + username, + password, + captchaCode, + captchaSrc, + loading, + loadingCaptcha, + onUsernameChange, + onPasswordChange, + onCaptchaCodeChange, + onRefreshCaptcha, + onSubmit, +}: PlayerLoginFormProps) { + const [showPassword, setShowPassword] = useState(false); + const isDesktop = variant === "desktop"; + + const inputClass = cn( + isDesktop && "h-10 rounded-xl border-gray-200 bg-white shadow-none", + ); + const captchaButtonClass = cn( + "flex shrink-0 cursor-pointer items-center justify-center overflow-hidden border border-input bg-muted/40 transition-colors hover:bg-muted/60 disabled:pointer-events-none disabled:opacity-50", + isDesktop + ? "h-10 min-w-[8.5rem] rounded-xl border-gray-200 bg-[#f4fbf6]" + : "h-8 min-w-[7.5rem] rounded-lg px-2 text-sm", + ); + + return ( +
+
+ +
+ {isDesktop ? ( + + ) : null} + onUsernameChange(e.target.value)} + autoComplete="username" + disabled={loading} + placeholder={isDesktop ? t("login.usernamePlaceholder") : undefined} + className={cn(inputClass, isDesktop && "pl-10")} + /> +
+
+ +
+ +
+ {isDesktop ? ( + + ) : null} + onPasswordChange(e.target.value)} + autoComplete="current-password" + disabled={loading} + placeholder={isDesktop ? t("login.passwordPlaceholder") : undefined} + className={cn(inputClass, isDesktop && "pr-10 pl-10")} + /> + {isDesktop ? ( + + ) : null} +
+
+ +
+ +
+
+ {isDesktop ? ( + + ) : null} + onCaptchaCodeChange(e.target.value)} + placeholder={t("login.captchaPlaceholder")} + maxLength={32} + disabled={loading} + className={cn(inputClass, isDesktop && "pl-10")} + /> +
+ +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/src/features/player/player-login-screen.tsx b/src/features/player/player-login-screen.tsx index 7e7b45d..6b2a8d4 100644 --- a/src/features/player/player-login-screen.tsx +++ b/src/features/player/player-login-screen.tsx @@ -1,7 +1,5 @@ "use client"; -import { Loader2 } from "lucide-react"; -import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -11,9 +9,9 @@ import { getPlayerMe } from "@/api/player"; import { getPlayerAuthCaptcha, postPlayerAuthLogin } from "@/api/player-auth"; import { getPublicCurrencies } from "@/api/currency"; import { LanguageSwitcher } from "@/components/language-switcher"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +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, @@ -149,91 +147,41 @@ export function PlayerLoginScreen(): React.ReactElement { } } + 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 ( -
-
-
- -
-
- + <> + + +
+ +
+ +
+
+ +
+
+

{t("login.title")}

+

{t("login.hint")}

+ + +
- -
-

- {t("login.title")} -

-

- {t("login.hint")} -

- -
void handleSubmit(e)}> -
- - setUsername(e.target.value)} - autoComplete="username" - disabled={loading} - /> -
-
- - setPassword(e.target.value)} - autoComplete="current-password" - disabled={loading} - /> -
-
- -
- setCaptchaCode(e.target.value)} - placeholder={t("login.captchaPlaceholder")} - maxLength={32} - disabled={loading} - className="min-w-0 flex-1" - /> - -
-
- -
-
-
+ ); -} +} \ No newline at end of file diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts index 90a50c9..851cff1 100644 --- a/src/hooks/use-mobile.ts +++ b/src/hooks/use-mobile.ts @@ -9,12 +9,19 @@ import { PLAYER_DESKTOP_BREAKPOINT_PX } from "@/lib/player-viewport"; * 桌面端启用侧栏 + 顶栏布局(见 UI 设计稿)。 */ export function useIsMobile(): boolean { - const [isMobile, setIsMobile] = useState(true); + const [isMobile, setIsMobile] = useState(() => { + if (typeof window === "undefined") return true; + return window.matchMedia( + `(max-width: ${PLAYER_DESKTOP_BREAKPOINT_PX - 1}px)`, + ).matches; + }); useEffect(() => { const query = `(max-width: ${PLAYER_DESKTOP_BREAKPOINT_PX - 1}px)`; const mq = window.matchMedia(query); const sync = () => setIsMobile(mq.matches); + // sync() is still needed for the rare case where the lazy initializer + // was correct but the viewport changed between SSR and hydration. sync(); mq.addEventListener("change", sync); return () => mq.removeEventListener("change", sync); diff --git a/src/i18n/locales/en/entry.json b/src/i18n/locales/en/entry.json index b0accdf..1d7b9ed 100644 --- a/src/i18n/locales/en/entry.json +++ b/src/i18n/locales/en/entry.json @@ -53,7 +53,11 @@ "title": "Agent player login", "hint": "Sign in with the account your agent created for you. Main-site users should enter from the main site.", "username": "Username", + "usernamePlaceholder": "Enter username", "password": "Password", + "passwordPlaceholder": "Enter password", + "showPassword": "Show password", + "hidePassword": "Hide password", "submit": "Sign in", "missingFields": "Please enter username and password", "failed": "Sign-in failed", diff --git a/src/i18n/locales/ne/entry.json b/src/i18n/locales/ne/entry.json index dc2ff6b..6f00b15 100644 --- a/src/i18n/locales/ne/entry.json +++ b/src/i18n/locales/ne/entry.json @@ -53,7 +53,11 @@ "title": "एजेन्ट खेलाडी लगइन", "hint": "एजेन्टले दिएको खाताबाट लगइन गर्नुहोस्। मुख्य साइट प्रयोगकर्ताहरू मुख्य साइटबाट प्रवेश गर्नुहोस्।", "username": "प्रयोगकर्ता नाम", + "usernamePlaceholder": "प्रयोगकर्ता नाम प्रविष्ट गर्नुहोस्", "password": "पासवर्ड", + "passwordPlaceholder": "पासवर्ड प्रविष्ट गर्नुहोस्", + "showPassword": "पासवर्ड देखाउनुहोस्", + "hidePassword": "पासवर्ड लुकाउनुहोस्", "submit": "लगइन", "missingFields": "कृपया प्रयोगकर्ता नाम र पासवर्ड भर्नुहोस्", "failed": "लगइन असफल", diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index e1778a4..9704e2f 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -2,7 +2,7 @@ "language": { "en": "English", "ne": "नेपाली", - "zh": "中文" + "zh": "简体中文" }, "languageShort": { "en": "EN", diff --git a/src/i18n/locales/zh/entry.json b/src/i18n/locales/zh/entry.json index 05332dc..65ab4bf 100644 --- a/src/i18n/locales/zh/entry.json +++ b/src/i18n/locales/zh/entry.json @@ -53,7 +53,11 @@ "title": "代理玩家登录", "hint": "使用代理为您开通的账号登录。主站用户请从主站进入。", "username": "登录账号", + "usernamePlaceholder": "请输入登录账号", "password": "密码", + "passwordPlaceholder": "请输入密码", + "showPassword": "显示密码", + "hidePassword": "隐藏密码", "submit": "登录", "missingFields": "请填写账号和密码", "failed": "登录失败",