116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useRef, useState } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { Diamond, ChevronDown, LogOut, Crown } from 'lucide-react';
|
||
import { useAdminStore } from '@/store/adminStore';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Avatar } from './Avatar';
|
||
|
||
/**
|
||
* 全局顶栏:左侧品牌 + 右侧用户菜单(头像 + 下拉含退出登录)。
|
||
* 在所有 /admin/* 路由(除 /admin/login)渲染。
|
||
*/
|
||
export function TopBar() {
|
||
const router = useRouter();
|
||
const admin = useAdminStore((s) => s.admin);
|
||
const [menuOpen, setMenuOpen] = useState(false);
|
||
const menuRef = useRef<HTMLDivElement>(null);
|
||
|
||
// 下拉打开时:监听外部点击 + Esc 键关闭
|
||
useEffect(() => {
|
||
if (!menuOpen) return;
|
||
const onMouseDown = (e: MouseEvent) => {
|
||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||
setMenuOpen(false);
|
||
}
|
||
};
|
||
const onKeyDown = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') setMenuOpen(false);
|
||
};
|
||
document.addEventListener('mousedown', onMouseDown);
|
||
document.addEventListener('keydown', onKeyDown);
|
||
return () => {
|
||
document.removeEventListener('mousedown', onMouseDown);
|
||
document.removeEventListener('keydown', onKeyDown);
|
||
};
|
||
}, [menuOpen]);
|
||
|
||
const handleLogout = () => {
|
||
setMenuOpen(false);
|
||
useAdminStore.getState().logout();
|
||
router.replace('/admin/login');
|
||
};
|
||
|
||
const nickname = admin?.nickname ?? admin?.username ?? '管理员';
|
||
const username = admin?.username ?? 'admin';
|
||
const isSuperAdmin = admin?.role === 'super_admin';
|
||
|
||
return (
|
||
<header className="sticky top-0 z-40 flex h-16 items-center justify-between border-b border-gray-200 bg-white/95 px-6 backdrop-blur">
|
||
<div className="flex items-center gap-2">
|
||
<Diamond className="h-6 w-6 text-brand-600" />
|
||
<span className="text-base font-semibold text-gray-900">
|
||
企业官网后台
|
||
</span>
|
||
</div>
|
||
|
||
<div className="relative" ref={menuRef}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setMenuOpen((v) => !v)}
|
||
className="flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-gray-100"
|
||
aria-haspopup="menu"
|
||
aria-expanded={menuOpen}
|
||
>
|
||
<Avatar src={admin?.avatar} name={nickname} size={32} />
|
||
<span className="text-sm font-medium text-gray-700">{nickname}</span>
|
||
{isSuperAdmin && (
|
||
<Badge variant="warning">
|
||
<Crown className="h-3 w-3" /> 超管
|
||
</Badge>
|
||
)}
|
||
<ChevronDown className="h-4 w-4 text-gray-500" />
|
||
</button>
|
||
|
||
{menuOpen && (
|
||
<div
|
||
role="menu"
|
||
className="absolute right-0 top-full mt-2 w-60 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg"
|
||
>
|
||
<div className="border-b border-gray-100 px-4 py-3">
|
||
<div className="flex items-center gap-3">
|
||
<Avatar src={admin?.avatar} name={nickname} size={40} />
|
||
<div className="min-w-0">
|
||
<p className="truncate text-sm font-medium text-gray-900">
|
||
{nickname}
|
||
</p>
|
||
<p className="truncate text-xs text-gray-500">@{username}</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-2">
|
||
{isSuperAdmin ? (
|
||
<Badge variant="warning">
|
||
<Crown className="h-3 w-3" /> 超级管理员
|
||
</Badge>
|
||
) : (
|
||
<Badge variant="secondary">普通管理员</Badge>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
role="menuitem"
|
||
onClick={handleLogout}
|
||
className="flex w-full items-center gap-2 px-4 py-2.5 text-left text-sm text-red-600 transition-colors hover:bg-red-50"
|
||
>
|
||
<LogOut className="h-4 w-4" />
|
||
退出登录
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</header>
|
||
);
|
||
}
|