236 lines
7.9 KiB
TypeScript
236 lines
7.9 KiB
TypeScript
"use client"
|
|
import {
|
|
BadgeQuestionMarkIcon,
|
|
ChevronDownIcon,
|
|
ChevronRightIcon,
|
|
LogOutIcon,
|
|
SettingsIcon,
|
|
UserIcon,
|
|
} from "lucide-react"
|
|
import Image from "next/image"
|
|
import Link from "next/link"
|
|
import { usePathname, useRouter } from "next/navigation"
|
|
import { useEffect, useRef, useState } from "react"
|
|
import { logout } from "@/actions/auth"
|
|
import { Button } from "@/components/ui/button"
|
|
import type { Admin } from "@/models/admin"
|
|
|
|
export default function Appbar(props: { admin: Admin }) {
|
|
const router = useRouter()
|
|
const [showDropdown, setShowDropdown] = useState(false)
|
|
const [showNotifications, setShowNotifications] = useState(false)
|
|
|
|
const pathname = usePathname()
|
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
const notificationRef = useRef<HTMLDivElement>(null)
|
|
|
|
// 处理点击外部关闭下拉菜单
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node)
|
|
) {
|
|
setShowDropdown(false)
|
|
}
|
|
if (
|
|
notificationRef.current &&
|
|
!notificationRef.current.contains(event.target as Node)
|
|
) {
|
|
setShowNotifications(false)
|
|
}
|
|
}
|
|
|
|
document.addEventListener("mousedown", handleClickOutside)
|
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
}, [])
|
|
|
|
const generateBreadcrumbs = () => {
|
|
const paths = pathname.split("/").filter(Boolean)
|
|
const hiddenSegments = ["client"]
|
|
const filteredPaths = paths.filter(path => !hiddenSegments.includes(path))
|
|
const breadcrumbs = [
|
|
{ path: "/", label: "首页" },
|
|
...filteredPaths.map((path, index) => {
|
|
const originalIndex = paths.findIndex(p => p === path)
|
|
const url = `/${paths.slice(0, originalIndex + 1).join("/")}`
|
|
const label = getBreadcrumbLabel(path)
|
|
return { path: url, label }
|
|
}),
|
|
]
|
|
|
|
return breadcrumbs
|
|
}
|
|
|
|
const getBreadcrumbLabel = (path: string) => {
|
|
const labels: Record<string, string> = {
|
|
dashboard: "控制台",
|
|
content: "内容管理",
|
|
articles: "文章管理",
|
|
media: "媒体库",
|
|
user: "客户认领",
|
|
roles: "角色权限",
|
|
settings: "系统设置",
|
|
logs: "系统日志",
|
|
proxy: "",
|
|
nodes: "节点列表",
|
|
trade: "交易明细",
|
|
billing: "账单详情",
|
|
cust: "客户管理",
|
|
product: "产品管理",
|
|
resources: "套餐管理",
|
|
batch: "提取记录",
|
|
channel: "IP管理",
|
|
pools: "IP池管理",
|
|
coupon: "优惠券",
|
|
admin: "管理员",
|
|
permissions: "权限列表",
|
|
discount: "折扣管理",
|
|
statistics: "数据统计",
|
|
balance: "余额明细",
|
|
}
|
|
|
|
return labels[path] || path
|
|
}
|
|
|
|
const breadcrumbs = generateBreadcrumbs()
|
|
const doLogout = async () => {
|
|
const resp = await logout()
|
|
if (resp.success) {
|
|
router.replace("/")
|
|
router.refresh()
|
|
}
|
|
}
|
|
|
|
return (
|
|
<header className="bg-white h-16 border-b border-gray-200 flex items-center justify-between px-6">
|
|
{/* 面包屑导航 */}
|
|
<div className="flex items-center text-sm">
|
|
{breadcrumbs.map((crumb, index) => (
|
|
<div key={crumb.path} className="flex items-center">
|
|
{index > 0 && (
|
|
<ChevronRightIcon size={18} className="text-gray-400" />
|
|
)}
|
|
<Link
|
|
href={crumb.path}
|
|
className={
|
|
index === breadcrumbs.length - 1
|
|
? "text-gray-800 font-medium"
|
|
: "text-gray-500 hover:text-gray-700"
|
|
}
|
|
>
|
|
{crumb.label}
|
|
</Link>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* 右侧用户信息和工具栏 */}
|
|
<div className="flex items-center space-x-4">
|
|
{/* 用户下拉菜单 */}
|
|
<div className="relative" ref={dropdownRef}>
|
|
<Button
|
|
onClick={() => setShowDropdown(!showDropdown)}
|
|
className="flex items-center space-x-2 rounded-lg text-gray-800 bg-gray-100 hover:bg-gray-100 p-2 transition-colors"
|
|
aria-label="用户菜单"
|
|
>
|
|
<div className="h-8 w-8 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center overflow-hidden border-2 border-white shadow-sm">
|
|
{props.admin ? (
|
|
props.admin.avatar ? (
|
|
<Image
|
|
src={props.admin.avatar}
|
|
alt="用户头像"
|
|
width={32}
|
|
height={32}
|
|
className="h-full w-full object-cover"
|
|
onError={e => {
|
|
const target = e.target as HTMLImageElement
|
|
target.style.display = "none"
|
|
const parent = target.parentElement
|
|
if (parent && props.admin?.name) {
|
|
parent.textContent = props.admin.name
|
|
.charAt(0)
|
|
.toUpperCase()
|
|
}
|
|
}}
|
|
/>
|
|
) : (
|
|
// 如果没有头像,直接显示用户名首字母
|
|
<span className="text-sm font-semibold">
|
|
{props.admin.name?.charAt(0).toUpperCase()}
|
|
</span>
|
|
)
|
|
) : (
|
|
// 加载状态或用户信息为空时
|
|
<UserIcon size={18} />
|
|
)}
|
|
</div>
|
|
<div className="hidden md:block text-left">
|
|
{props.admin && (
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-800">
|
|
{props.admin.name}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{props.admin.username}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<ChevronDownIcon />
|
|
</Button>
|
|
|
|
{/* 用户下拉内容 */}
|
|
{showDropdown && (
|
|
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg py-2 z-20 border border-gray-200">
|
|
{props.admin && (
|
|
<div className="px-4 py-2 border-b border-gray-100 md:hidden">
|
|
<p className="font-medium text-gray-800">
|
|
{props.admin.name}
|
|
</p>
|
|
|
|
<p className="text-xs text-gray-500">{props.admin.name}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="py-1">
|
|
<Link
|
|
href="/profile"
|
|
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
<UserIcon size={18} />
|
|
<span className="pl-3">个人资料</span>
|
|
</Link>
|
|
<Link
|
|
href="/settings/account"
|
|
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
<SettingsIcon size={18} />
|
|
<span className="pl-3">账号设置</span>
|
|
</Link>
|
|
<Link
|
|
href="/system/help"
|
|
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
|
>
|
|
<BadgeQuestionMarkIcon size={18} />
|
|
<span className="pl-3">帮助中心</span>
|
|
</Link>
|
|
</div>
|
|
<div className="border-t border-gray-100 pt-1">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={doLogout}
|
|
className="flex items-center justify-start px-4 py-2 w-full text-sm text-red-600 hover:text-red-700 hover:bg-gray-100 font-normal"
|
|
>
|
|
<LogOutIcon size={18} className="ml-2" />
|
|
退出登录
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|