refactor(dashboard): extract RoleGuard and unify mobile nav with Button
This commit is contained in:
@@ -42,10 +42,6 @@ export default function ServicesPage() {
|
|||||||
const scopedPlayerIdSet = new Set(scopedPlayerIds)
|
const scopedPlayerIdSet = new Set(scopedPlayerIds)
|
||||||
const scopedServices = services.filter((service) => scopedPlayerIdSet.has(service.playerId))
|
const scopedServices = services.filter((service) => scopedPlayerIdSet.has(service.playerId))
|
||||||
|
|
||||||
if (currentRole !== "player" && currentRole !== "owner") {
|
|
||||||
return <div className="text-sm text-muted-foreground">当前身份不可管理服务</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-8">
|
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from "next/link"
|
|
||||||
import { AuthGuard } from "@/components/auth-guard"
|
import { AuthGuard } from "@/components/auth-guard"
|
||||||
import { DashboardSidebar } from "@/components/dashboard-sidebar"
|
import { DashboardSidebar } from "@/components/dashboard-sidebar"
|
||||||
import { Header } from "@/components/header"
|
import { Header } from "@/components/header"
|
||||||
import { Button } from "@/components/ui/button"
|
import { RoleGuard } from "@/components/role-guard"
|
||||||
import { useAuthStore } from "@/store/auth"
|
|
||||||
|
|
||||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
|
|
||||||
const currentRole = useAuthStore((state) => state.currentRole)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
@@ -20,15 +15,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
</div>
|
</div>
|
||||||
<main className="flex-1 p-6">
|
<main className="flex-1 p-6">
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
{isAuthenticated && currentRole === "consumer" ? (
|
<RoleGuard>{children}</RoleGuard>
|
||||||
<div className="flex min-h-[50vh] items-center justify-center">
|
|
||||||
<Button asChild>
|
|
||||||
<Link href="/">发现</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
|
||||||
const playerLinks = [
|
export const playerLinks = [
|
||||||
{ href: "/dashboard", label: "概览", icon: LayoutDashboard },
|
{ href: "/dashboard", label: "概览", icon: LayoutDashboard },
|
||||||
{ href: "/dashboard/services", label: "服务管理", icon: ListOrdered },
|
{ href: "/dashboard/services", label: "服务管理", icon: ListOrdered },
|
||||||
]
|
]
|
||||||
|
|
||||||
const ownerLinks = [
|
export const ownerLinks = [
|
||||||
{ href: "/dashboard", label: "概览", icon: LayoutDashboard },
|
{ href: "/dashboard", label: "概览", icon: LayoutDashboard },
|
||||||
{ href: "/dashboard/services", label: "服务管理", icon: ListOrdered },
|
{ href: "/dashboard/services", label: "服务管理", icon: ListOrdered },
|
||||||
{ href: "/dashboard/shop", label: "店铺管理", icon: Store },
|
{ href: "/dashboard/shop", label: "店铺管理", icon: Store },
|
||||||
|
|||||||
+36
-20
@@ -33,6 +33,8 @@ import { useAuthStore } from "@/store/auth"
|
|||||||
import { useNotificationStore } from "@/store/notifications"
|
import { useNotificationStore } from "@/store/notifications"
|
||||||
import { useShopStore } from "@/store/shops"
|
import { useShopStore } from "@/store/shops"
|
||||||
|
|
||||||
|
import { canAccessDashboard } from "@/components/role-guard"
|
||||||
|
|
||||||
const roleLabels: Record<UserRole, string> = {
|
const roleLabels: Record<UserRole, string> = {
|
||||||
consumer: "客户",
|
consumer: "客户",
|
||||||
player: "打手",
|
player: "打手",
|
||||||
@@ -53,6 +55,7 @@ export function Header() {
|
|||||||
? [
|
? [
|
||||||
{ href: "/", label: "发现" },
|
{ href: "/", label: "发现" },
|
||||||
{ href: "/community", label: "社区" },
|
{ href: "/community", label: "社区" },
|
||||||
|
{ href: "/orders", label: "订单" },
|
||||||
{ href: "/chat", label: "消息" },
|
{ href: "/chat", label: "消息" },
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@@ -69,7 +72,9 @@ export function Header() {
|
|||||||
|
|
||||||
const handleRoleSwitch = (role: UserRole) => {
|
const handleRoleSwitch = (role: UserRole) => {
|
||||||
switchRole(role)
|
switchRole(role)
|
||||||
|
if (pathname.startsWith("/dashboard") && !canAccessDashboard(role, pathname)) {
|
||||||
router.push(role === "consumer" ? "/" : "/dashboard")
|
router.push(role === "consumer" ? "/" : "/dashboard")
|
||||||
|
}
|
||||||
setMobileOpen(false)
|
setMobileOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +252,7 @@ export function Header() {
|
|||||||
<Menu className="h-5 w-5" />
|
<Menu className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="left" className="w-72">
|
<SheetContent side="right" className="w-72">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle className="flex items-center gap-2">
|
<SheetTitle className="flex items-center gap-2">
|
||||||
<Gamepad2 className="h-5 w-5" />
|
<Gamepad2 className="h-5 w-5" />
|
||||||
@@ -269,38 +274,49 @@ export function Header() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<nav className="flex flex-col gap-1">
|
<nav className="flex flex-col gap-1">
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => {
|
||||||
<Link
|
const isActive =
|
||||||
key={link.href}
|
|
||||||
href={link.href}
|
|
||||||
onClick={() => setMobileOpen(false)}
|
|
||||||
className={cn(
|
|
||||||
"px-3 py-2 rounded-full text-sm font-medium transition-colors",
|
|
||||||
pathname === link.href ||
|
pathname === link.href ||
|
||||||
(link.href !== "/" && pathname.startsWith(link.href))
|
(link.href !== "/" && pathname.startsWith(link.href))
|
||||||
? "bg-primary/10 text-primary"
|
return (
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
<Button
|
||||||
)}
|
key={link.href}
|
||||||
|
variant={isActive ? "secondary" : "ghost"}
|
||||||
|
className={cn("w-full justify-start", !isActive && "text-muted-foreground")}
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
|
<Link href={link.href} onClick={() => setMobileOpen(false)}>
|
||||||
{link.label}
|
{link.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Button
|
||||||
href="/wallet"
|
variant={pathname === "/wallet" ? "secondary" : "ghost"}
|
||||||
onClick={() => setMobileOpen(false)}
|
className={cn(
|
||||||
className="px-3 py-2 rounded-full text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
"w-full justify-start",
|
||||||
|
pathname !== "/wallet" && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
|
<Link href="/wallet" onClick={() => setMobileOpen(false)}>
|
||||||
钱包
|
钱包
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
</Button>
|
||||||
href="/settings"
|
<Button
|
||||||
onClick={() => setMobileOpen(false)}
|
variant={pathname === "/settings" ? "secondary" : "ghost"}
|
||||||
className="px-3 py-2 rounded-full text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
className={cn(
|
||||||
|
"w-full justify-start",
|
||||||
|
pathname !== "/settings" && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
|
<Link href="/settings" onClick={() => setMobileOpen(false)}>
|
||||||
设置
|
设置
|
||||||
</Link>
|
</Link>
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
import { ownerLinks, playerLinks } from "@/components/dashboard-sidebar"
|
||||||
|
import type { UserRole } from "@/lib/types"
|
||||||
|
import { useAuthStore } from "@/store/auth"
|
||||||
|
|
||||||
|
const dashboardRoutes: Record<string, readonly { href: string }[]> = {
|
||||||
|
player: playerLinks,
|
||||||
|
owner: ownerLinks,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canAccessDashboard(role: UserRole, pathname: string) {
|
||||||
|
const routes = dashboardRoutes[role]
|
||||||
|
if (!routes) return false
|
||||||
|
return routes.some((link) => pathname === link.href || pathname.startsWith(link.href + "/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoleGuard({ children }: { children: React.ReactNode }) {
|
||||||
|
const currentRole = useAuthStore((state) => state.currentRole)
|
||||||
|
const pathname = usePathname()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const allowed = canAccessDashboard(currentRole, pathname)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!allowed) {
|
||||||
|
router.replace(currentRole === "consumer" ? "/" : "/dashboard")
|
||||||
|
}
|
||||||
|
}, [allowed, currentRole, pathname, router])
|
||||||
|
|
||||||
|
if (!allowed) return null
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user