修整优惠券页面,添加发放功能及权限控制 & 用户添加查看优惠券发放详情 & 添加切换线上环境页面显示功能
This commit is contained in:
348
src/app/(root)/_navigation/index.tsx
Normal file
348
src/app/(root)/_navigation/index.tsx
Normal file
@@ -0,0 +1,348 @@
|
||||
"use client"
|
||||
import {
|
||||
Activity,
|
||||
BarChart3,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
CircleDollarSign,
|
||||
ClipboardList,
|
||||
ComputerIcon,
|
||||
ContactRound,
|
||||
DollarSign,
|
||||
DoorClosedIcon,
|
||||
FolderCode,
|
||||
Home,
|
||||
KeyRound,
|
||||
type LucideIcon,
|
||||
Package,
|
||||
ScanSearch,
|
||||
Shield,
|
||||
ShoppingBag,
|
||||
SquarePercent,
|
||||
TicketPercent,
|
||||
Users,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { createContext, type ReactNode, useContext, useState } from "react"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import { Auth } from "@/components/auth"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import {
|
||||
ScopeAdminRead,
|
||||
ScopeAdminRoleRead,
|
||||
ScopeBalanceActivity,
|
||||
ScopeBatchRead,
|
||||
ScopeBillRead,
|
||||
ScopeChannelRead,
|
||||
ScopeCouponRead,
|
||||
ScopeDiscountRead,
|
||||
ScopePermissionRead,
|
||||
ScopeProductRead,
|
||||
ScopeProxyRead,
|
||||
ScopeResourceRead,
|
||||
ScopeTradeRead,
|
||||
ScopeUserRead,
|
||||
ScopeUserReadNotBind,
|
||||
ScopeUserReadOne,
|
||||
} from "@/lib/scopes"
|
||||
import Logo from "./logo"
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NavGroup Component
|
||||
interface NavGroupProps {
|
||||
title: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function NavGroup({ title, children }: NavGroupProps) {
|
||||
const { collapsed } = useNavigation()
|
||||
|
||||
return (
|
||||
<div className="px-3">
|
||||
{!collapsed && (
|
||||
<h3 className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
<ul className={`${collapsed ? "mt-0" : "mt-2"} space-y-1`}>{children}</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// NavItem Component
|
||||
interface NavItemProps {
|
||||
href: string
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
requiredScope?: string
|
||||
}
|
||||
|
||||
function NavItem({ href, icon: Icon, label, requiredScope }: NavItemProps) {
|
||||
const { collapsed, isActive } = useNavigation()
|
||||
const active = isActive(href)
|
||||
|
||||
let linkContent = (
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
<Icon className={`h-5 w-5 ${collapsed ? "" : "shrink-0"}`} />
|
||||
{!collapsed && <span className="ml-3 font-medium text-sm">{label}</span>}
|
||||
</Link>
|
||||
)
|
||||
|
||||
if (collapsed) {
|
||||
linkContent = (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>{label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (requiredScope) {
|
||||
linkContent = (
|
||||
<Auth scope={requiredScope}>
|
||||
<li>{linkContent}</li>
|
||||
</Auth>
|
||||
)
|
||||
}
|
||||
|
||||
return linkContent
|
||||
}
|
||||
|
||||
// NavSeparator Component
|
||||
function NavSeparator() {
|
||||
const { collapsed } = useNavigation()
|
||||
|
||||
if (collapsed) return null
|
||||
|
||||
return (
|
||||
<div className="my-4">
|
||||
<Separator />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const menuSections: { title: string; items: NavItemProps[] }[] = [
|
||||
{
|
||||
title: "概览",
|
||||
items: [
|
||||
{ href: "/", icon: Home, label: "首页" },
|
||||
{ href: "/statistics", icon: BarChart3, label: "数据统计" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "客户",
|
||||
items: [
|
||||
{
|
||||
href: "/user",
|
||||
icon: Users,
|
||||
label: "客户认领",
|
||||
requiredScope: ScopeUserReadNotBind,
|
||||
},
|
||||
{
|
||||
href: "/client/cust",
|
||||
icon: ScanSearch,
|
||||
label: "客户查询",
|
||||
requiredScope: ScopeUserReadOne,
|
||||
},
|
||||
{
|
||||
href: "/cust",
|
||||
icon: ContactRound,
|
||||
label: "客户管理",
|
||||
requiredScope: ScopeUserRead,
|
||||
},
|
||||
{
|
||||
href: "/trade",
|
||||
icon: Activity,
|
||||
label: "交易明细",
|
||||
requiredScope: ScopeTradeRead,
|
||||
},
|
||||
{
|
||||
href: "/balance",
|
||||
icon: CircleDollarSign,
|
||||
label: "余额明细",
|
||||
requiredScope: ScopeBalanceActivity,
|
||||
},
|
||||
{
|
||||
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",
|
||||
icon: FolderCode,
|
||||
label: "IP管理",
|
||||
requiredScope: ScopeChannelRead,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "系统",
|
||||
items: [
|
||||
{
|
||||
href: "/gateway",
|
||||
icon: DoorClosedIcon,
|
||||
label: "网关列表",
|
||||
requiredScope: ScopeProxyRead,
|
||||
},
|
||||
{
|
||||
href: "/admin",
|
||||
icon: Shield,
|
||||
label: "管理员",
|
||||
requiredScope: ScopeAdminRead,
|
||||
},
|
||||
{
|
||||
href: "/roles",
|
||||
icon: KeyRound,
|
||||
label: "角色列表",
|
||||
requiredScope: ScopeAdminRoleRead,
|
||||
},
|
||||
{
|
||||
href: "/permissions",
|
||||
icon: Shield,
|
||||
label: "权限列表",
|
||||
requiredScope: ScopePermissionRead,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Main Navigation Component
|
||||
export default function Navigation() {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const isActive = (path: string) => {
|
||||
if (path === "/") {
|
||||
return pathname === path
|
||||
}
|
||||
return pathname === path || pathname.startsWith(path + "/")
|
||||
}
|
||||
|
||||
const contextValue: NavigationContextType = {
|
||||
collapsed,
|
||||
pathname,
|
||||
isActive,
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<NavigationContext.Provider value={contextValue}>
|
||||
<aside
|
||||
className={twJoin(
|
||||
"bg-white border-r border-slate-200 transition-all duration-300 ease-in-out flex flex-col h-full",
|
||||
collapsed ? "w-16" : "w-64",
|
||||
)}
|
||||
>
|
||||
{/*Logo 区域 */}
|
||||
<Logo collapsed={collapsed} />
|
||||
|
||||
{/* Navigation Menu */}
|
||||
<ScrollArea className="flex-1 py-3 overflow-hidden">
|
||||
<nav className="space-y-3">
|
||||
{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>
|
||||
))}
|
||||
</nav>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="p-4 border-t border-border mt-auto shrink-0">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
28
src/app/(root)/_navigation/logo.tsx
Normal file
28
src/app/(root)/_navigation/logo.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ComputerIcon } from "lucide-react"
|
||||
import { getNodeEnv } from "@/actions/env"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export default async function Logo(props: { collapsed: boolean }) {
|
||||
const env = await getNodeEnv()
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`h-16 flex items-center justify-between border-b px-4 shrink-0`,
|
||||
env === "production" && "bg-amber-100",
|
||||
)}
|
||||
>
|
||||
{!props.collapsed ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<ComputerIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-xl font-bold tracking-wide">管理系统</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full flex justify-center">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user