Files
admin/src/app/(root)/appbar.tsx

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>
)
}