feat: 优化登录与入口页面加载体验 - 为登录页面添加 Suspense 边界,统一 fallback 样式 - 重构入口守卫逻辑,在布局阶段处理未登录重定向,避免闪烁 - 登录页面支持会话过期提示,并自动清理 URL 参数 feat: 信用盘与钱包盘文案差异化展示 - 底部导航「钱包」标签在信用盘模式下显示为「信用」 - 大厅余额条在信用盘模式下使用「可
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { usePathname } from "next/navigation";
|
||
|
||
import { BarChart3, ClipboardList, Home, Wallet } from "lucide-react";
|
||
import { useTranslation } from "react-i18next";
|
||
|
||
import { playerViewportFixedBarClass } from "@/lib/player-viewport";
|
||
import { isCreditFundingPlayer } from "@/lib/player-funding-mode";
|
||
import { cn } from "@/lib/utils";
|
||
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
||
|
||
const tabs = [
|
||
{
|
||
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/"),
|
||
},
|
||
] as const;
|
||
|
||
/**
|
||
* 移动端 H5 主导航:底部 Tab(界面文档 §1.1 以手机为主)。
|
||
*/
|
||
export function PlayerBottomNav() {
|
||
const pathname = usePathname() ?? "";
|
||
const { t } = useTranslation("player");
|
||
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
|
||
|
||
return (
|
||
<nav
|
||
className={cn(
|
||
playerViewportFixedBarClass,
|
||
"bottom-0 border-t border-[#e4ebf5] bg-white/96 pb-[env(safe-area-inset-bottom,0px)] shadow-[0_-10px_30px_rgba(15,23,42,0.08)] backdrop-blur-md",
|
||
)}
|
||
aria-label={t("nav.aria")}
|
||
>
|
||
<div className="grid h-14 w-full grid-cols-4 grid-rows-1">
|
||
{tabs.map(({ href, labelKey, labelDefault, icon: Icon, match, ...tab }) => {
|
||
const active = match(pathname);
|
||
const creditLabelKey = "creditLabelKey" in tab ? tab.creditLabelKey : undefined;
|
||
const creditLabelDefault = "creditLabelDefault" in tab ? tab.creditLabelDefault : undefined;
|
||
const label = creditMode && creditLabelKey
|
||
? t(creditLabelKey, { defaultValue: creditLabelDefault ?? labelDefault })
|
||
: t(labelKey, { defaultValue: labelDefault });
|
||
return (
|
||
<Link
|
||
key={href}
|
||
href={href}
|
||
prefetch
|
||
aria-current={active ? "page" : undefined}
|
||
className={cn(
|
||
"relative flex min-h-0 min-w-0 max-w-full flex-col items-center justify-center gap-1 px-0.5 text-center text-[10px] font-semibold leading-tight transition-colors sm:text-[11px]",
|
||
active
|
||
? "text-[#f10b32]"
|
||
: "text-[#6b7280] hover:text-[#1f2937] active:text-[#1f2937]",
|
||
)}
|
||
>
|
||
<Icon
|
||
aria-hidden
|
||
className={cn("size-5 shrink-0 sm:size-[22px]", active && "stroke-[2.5px]")}
|
||
/>
|
||
<span className="w-full truncate">{label}</span>
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
</nav>
|
||
);
|
||
}
|