2025-01-23 23:59:49 +08:00
|
|
|
import { useClickOutside } from "@web/src/hooks/useClickOutside";
|
|
|
|
import { useAuth } from "@web/src/providers/auth-provider";
|
|
|
|
import { motion, AnimatePresence } from "framer-motion";
|
2025-01-24 15:05:03 +08:00
|
|
|
import { useState, useRef, useCallback, useMemo } from "react";
|
2025-01-23 23:59:49 +08:00
|
|
|
import { Avatar } from "../../common/element/Avatar";
|
2025-01-24 15:05:03 +08:00
|
|
|
import {
|
|
|
|
UserOutlined,
|
|
|
|
SettingOutlined,
|
|
|
|
QuestionCircleOutlined,
|
|
|
|
LogoutOutlined
|
|
|
|
} from "@ant-design/icons";
|
|
|
|
import { Spin } from "antd";
|
2025-01-24 17:37:51 +08:00
|
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
import { MenuItemType } from "./types";
|
2025-01-24 15:05:03 +08:00
|
|
|
const menuVariants = {
|
|
|
|
hidden: { opacity: 0, scale: 0.95, y: -10 },
|
|
|
|
visible: {
|
|
|
|
opacity: 1,
|
|
|
|
scale: 1,
|
|
|
|
y: 0,
|
|
|
|
transition: {
|
|
|
|
type: "spring",
|
|
|
|
stiffness: 300,
|
|
|
|
damping: 30
|
|
|
|
}
|
|
|
|
},
|
|
|
|
exit: {
|
|
|
|
opacity: 0,
|
|
|
|
scale: 0.95,
|
|
|
|
y: -10,
|
|
|
|
transition: {
|
|
|
|
duration: 0.2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2025-01-23 23:59:49 +08:00
|
|
|
|
|
|
|
export function UserMenu() {
|
|
|
|
const [showMenu, setShowMenu] = useState(false);
|
|
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
2025-01-24 15:05:03 +08:00
|
|
|
const { user, logout, isLoading } = useAuth();
|
2025-01-24 17:37:51 +08:00
|
|
|
const navigate = useNavigate()
|
2025-01-23 23:59:49 +08:00
|
|
|
useClickOutside(menuRef, () => setShowMenu(false));
|
|
|
|
|
2025-01-24 15:05:03 +08:00
|
|
|
const toggleMenu = useCallback(() => {
|
|
|
|
setShowMenu(prev => !prev);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const menuItems: MenuItemType[] = useMemo(() => [
|
2025-01-23 23:59:49 +08:00
|
|
|
{
|
2025-01-24 15:05:03 +08:00
|
|
|
icon: <UserOutlined className="text-lg" />,
|
2025-01-23 23:59:49 +08:00
|
|
|
label: '个人信息',
|
|
|
|
action: () => { },
|
|
|
|
},
|
|
|
|
{
|
2025-01-24 15:05:03 +08:00
|
|
|
icon: <SettingOutlined className="text-lg" />,
|
2025-01-23 23:59:49 +08:00
|
|
|
label: '设置',
|
2025-01-24 17:37:51 +08:00
|
|
|
action: () => {
|
|
|
|
navigate('/admin/staff')
|
|
|
|
},
|
2025-01-23 23:59:49 +08:00
|
|
|
},
|
|
|
|
{
|
2025-01-24 15:05:03 +08:00
|
|
|
icon: <QuestionCircleOutlined className="text-lg" />,
|
2025-01-23 23:59:49 +08:00
|
|
|
label: '帮助',
|
|
|
|
action: () => { },
|
|
|
|
},
|
|
|
|
{
|
2025-01-24 15:05:03 +08:00
|
|
|
icon: <LogoutOutlined className="text-lg" />,
|
2025-01-23 23:59:49 +08:00
|
|
|
label: '注销',
|
|
|
|
action: () => logout(),
|
|
|
|
},
|
2025-01-24 15:05:03 +08:00
|
|
|
], [logout]);
|
|
|
|
|
|
|
|
const handleMenuItemClick = useCallback((action: () => void) => {
|
|
|
|
action();
|
|
|
|
setShowMenu(false);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
return (
|
|
|
|
<div className="flex items-center justify-center w-10 h-10">
|
|
|
|
<Spin size="small" />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2025-01-23 23:59:49 +08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div ref={menuRef} className="relative">
|
|
|
|
<motion.button
|
2025-01-24 15:05:03 +08:00
|
|
|
aria-label="用户菜单"
|
|
|
|
aria-haspopup="true"
|
|
|
|
aria-expanded={showMenu}
|
|
|
|
aria-controls="user-menu"
|
|
|
|
whileHover={{ scale: 1.02 }}
|
|
|
|
whileTap={{ scale: 0.98 }}
|
|
|
|
onClick={toggleMenu}
|
|
|
|
className="relative rounded-full focus:outline-none
|
|
|
|
focus:ring-2 focus:ring-[#00538E]/80 focus:ring-offset-2
|
|
|
|
focus:ring-offset-white transition-all duration-200 ease-in-out"
|
2025-01-23 23:59:49 +08:00
|
|
|
>
|
|
|
|
<Avatar
|
|
|
|
src={user?.avatar}
|
|
|
|
name={user?.showname || user?.username}
|
|
|
|
size={40}
|
2025-01-24 15:05:03 +08:00
|
|
|
className="ring-2 ring-white hover:ring-[#00538E]/90
|
|
|
|
transition-all duration-200 ease-in-out shadow-md
|
|
|
|
hover:shadow-lg"
|
|
|
|
/>
|
|
|
|
<span
|
|
|
|
className="absolute bottom-0 right-0 h-3 w-3
|
|
|
|
rounded-full bg-emerald-500 ring-2 ring-white
|
|
|
|
shadow-sm transition-transform duration-200
|
|
|
|
ease-in-out hover:scale-110"
|
|
|
|
aria-hidden="true"
|
2025-01-23 23:59:49 +08:00
|
|
|
/>
|
|
|
|
</motion.button>
|
|
|
|
|
|
|
|
<AnimatePresence>
|
|
|
|
{showMenu && (
|
|
|
|
<motion.div
|
2025-01-24 15:05:03 +08:00
|
|
|
initial="hidden"
|
|
|
|
animate="visible"
|
|
|
|
exit="exit"
|
|
|
|
variants={menuVariants}
|
|
|
|
role="menu"
|
|
|
|
id="user-menu"
|
|
|
|
aria-orientation="vertical"
|
|
|
|
aria-labelledby="user-menu-button"
|
2025-01-23 23:59:49 +08:00
|
|
|
style={{ zIndex: 100 }}
|
2025-01-24 15:05:03 +08:00
|
|
|
className="absolute right-0 mt-3 w-64 origin-top-right
|
|
|
|
bg-white rounded-xl overflow-hidden shadow-lg
|
|
|
|
border border-[#E5EDF5]"
|
2025-01-23 23:59:49 +08:00
|
|
|
>
|
2025-01-24 15:05:03 +08:00
|
|
|
{/* User Profile Section */}
|
|
|
|
<div
|
|
|
|
className="px-4 py-4 bg-gradient-to-b from-[#F6F9FC] to-white
|
|
|
|
border-b border-[#E5EDF5] "
|
2025-01-24 17:37:51 +08:00
|
|
|
|
2025-01-24 15:05:03 +08:00
|
|
|
>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
<Avatar
|
|
|
|
src={user?.avatar}
|
|
|
|
name={user?.showname || user?.username}
|
|
|
|
size={40}
|
|
|
|
className="ring-2 ring-white shadow-sm"
|
|
|
|
/>
|
|
|
|
<div className="flex flex-col space-y-0.5">
|
|
|
|
<span className="text-sm font-semibold text-[#00538E]">
|
|
|
|
{user?.showname || user?.username}
|
|
|
|
</span>
|
|
|
|
<span className="text-xs text-[#718096] flex items-center gap-1.5">
|
|
|
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
|
|
在线
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
2025-01-23 23:59:49 +08:00
|
|
|
</div>
|
|
|
|
|
2025-01-24 15:05:03 +08:00
|
|
|
{/* Menu Items */}
|
2025-01-23 23:59:49 +08:00
|
|
|
<div className="p-2">
|
|
|
|
{menuItems.map((item, index) => (
|
2025-01-24 15:05:03 +08:00
|
|
|
<button
|
2025-01-23 23:59:49 +08:00
|
|
|
key={index}
|
2025-01-24 15:05:03 +08:00
|
|
|
role="menuitem"
|
|
|
|
tabIndex={showMenu ? 0 : -1}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.stopPropagation();
|
|
|
|
handleMenuItemClick(item.action);
|
|
|
|
}}
|
|
|
|
className={`flex items-center gap-3 w-full px-4 py-3
|
|
|
|
text-sm font-medium rounded-lg transition-all
|
|
|
|
focus:outline-none
|
|
|
|
focus:ring-2 focus:ring-[#00538E]/20
|
|
|
|
group relative overflow-hidden
|
|
|
|
active:scale-[0.99]
|
|
|
|
${item.label === '注销'
|
|
|
|
? 'text-[#B22234] hover:bg-red-50/80 hover:text-red-700'
|
|
|
|
: 'text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]'
|
|
|
|
}`}
|
2025-01-23 23:59:49 +08:00
|
|
|
>
|
2025-01-24 15:05:03 +08:00
|
|
|
<span className={`w-5 h-5 flex items-center justify-center
|
|
|
|
transition-all duration-200 ease-in-out
|
|
|
|
group-hover:scale-110 group-hover:rotate-6
|
|
|
|
group-hover:translate-x-0.5 ${item.label === '注销'
|
|
|
|
? 'group-hover:text-red-600'
|
|
|
|
: 'group-hover:text-[#003F6A]'}`}>
|
|
|
|
{item.icon}
|
|
|
|
</span>
|
2025-01-23 23:59:49 +08:00
|
|
|
<span>{item.label}</span>
|
2025-01-24 15:05:03 +08:00
|
|
|
</button>
|
2025-01-23 23:59:49 +08:00
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</motion.div>
|
|
|
|
)}
|
|
|
|
</AnimatePresence>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|