);
-}
+}
\ No newline at end of file
diff --git a/src/components/layout/player-notification-bell.tsx b/src/components/layout/player-notification-bell.tsx
index 8b5b9ae..df6765f 100644
--- a/src/components/layout/player-notification-bell.tsx
+++ b/src/components/layout/player-notification-bell.tsx
@@ -1,12 +1,30 @@
"use client";
+import { Bell } from "lucide-react";
+import Link from "next/link";
import { type ReactElement } from "react";
+import { cn } from "@/lib/utils";
+
type PlayerNotificationBellProps = {
className?: string;
};
-/** 顶栏铃铛:暂隐藏,待接入通用通知中心后再开放 */
-export function PlayerNotificationBell(_props: PlayerNotificationBellProps): ReactElement | null {
- return null;
-}
+/** 顶栏铃铛:跳转通知页(完整通知中心待后续接入) */
+export function PlayerNotificationBell({
+ className,
+}: PlayerNotificationBellProps): ReactElement {
+ return (
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/layout/player-panel.tsx b/src/components/layout/player-panel.tsx
index 8558f08..bf72b70 100644
--- a/src/components/layout/player-panel.tsx
+++ b/src/components/layout/player-panel.tsx
@@ -1,21 +1,14 @@
"use client";
-import Link from "next/link";
import type { ReactNode } from "react";
-import { ChevronLeft } from "lucide-react";
-import { useTranslation } from "react-i18next";
-
-import { LanguageSwitcher } from "@/components/language-switcher";
-import { usePlayerStickyTopStyle } from "@/hooks/use-player-sticky-top-style";
-import {
- playerHeaderControl,
- playerPageHeader,
-} from "@/lib/player-spacing";
+import { playerPageShellPadding } from "@/lib/player-spacing";
+import { playerContentColumnClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
type PlayerPanelProps = {
- title: string;
+ /** 保留供调用方语义标注,不再渲染页内顶栏(导航由外壳侧栏/顶栏承担) */
+ title?: string;
children: ReactNode;
backHref?: string;
backLabel?: string;
@@ -24,65 +17,21 @@ type PlayerPanelProps = {
};
export function PlayerPanel({
- title,
children,
- backHref = "/hall",
- backLabel,
className,
containerClassName,
}: PlayerPanelProps) {
- const { t: tp } = useTranslation("player");
- const resolvedBackLabel = backLabel ?? tp("panel.home");
- const stickyTopStyle = usePlayerStickyTopStyle();
-
return (
-
+
);
-}
+}
\ No newline at end of file
diff --git a/src/components/layout/player-sidebar.tsx b/src/components/layout/player-sidebar.tsx
new file mode 100644
index 0000000..7160c89
--- /dev/null
+++ b/src/components/layout/player-sidebar.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import {
+ BarChart3,
+ BookOpen,
+ ClipboardList,
+ Home,
+ Wallet,
+} from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
+import { cn } from "@/lib/utils";
+import { usePlayerSessionStore } from "@/stores/player-session-store";
+
+const navItems = [
+ {
+ href: "/hall",
+ labelKey: "nav.home",
+ labelDefault: "首页",
+ icon: Home,
+ match: (p: string) => p === "/hall",
+ },
+ {
+ href: "/results",
+ labelKey: "nav.results",
+ labelDefault: "开奖结果",
+ icon: BarChart3,
+ match: (p: string) => p === "/results" || p.startsWith("/results/"),
+ },
+ {
+ href: "/orders",
+ labelKey: "nav.orders",
+ labelDefault: "我的注单",
+ icon: ClipboardList,
+ match: (p: string) => p === "/orders" || p.startsWith("/orders/"),
+ },
+ {
+ href: "/wallet",
+ labelKey: "nav.wallet",
+ creditLabelKey: "nav.credit",
+ labelDefault: "钱包",
+ creditLabelDefault: "信用",
+ icon: Wallet,
+ match: (p: string) => p === "/wallet" || p.startsWith("/wallet/"),
+ },
+ {
+ href: "/rules",
+ labelKey: "nav.rules",
+ labelDefault: "规则",
+ icon: BookOpen,
+ match: (p: string) => p === "/rules",
+ },
+] as const;
+
+/** 与 lotteryadmin `NAV_BTN` / `NAV_ACTIVE` 一致 */
+const playerNavLinkClass =
+ "flex h-8 w-full items-center gap-2 rounded-md px-2.5 text-[13px] leading-snug font-normal text-sidebar-foreground/90 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0";
+
+const playerNavLinkActiveClass =
+ "bg-sidebar-primary/90 font-medium text-sidebar-primary-foreground shadow-sm hover:bg-sidebar-primary/90 hover:text-sidebar-primary-foreground";
+
+/** 桌面端左侧导航(激活态对齐后台侧栏) */
+export function PlayerSidebar() {
+ const pathname = usePathname() ?? "";
+ const { t } = useTranslation("player");
+ const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/network-status-banner.tsx b/src/components/network-status-banner.tsx
index f3e47c6..10cd649 100644
--- a/src/components/network-status-banner.tsx
+++ b/src/components/network-status-banner.tsx
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { usePlayerBannerHeightRef } from "@/hooks/use-player-banner-height-ref";
import { useWebSocketManager } from "@/hooks/use-websocket-manager";
-import { playerViewportColumnClass } from "@/lib/player-viewport";
+import { playerContentColumnClass } from "@/lib/player-viewport";
import { useErrorStore } from "@/stores/error-store";
import { cn } from "@/lib/utils";
@@ -43,7 +43,7 @@ export function NetworkStatusBanner(): React.ReactElement | null {
+
{/* 错误图标 */}
+
{/* 错误图标 */}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
index 014f5aa..1a3f8f4 100644
--- a/src/components/ui/dialog.tsx
+++ b/src/components/ui/dialog.tsx
@@ -4,6 +4,10 @@ import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
+import {
+ playerViewportModalClass,
+ playerViewportOverlayClass,
+} from "@/lib/player-viewport"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
@@ -31,7 +35,8 @@ function DialogOverlay({
{
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "calc(var(--radius) * 0.8)",
- "--width": "min(280px, calc(100vw - 24px))",
+ "--width": "min(280px, calc(var(--player-viewport-max, 480px) - 24px))",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast:
- "cn-toast !min-h-0 !w-full !max-w-[min(280px,calc(100vw-24px))] !items-center !gap-2 !rounded-lg !border !px-3 !py-2 !text-xs !shadow-md",
+ "cn-toast !min-h-0 !w-full !max-w-[min(280px,calc(var(--player-viewport-max,480px)-24px))] !items-center !gap-2 !rounded-lg !border !px-3 !py-2 !text-xs !shadow-md",
title: "!text-xs !font-semibold !leading-snug",
description: "!text-[11px] !leading-snug !text-muted-foreground",
icon: "!mr-0 !size-3.5",
diff --git a/src/features/hall/hall-screen.tsx b/src/features/hall/hall-screen.tsx
index 087b792..b8f4c0b 100644
--- a/src/features/hall/hall-screen.tsx
+++ b/src/features/hall/hall-screen.tsx
@@ -1,21 +1,16 @@
"use client";
-import { BookOpen } from "lucide-react";
-import Image from "next/image";
-import Link from "next/link";
import { useTranslation } from "react-i18next";
-import { LanguageSwitcher } from "@/components/language-switcher";
import { HallBettingGrid } from "@/features/hall/hall-betting-grid";
-import { usePlayerStickyTopStyle } from "@/hooks/use-player-sticky-top-style";
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
import { HallDrawPanel } from "@/features/hall/hall-draw-panel";
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 { playerHeaderControl, playerPageHeader, playerPageInset } from "@/lib/player-spacing";
-import { playerViewportColumnClass } from "@/lib/player-viewport";
+import { playerPageInset } from "@/lib/player-spacing";
+import { playerContentColumnClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
/**
@@ -26,52 +21,15 @@ export function HallScreen() {
const drawLive = useHallDrawLive();
const { activeCurrency } = useActivePlayerCurrency();
const { burstEvent, clearBurstEvent } = useJackpotBurstLive(tp);
- const stickyTopStyle = usePlayerStickyTopStyle();
return (
-
-
-
-
+
+
@@ -81,4 +39,4 @@ export function HallScreen() {
);
-}
+}
\ No newline at end of file
diff --git a/src/features/hall/jackpot-burst-overlay.tsx b/src/features/hall/jackpot-burst-overlay.tsx
index 2513ef6..3df39ea 100644
--- a/src/features/hall/jackpot-burst-overlay.tsx
+++ b/src/features/hall/jackpot-burst-overlay.tsx
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { getDrawResultByNo } from "@/api/draw";
import { Button } from "@/components/ui/button";
import { formatMinorAsCurrency } from "@/lib/money";
+import { playerViewportOverlayClass } from "@/lib/player-viewport";
import { cn } from "@/lib/utils";
export type JackpotBurstEvent = {
@@ -214,7 +215,10 @@ function JackpotBurstOverlayContent({
return (
{phase === "loading" || waitingForEmbeddedToken ? (
-
+
@@ -525,7 +525,7 @@ export function EntryGate() {
) : null}
{phase === "failed" ? (
-
+
@@ -601,7 +601,7 @@ export function EntryGate() {
) : null}
{phase === "success" ? (
-
+
diff --git a/src/features/player/player-login-screen.tsx b/src/features/player/player-login-screen.tsx
index e9e0a44..7e7b45d 100644
--- a/src/features/player/player-login-screen.tsx
+++ b/src/features/player/player-login-screen.tsx
@@ -160,7 +160,7 @@ export function PlayerLoginScreen(): React.ReactElement {
-
+
{t("login.title")}
diff --git a/src/features/results/draw-results-list-screen.tsx b/src/features/results/draw-results-list-screen.tsx
index 540c67f..6b4ee13 100644
--- a/src/features/results/draw-results-list-screen.tsx
+++ b/src/features/results/draw-results-list-screen.tsx
@@ -117,7 +117,8 @@ export function DrawResultsListScreen() {
{t("results.businessDate")}
-
+
+
{date ? (