2025-12-29 14:37:03 +08:00
|
|
|
"use client"
|
|
|
|
|
import {
|
|
|
|
|
Activity,
|
|
|
|
|
BarChart3,
|
|
|
|
|
ChevronsLeft,
|
|
|
|
|
ChevronsRight,
|
2026-04-09 17:08:59 +08:00
|
|
|
CircleDollarSign,
|
2025-12-29 14:37:03 +08:00
|
|
|
ClipboardList,
|
2025-12-29 18:01:16 +08:00
|
|
|
ComputerIcon,
|
2026-03-26 15:27:52 +08:00
|
|
|
ContactRound,
|
2025-12-29 14:37:03 +08:00
|
|
|
DollarSign,
|
2026-04-18 17:41:27 +08:00
|
|
|
DoorClosedIcon,
|
2026-04-09 17:08:59 +08:00
|
|
|
FolderCode,
|
2025-12-29 14:37:03 +08:00
|
|
|
Home,
|
2026-03-18 17:13:31 +08:00
|
|
|
KeyRound,
|
2025-12-29 14:37:03 +08:00
|
|
|
type LucideIcon,
|
|
|
|
|
Package,
|
2026-04-08 13:41:12 +08:00
|
|
|
ScanSearch,
|
2025-12-29 14:37:03 +08:00
|
|
|
Shield,
|
2026-03-23 17:49:47 +08:00
|
|
|
ShoppingBag,
|
2026-03-24 17:14:50 +08:00
|
|
|
SquarePercent,
|
2026-03-27 15:51:40 +08:00
|
|
|
TicketPercent,
|
2025-12-29 14:37:03 +08:00
|
|
|
Users,
|
|
|
|
|
} from "lucide-react"
|
|
|
|
|
import Link from "next/link"
|
|
|
|
|
import { usePathname } from "next/navigation"
|
|
|
|
|
import { createContext, type ReactNode, useContext, useState } from "react"
|
2025-12-29 18:01:16 +08:00
|
|
|
import { twJoin } from "tailwind-merge"
|
2026-04-01 13:14:28 +08:00
|
|
|
import { Auth } from "@/components/auth"
|
2025-12-29 14:37:03 +08:00
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
|
|
|
import { Separator } from "@/components/ui/separator"
|
2025-12-29 18:01:16 +08:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipProvider,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "@/components/ui/tooltip"
|
2026-04-01 13:14:28 +08:00
|
|
|
import {
|
|
|
|
|
ScopeAdminRead,
|
|
|
|
|
ScopeAdminRoleRead,
|
2026-04-11 14:57:45 +08:00
|
|
|
ScopeBalanceActivity,
|
2026-04-01 13:14:28 +08:00
|
|
|
ScopeBatchRead,
|
|
|
|
|
ScopeBillRead,
|
|
|
|
|
ScopeChannelRead,
|
|
|
|
|
ScopeCouponRead,
|
|
|
|
|
ScopeDiscountRead,
|
|
|
|
|
ScopePermissionRead,
|
|
|
|
|
ScopeProductRead,
|
2026-04-18 17:58:40 +08:00
|
|
|
ScopeProxyRead,
|
2026-04-01 13:14:28 +08:00
|
|
|
ScopeResourceRead,
|
|
|
|
|
ScopeTradeRead,
|
|
|
|
|
ScopeUserRead,
|
2026-04-08 13:41:12 +08:00
|
|
|
ScopeUserReadNotBind,
|
2026-04-01 13:14:28 +08:00
|
|
|
ScopeUserReadOne,
|
|
|
|
|
} from "@/lib/scopes"
|
2025-12-29 10:41:23 +08:00
|
|
|
|
2025-12-29 14:37:03 +08:00
|
|
|
// Navigation Context
|
|
|
|
|
interface NavigationContextType {
|
|
|
|
|
collapsed: boolean
|
|
|
|
|
pathname: string
|
|
|
|
|
isActive: (path: string) => boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const NavigationContext = createContext<NavigationContextType | undefined>(
|
|
|
|
|
undefined,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const useNavigation = () => {
|
|
|
|
|
const context = useContext(NavigationContext)
|
|
|
|
|
if (!context) {
|
|
|
|
|
throw new Error("Navigation components must be used within Navigation")
|
|
|
|
|
}
|
|
|
|
|
return context
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 18:01:16 +08:00
|
|
|
// NavGroup Component
|
|
|
|
|
interface NavGroupProps {
|
2025-12-29 14:37:03 +08:00
|
|
|
title: string
|
|
|
|
|
children: ReactNode
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 18:01:16 +08:00
|
|
|
function NavGroup({ title, children }: NavGroupProps) {
|
2025-12-29 14:37:03 +08:00
|
|
|
const { collapsed } = useNavigation()
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="px-3">
|
|
|
|
|
{!collapsed && (
|
2025-12-29 18:01:16 +08:00
|
|
|
<h3 className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
2025-12-29 14:37:03 +08:00
|
|
|
{title}
|
|
|
|
|
</h3>
|
|
|
|
|
)}
|
|
|
|
|
<ul className={`${collapsed ? "mt-0" : "mt-2"} space-y-1`}>{children}</ul>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 18:01:16 +08:00
|
|
|
// NavItem Component
|
|
|
|
|
interface NavItemProps {
|
2025-12-29 14:37:03 +08:00
|
|
|
href: string
|
|
|
|
|
icon: LucideIcon
|
|
|
|
|
label: string
|
2026-04-01 13:14:28 +08:00
|
|
|
requiredScope?: string
|
2025-12-29 14:37:03 +08:00
|
|
|
}
|
2025-12-29 10:41:23 +08:00
|
|
|
|
2026-04-01 13:14:28 +08:00
|
|
|
function NavItem({ href, icon: Icon, label, requiredScope }: NavItemProps) {
|
2025-12-29 14:37:03 +08:00
|
|
|
const { collapsed, isActive } = useNavigation()
|
|
|
|
|
const active = isActive(href)
|
|
|
|
|
|
2026-04-01 13:14:28 +08:00
|
|
|
let linkContent = (
|
2025-12-29 18:01:16 +08:00
|
|
|
<Link
|
|
|
|
|
href={href}
|
|
|
|
|
className={`flex items-center ${
|
|
|
|
|
collapsed ? "justify-center w-10 h-10" : "px-3 py-2"
|
|
|
|
|
} rounded-md transition-colors ${
|
|
|
|
|
active
|
|
|
|
|
? "bg-accent text-accent-foreground"
|
|
|
|
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2026-01-06 14:57:55 +08:00
|
|
|
<Icon className={`h-5 w-5 ${collapsed ? "" : "shrink-0"}`} />
|
2025-12-29 18:01:16 +08:00
|
|
|
{!collapsed && <span className="ml-3 font-medium text-sm">{label}</span>}
|
|
|
|
|
</Link>
|
2025-12-29 14:37:03 +08:00
|
|
|
)
|
2025-12-29 18:01:16 +08:00
|
|
|
|
|
|
|
|
if (collapsed) {
|
2026-04-01 13:14:28 +08:00
|
|
|
linkContent = (
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
|
|
|
|
|
<TooltipContent side="right">
|
|
|
|
|
<p>{label}</p>
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (requiredScope) {
|
|
|
|
|
linkContent = (
|
|
|
|
|
<Auth scope={requiredScope}>
|
|
|
|
|
<li>{linkContent}</li>
|
|
|
|
|
</Auth>
|
2025-12-29 18:01:16 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 13:14:28 +08:00
|
|
|
return linkContent
|
2025-12-29 10:41:23 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-29 18:01:16 +08:00
|
|
|
// NavSeparator Component
|
|
|
|
|
function NavSeparator() {
|
2025-12-29 14:37:03 +08:00
|
|
|
const { collapsed } = useNavigation()
|
|
|
|
|
|
|
|
|
|
if (collapsed) return null
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="my-4">
|
|
|
|
|
<Separator />
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2025-12-29 10:41:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 13:14:28 +08:00
|
|
|
const menuSections: { title: string; items: NavItemProps[] }[] = [
|
|
|
|
|
{
|
|
|
|
|
title: "概览",
|
|
|
|
|
items: [
|
|
|
|
|
{ href: "/", icon: Home, label: "首页" },
|
|
|
|
|
{ href: "/statistics", icon: BarChart3, label: "数据统计" },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "客户",
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
href: "/user",
|
|
|
|
|
icon: Users,
|
|
|
|
|
label: "客户认领",
|
2026-04-08 13:41:12 +08:00
|
|
|
requiredScope: ScopeUserReadNotBind,
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-04-09 17:08:59 +08:00
|
|
|
href: "/client/cust",
|
2026-04-08 13:41:12 +08:00
|
|
|
icon: ScanSearch,
|
|
|
|
|
label: "客户查询",
|
2026-04-01 13:14:28 +08:00
|
|
|
requiredScope: ScopeUserReadOne,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/cust",
|
|
|
|
|
icon: ContactRound,
|
|
|
|
|
label: "客户管理",
|
|
|
|
|
requiredScope: ScopeUserRead,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/trade",
|
|
|
|
|
icon: Activity,
|
|
|
|
|
label: "交易明细",
|
|
|
|
|
requiredScope: ScopeTradeRead,
|
|
|
|
|
},
|
2026-04-11 14:57:45 +08:00
|
|
|
{
|
|
|
|
|
href: "/balance",
|
|
|
|
|
icon: CircleDollarSign,
|
|
|
|
|
label: "余额明细",
|
|
|
|
|
requiredScope: ScopeBalanceActivity,
|
|
|
|
|
},
|
2026-04-01 13:14:28 +08:00
|
|
|
{
|
|
|
|
|
href: "/billing",
|
|
|
|
|
icon: DollarSign,
|
|
|
|
|
label: "账单详情",
|
|
|
|
|
requiredScope: ScopeBillRead,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "运营",
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
href: "/product",
|
|
|
|
|
icon: ShoppingBag,
|
|
|
|
|
label: "产品管理",
|
|
|
|
|
requiredScope: ScopeProductRead,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/discount",
|
|
|
|
|
icon: SquarePercent,
|
|
|
|
|
label: "折扣管理",
|
|
|
|
|
requiredScope: ScopeDiscountRead,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/coupon",
|
|
|
|
|
icon: TicketPercent,
|
|
|
|
|
label: "优惠券",
|
|
|
|
|
requiredScope: ScopeCouponRead,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/resources",
|
|
|
|
|
icon: Package,
|
|
|
|
|
label: "套餐管理",
|
|
|
|
|
requiredScope: ScopeResourceRead,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/batch",
|
|
|
|
|
icon: ClipboardList,
|
|
|
|
|
label: "提取记录",
|
|
|
|
|
requiredScope: ScopeBatchRead,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/channel",
|
2026-04-09 17:08:59 +08:00
|
|
|
icon: FolderCode,
|
2026-04-01 13:14:28 +08:00
|
|
|
label: "IP管理",
|
|
|
|
|
requiredScope: ScopeChannelRead,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "系统",
|
|
|
|
|
items: [
|
2026-04-18 17:41:27 +08:00
|
|
|
{
|
|
|
|
|
href: "/gateway",
|
|
|
|
|
icon: DoorClosedIcon,
|
|
|
|
|
label: "网关列表",
|
2026-04-18 17:58:40 +08:00
|
|
|
requiredScope:ScopeProxyRead
|
2026-04-18 17:41:27 +08:00
|
|
|
},
|
2026-04-01 13:14:28 +08:00
|
|
|
{
|
|
|
|
|
href: "/admin",
|
|
|
|
|
icon: Shield,
|
|
|
|
|
label: "管理员",
|
|
|
|
|
requiredScope: ScopeAdminRead,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/roles",
|
|
|
|
|
icon: KeyRound,
|
|
|
|
|
label: "角色列表",
|
|
|
|
|
requiredScope: ScopeAdminRoleRead,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: "/permissions",
|
|
|
|
|
icon: Shield,
|
|
|
|
|
label: "权限列表",
|
|
|
|
|
requiredScope: ScopePermissionRead,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
2025-12-29 14:37:03 +08:00
|
|
|
// Main Navigation Component
|
|
|
|
|
export default function Navigation() {
|
2025-12-29 10:41:23 +08:00
|
|
|
const [collapsed, setCollapsed] = useState(false)
|
|
|
|
|
const pathname = usePathname()
|
|
|
|
|
const isActive = (path: string) => {
|
2026-04-09 17:08:59 +08:00
|
|
|
if (path === "/") {
|
|
|
|
|
return pathname === path
|
|
|
|
|
}
|
|
|
|
|
return pathname === path || pathname.startsWith(path + "/")
|
2025-12-29 14:37:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const contextValue: NavigationContextType = {
|
|
|
|
|
collapsed,
|
|
|
|
|
pathname,
|
|
|
|
|
isActive,
|
2025-12-29 10:41:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-29 18:01:16 +08:00
|
|
|
<TooltipProvider delayDuration={0}>
|
|
|
|
|
<NavigationContext.Provider value={contextValue}>
|
|
|
|
|
<aside
|
|
|
|
|
className={twJoin(
|
2026-04-09 17:08:59 +08:00
|
|
|
"bg-background border-r border-border transition-all duration-300 ease-in-out flex flex-col h-full",
|
2025-12-29 18:01:16 +08:00
|
|
|
collapsed ? "w-16" : "w-64",
|
2025-12-29 14:37:03 +08:00
|
|
|
)}
|
2025-12-29 18:01:16 +08:00
|
|
|
>
|
2026-04-09 17:08:59 +08:00
|
|
|
{/*Logo 区域 */}
|
|
|
|
|
<div className="h-16 flex items-center justify-center border-b border-border p-4 shrink-0">
|
2025-12-29 18:01:16 +08:00
|
|
|
{!collapsed ? (
|
|
|
|
|
<span className="text-xl font-bold tracking-wide text-foreground">
|
|
|
|
|
管理系统
|
|
|
|
|
</span>
|
2025-12-29 14:37:03 +08:00
|
|
|
) : (
|
2025-12-29 18:01:16 +08:00
|
|
|
<span className="text-xl font-bold mx-auto text-foreground">
|
|
|
|
|
<ComputerIcon />
|
|
|
|
|
</span>
|
2025-12-29 14:37:03 +08:00
|
|
|
)}
|
2025-12-29 18:01:16 +08:00
|
|
|
</div>
|
|
|
|
|
{/* Navigation Menu */}
|
2026-04-09 17:08:59 +08:00
|
|
|
<ScrollArea className="flex-1 py-3 overflow-hidden">
|
2025-12-29 18:01:16 +08:00
|
|
|
<nav className="space-y-3">
|
2026-04-01 13:14:28 +08:00
|
|
|
{menuSections.map((section, idx) => (
|
|
|
|
|
<div key={section.title}>
|
|
|
|
|
<NavGroup title={section.title}>
|
|
|
|
|
{section.items.map(item => (
|
|
|
|
|
<NavItem key={item.label} {...item} />
|
|
|
|
|
))}
|
|
|
|
|
</NavGroup>
|
|
|
|
|
{idx !== menuSections.length - 1 && <NavSeparator />}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2025-12-29 18:01:16 +08:00
|
|
|
</nav>
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
|
2026-04-09 17:08:59 +08:00
|
|
|
<div className="p-4 border-t border-border mt-auto shrink-0">
|
2025-12-29 18:01:16 +08:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
|
|
|
className="w-full justify-center text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
|
|
|
>
|
|
|
|
|
{collapsed ? (
|
|
|
|
|
<ChevronsRight className="h-5 w-5" />
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<ChevronsLeft className="h-5 w-5" />
|
|
|
|
|
<span className="ml-2 text-sm">收起菜单</span>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</NavigationContext.Provider>
|
|
|
|
|
</TooltipProvider>
|
2025-12-29 10:41:23 +08:00
|
|
|
)
|
|
|
|
|
}
|