2026-06-22 14:43:46 +08:00

116 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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