d0d21fa935
Unify user-facing role terminology by replacing 消费者 with 客户 while keeping role keys unchanged. This aligns account settings, header role switch, global role labels, and order list role text.
351 lines
13 KiB
TypeScript
351 lines
13 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
Bell,
|
|
Gamepad2,
|
|
LogOut,
|
|
Menu,
|
|
MessageSquare,
|
|
Search,
|
|
Settings,
|
|
ShoppingBag,
|
|
User,
|
|
Wallet,
|
|
} from "lucide-react"
|
|
import Link from "next/link"
|
|
import { usePathname, useRouter } from "next/navigation"
|
|
import { useState } from "react"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
|
import type { UserRole } from "@/lib/types"
|
|
import { cn } from "@/lib/utils"
|
|
import { useAuthStore } from "@/store/auth"
|
|
import { useNotificationStore } from "@/store/notifications"
|
|
import { useShopStore } from "@/store/shops"
|
|
|
|
const roleLabels: Record<UserRole, string> = {
|
|
consumer: "客户",
|
|
player: "打手",
|
|
owner: "店主",
|
|
}
|
|
|
|
export function Header() {
|
|
const [mobileOpen, setMobileOpen] = useState(false)
|
|
const pathname = usePathname()
|
|
const router = useRouter()
|
|
const { isAuthenticated, currentRole, verifiedRoles, switchRole, logout, user } = useAuthStore()
|
|
const ownerShop = useShopStore((state) =>
|
|
user ? state.shops.find((shop) => shop.owner.id === user.id) : undefined,
|
|
)
|
|
|
|
const navLinks =
|
|
currentRole === "consumer"
|
|
? [
|
|
{ href: "/", label: "首页" },
|
|
{ href: "/search", label: "找陪玩" },
|
|
{ href: "/community", label: "社区" },
|
|
{ href: "/chat", label: "消息" },
|
|
]
|
|
: [
|
|
{ href: "/", label: "首页" },
|
|
{ href: "/community", label: "社区" },
|
|
{ href: "/orders", label: "订单" },
|
|
{ href: "/chat", label: "消息" },
|
|
{ href: "/dashboard", label: "管理后台" },
|
|
]
|
|
|
|
const availableRoles = (Object.entries(roleLabels) as [UserRole, string][]).filter(([role]) =>
|
|
verifiedRoles.includes(role),
|
|
)
|
|
|
|
const handleRoleSwitch = (role: UserRole) => {
|
|
switchRole(role)
|
|
router.push(role === "consumer" ? "/" : "/dashboard")
|
|
setMobileOpen(false)
|
|
}
|
|
|
|
const unreadCount = useNotificationStore(
|
|
(state) => state.notifications.filter((notification) => !notification.read).length,
|
|
)
|
|
|
|
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault()
|
|
const formData = new FormData(e.currentTarget)
|
|
const q = formData.get("q") as string
|
|
if (q.trim()) router.push(`/search?q=${encodeURIComponent(q.trim())}`)
|
|
}
|
|
|
|
const profileHref =
|
|
currentRole === "player"
|
|
? user
|
|
? `/player/${user.id}`
|
|
: "/settings"
|
|
: currentRole === "owner"
|
|
? `/shop/${ownerShop?.id ?? "shop1"}`
|
|
: user
|
|
? `/user/${user.id}`
|
|
: "/settings"
|
|
|
|
return (
|
|
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="container mx-auto flex h-14 items-center gap-4 px-4">
|
|
<Link href="/" className="flex items-center gap-2 font-bold text-lg shrink-0">
|
|
<Gamepad2 className="h-5 w-5" />
|
|
聚玩
|
|
</Link>
|
|
|
|
<nav className="hidden md:flex items-center gap-1">
|
|
{navLinks.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
className={cn(
|
|
"px-3 py-1.5 rounded-md text-sm font-medium transition-colors",
|
|
pathname === link.href || (link.href !== "/" && pathname.startsWith(link.href))
|
|
? "bg-accent text-accent-foreground"
|
|
: "text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
)}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
|
|
<form onSubmit={handleSearch} className="hidden md:flex flex-1 max-w-sm">
|
|
<div className="relative w-full">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input name="q" placeholder="搜索打手、店铺..." className="pl-8 h-9" />
|
|
</div>
|
|
</form>
|
|
|
|
<div className="flex flex-1 md:flex-none items-center justify-end gap-1">
|
|
{isAuthenticated ? (
|
|
<>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm" className="hidden md:flex text-xs h-8">
|
|
{roleLabels[currentRole]}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>切换身份</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{availableRoles.map(([role, label]) => (
|
|
<DropdownMenuItem
|
|
key={role}
|
|
onClick={() => handleRoleSwitch(role)}
|
|
className={cn(currentRole === role && "bg-accent")}
|
|
>
|
|
{label}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
<Button variant="ghost" size="icon" className="relative h-9 w-9" asChild>
|
|
<Link href="/notifications">
|
|
<Bell className="h-4 w-4" />
|
|
{unreadCount > 0 && (
|
|
<Badge className="absolute -top-1 -right-1 h-4 min-w-4 px-1 flex items-center justify-center text-[10px]">
|
|
{unreadCount}
|
|
</Badge>
|
|
)}
|
|
</Link>
|
|
</Button>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="rounded-full h-9 w-9">
|
|
<Avatar className="h-7 w-7">
|
|
<AvatarImage src={user?.avatar} />
|
|
<AvatarFallback>{user?.nickname?.[0] ?? "?"}</AvatarFallback>
|
|
</Avatar>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48">
|
|
<DropdownMenuLabel>{user?.nickname ?? "未登录"}</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem asChild>
|
|
<Link href={profileHref}>
|
|
<User className="mr-2 h-4 w-4" />
|
|
个人主页
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/orders">
|
|
<ShoppingBag className="mr-2 h-4 w-4" />
|
|
我的订单
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/chat">
|
|
<MessageSquare className="mr-2 h-4 w-4" />
|
|
消息
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/wallet">
|
|
<Wallet className="mr-2 h-4 w-4" />
|
|
钱包
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
{(currentRole === "player" || currentRole === "owner") && (
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/dashboard">
|
|
<Gamepad2 className="mr-2 h-4 w-4" />
|
|
管理后台
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem asChild>
|
|
<Link href="/settings">
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
设置
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuGroup>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={logout}>
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
退出登录
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</>
|
|
) : (
|
|
<div className="hidden md:flex items-center gap-2">
|
|
<Button variant="ghost" size="sm" asChild>
|
|
<Link href="/login">登录</Link>
|
|
</Button>
|
|
<Button size="sm" asChild>
|
|
<Link href="/register">注册</Link>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
|
<SheetTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="md:hidden h-9 w-9">
|
|
<Menu className="h-5 w-5" />
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="left" className="w-72">
|
|
<SheetHeader>
|
|
<SheetTitle className="flex items-center gap-2">
|
|
<Gamepad2 className="h-5 w-5" />
|
|
聚玩
|
|
</SheetTitle>
|
|
</SheetHeader>
|
|
<div className="mt-4 space-y-4">
|
|
<form onSubmit={handleSearch}>
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input name="q" placeholder="搜索打手、店铺..." className="pl-8" />
|
|
</div>
|
|
</form>
|
|
|
|
{isAuthenticated && (
|
|
<div className="flex items-center gap-2 px-1">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage src={user?.avatar} />
|
|
<AvatarFallback>{user?.nickname?.[0] ?? "?"}</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{user?.nickname ?? "未登录"}</p>
|
|
<p className="text-xs text-muted-foreground">{roleLabels[currentRole]}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<nav className="flex flex-col gap-1">
|
|
{navLinks.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
onClick={() => setMobileOpen(false)}
|
|
className={cn(
|
|
"px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
|
pathname === link.href ||
|
|
(link.href !== "/" && pathname.startsWith(link.href))
|
|
? "bg-accent text-accent-foreground"
|
|
: "text-muted-foreground hover:text-foreground hover:bg-accent/50",
|
|
)}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
))}
|
|
{isAuthenticated && (
|
|
<>
|
|
<Link
|
|
href="/wallet"
|
|
onClick={() => setMobileOpen(false)}
|
|
className="px-3 py-2 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
|
>
|
|
钱包
|
|
</Link>
|
|
<Link
|
|
href="/settings"
|
|
onClick={() => setMobileOpen(false)}
|
|
className="px-3 py-2 rounded-md text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
|
>
|
|
设置
|
|
</Link>
|
|
</>
|
|
)}
|
|
</nav>
|
|
|
|
{isAuthenticated && (
|
|
<div className="border-t pt-4">
|
|
<p className="px-3 text-xs text-muted-foreground mb-2">切换身份</p>
|
|
<div className="flex gap-1 px-3">
|
|
{availableRoles.map(([role, label]) => (
|
|
<Button
|
|
key={role}
|
|
variant={currentRole === role ? "default" : "outline"}
|
|
size="sm"
|
|
className="text-xs flex-1"
|
|
onClick={() => handleRoleSwitch(role)}
|
|
>
|
|
{label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{!isAuthenticated && (
|
|
<div className="border-t pt-4 flex gap-2 px-3">
|
|
<Button variant="outline" className="flex-1" asChild>
|
|
<Link href="/login" onClick={() => setMobileOpen(false)}>
|
|
登录
|
|
</Link>
|
|
</Button>
|
|
<Button className="flex-1" asChild>
|
|
<Link href="/register" onClick={() => setMobileOpen(false)}>
|
|
注册
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|