Files
lotteryAdmin/src/components/admin/admin-sidebar.tsx
kang a550c418e5 refactor(layout, i18n, admin): 优化布局结构与多语言支持
调整 AdminShell 组件的子组件顺序,提升代码可读性。更新 admin-breadcrumb 组件,简化导航标签翻译逻辑,确保多语言支持的一致性。重构 admin-language-switcher 组件,优化语言切换的用户体验,增强界面交互性。更新多语言配置,新增登录界面的副标题,提升用户体验。
2026-05-30 17:46:27 +08:00

199 lines
7.7 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, type ReactElement } from "react";
import { useTranslation } from "react-i18next";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarSeparator,
} from "@/components/ui/sidebar";
import { adminNavLabel } from "@/lib/admin-nav-label";
import { cn } from "@/lib/utils";
import { resolveAdminNavIcon } from "@/modules/_config/admin-nav-icons";
import { ADMIN_BASE } from "@/modules/_config/admin-nav";
import { useAdminProfile, useAdminSessionStore } from "@/stores/admin-session";
/** 与常见导航项文字宽度接近,避免整齐灰条 */
const SIDEBAR_NAV_SKELETON_WIDTHS = ["68%", "82%", "58%", "74%", "64%", "78%", "55%", "70%", "62%"] as const;
function SidebarNavSkeletonRow({
labelWidth,
delayMs,
}: {
labelWidth: string;
delayMs: number;
}): ReactElement {
return (
<SidebarMenuItem>
<div
aria-hidden
className="flex h-8 w-full items-center gap-2 rounded-md px-2 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-1.5"
style={{ animationDelay: `${delayMs}ms` }}
>
<span
className="size-4 shrink-0 rounded-[4px] bg-white/12 motion-safe:animate-pulse"
style={{ animationDelay: `${delayMs}ms` }}
/>
<span
className="h-2.5 shrink-0 rounded-full bg-white/12 motion-safe:animate-pulse group-data-[collapsible=icon]:hidden"
style={{ width: labelWidth, animationDelay: `${delayMs + 40}ms` }}
/>
</div>
</SidebarMenuItem>
);
}
function AdminSidebarSkeleton(): ReactElement {
const { t } = useTranslation("common");
return (
<Sidebar collapsible="icon" className="overflow-hidden">
<SidebarHeader className="flex h-14 shrink-0 items-center gap-0 border-b border-sidebar-border p-0 px-2">
<SidebarMenu className="h-full w-full">
<SidebarMenuItem className="h-full">
<div className="flex h-12 w-full items-center px-1 group-data-[collapsible=icon]:justify-center">
<img
src="/logo.png"
alt="N lotto"
className="h-auto max-h-11 w-full object-contain object-left opacity-95 group-data-[collapsible=icon]:max-h-8 group-data-[collapsible=icon]:object-center"
/>
</div>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent className="relative overflow-hidden">
<div
className="pointer-events-none absolute inset-x-0 bottom-0 h-[22rem] opacity-55 group-data-[collapsible=icon]:hidden"
aria-hidden
>
<img
src="/image6.png"
alt=""
className="h-full w-full object-cover object-bottom"
/>
<div className="absolute inset-x-0 top-0 h-28 bg-linear-to-b from-sidebar to-transparent" />
<div className="absolute inset-0 bg-sidebar/20" />
</div>
<SidebarGroup className="relative z-10">
<SidebarGroupLabel className="text-sidebar-foreground/55">
{t("sidebar.workspace", { defaultValue: "Workspace" })}
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu className={cn("gap-0.5", "motion-safe:opacity-90")}>
{SIDEBAR_NAV_SKELETON_WIDTHS.map((width, i) => (
<SidebarNavSkeletonRow key={i} labelWidth={width} delayMs={i * 55} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarSeparator />
<SidebarRail />
<span className="sr-only" role="status" aria-live="polite">
{t("auth.checking")}
</span>
</Sidebar>
);
}
function isActive(pathname: string, item: { href: string; activeMatchPrefix?: string; segment?: string }): boolean {
const { href, activeMatchPrefix, segment } = item;
const prefix = activeMatchPrefix ?? href;
if (prefix === ADMIN_BASE || prefix === `${ADMIN_BASE}/`) {
return pathname === ADMIN_BASE || pathname === `${ADMIN_BASE}/`;
}
// Keep "settings" independent from its child routes like /admin/settings/currencies.
if (segment === "settings") {
return pathname === href;
}
return pathname === prefix || pathname.startsWith(`${prefix}/`);
}
export function AdminAppSidebar() {
const { t } = useTranslation(["common", "dashboard", "players", "draws", "config", "wallet", "risk", "settlement", "jackpot", "reconcile", "tickets", "audit", "reports"]);
const pathname = usePathname();
const shellAuthPending = useAdminSessionStore((s) => s.shellAuthPending);
const profile = useAdminProfile();
if (shellAuthPending) {
return <AdminSidebarSkeleton />;
}
const visibleNav = useMemo(
() => (profile?.navigation ?? []).filter((item) => item.segment !== "risk"),
[profile?.navigation],
);
return (
<Sidebar collapsible="icon" className="overflow-hidden">
<SidebarHeader className="flex h-14 shrink-0 items-center gap-0 border-b border-sidebar-border p-0 px-2">
<SidebarMenu className="h-full w-full">
<SidebarMenuItem className="h-full">
<SidebarMenuButton
render={<Link href={ADMIN_BASE} />}
className="h-full min-h-0 justify-start px-1 py-0 hover:bg-transparent group-data-[collapsible=icon]:justify-center"
>
<div className="flex h-12 w-full items-center group-data-[collapsible=icon]:size-10 group-data-[collapsible=icon]:justify-center">
<img
src="/logo.png"
alt="N lotto"
className="h-auto max-h-11 w-full object-contain object-left group-data-[collapsible=icon]:max-h-8 group-data-[collapsible=icon]:object-center"
/>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent className="relative overflow-hidden">
<div
className="pointer-events-none absolute inset-x-0 bottom-0 h-[22rem] opacity-55 group-data-[collapsible=icon]:hidden"
aria-hidden
>
<img
src="/image6.png"
alt=""
className="h-full w-full object-cover object-bottom"
/>
<div className="absolute inset-x-0 top-0 h-28 bg-linear-to-b from-sidebar to-transparent" />
<div className="absolute inset-0 bg-sidebar/20" />
</div>
<SidebarGroup>
<SidebarGroupLabel>{t("sidebar.workspace", { ns: "common", defaultValue: "Workspace" })}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{visibleNav.map((item) => {
const Icon = resolveAdminNavIcon(item.segment);
return (
<SidebarMenuItem key={item.segment}>
<SidebarMenuButton
tooltip={adminNavLabel(item.segment, t, item.label)}
isActive={isActive(pathname, item)}
render={<Link href={item.href} />}
className="font-medium text-sidebar-foreground/90 hover:text-sidebar-accent-foreground data-active:bg-red-600 data-active:text-white data-active:shadow-sm"
>
<Icon data-icon="inline-start" aria-hidden />
<span>{adminNavLabel(item.segment, t, item.label)}</span>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarSeparator />
<SidebarRail />
</Sidebar>
);
}