doctor-mail/apps/web/src/components/layout/element/usermenu.tsx

198 lines
7.8 KiB
TypeScript
Raw Normal View History

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