112 lines
4.5 KiB
TypeScript
112 lines
4.5 KiB
TypeScript
![]() |
// ... existing imports ...
|
||
|
|
||
|
import { UserCircleIcon, Cog6ToothIcon, QuestionMarkCircleIcon, ArrowLeftStartOnRectangleIcon } from "@heroicons/react/24/outline";
|
||
|
import { useClickOutside } from "@web/src/hooks/useClickOutside";
|
||
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||
|
import { motion, AnimatePresence } from "framer-motion";
|
||
|
import { useState, useRef } from "react";
|
||
|
import { Avatar } from "../../common/element/Avatar";
|
||
|
|
||
|
export function UserMenu() {
|
||
|
const [showMenu, setShowMenu] = useState(false);
|
||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||
|
const { user, logout } = useAuth();
|
||
|
useClickOutside(menuRef, () => setShowMenu(false));
|
||
|
|
||
|
const menuItems = [
|
||
|
{
|
||
|
icon: <UserCircleIcon className="w-5 h-5" />,
|
||
|
label: '个人信息',
|
||
|
action: () => { },
|
||
|
color: 'text-primary-600'
|
||
|
},
|
||
|
{
|
||
|
icon: <Cog6ToothIcon className="w-5 h-5" />,
|
||
|
label: '设置',
|
||
|
action: () => { },
|
||
|
color: 'text-gray-600'
|
||
|
},
|
||
|
{
|
||
|
icon: <QuestionMarkCircleIcon className="w-5 h-5" />,
|
||
|
label: '帮助',
|
||
|
action: () => { },
|
||
|
color: 'text-gray-600'
|
||
|
},
|
||
|
{
|
||
|
icon: <ArrowLeftStartOnRectangleIcon className="w-5 h-5" />,
|
||
|
label: '注销',
|
||
|
action: () => logout(),
|
||
|
color: 'text-red-600'
|
||
|
},
|
||
|
];
|
||
|
|
||
|
return (
|
||
|
<div ref={menuRef} className="relative">
|
||
|
<motion.button
|
||
|
whileHover={{ scale: 1.05 }}
|
||
|
whileTap={{ scale: 0.95 }}
|
||
|
onClick={() => setShowMenu(!showMenu)}
|
||
|
className="relative rounded-full focus:outline-none
|
||
|
focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
||
|
focus:ring-offset-[#13294B]"
|
||
|
>
|
||
|
<Avatar
|
||
|
src={user?.avatar}
|
||
|
name={user?.showname || user?.username}
|
||
|
size={40}
|
||
|
className="ring-2 ring-white/80 hover:ring-blue-400
|
||
|
transition-all duration-300"
|
||
|
/>
|
||
|
<span className="absolute bottom-0 right-0 h-3 w-3
|
||
|
rounded-full bg-green-500 ring-2 ring-white" />
|
||
|
</motion.button>
|
||
|
|
||
|
<AnimatePresence>
|
||
|
{showMenu && (
|
||
|
<motion.div
|
||
|
initial={{ opacity: 0, scale: 0.95, y: -10 }}
|
||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
|
exit={{ opacity: 0, scale: 0.95, y: -10 }}
|
||
|
transition={{
|
||
|
duration: 0.2,
|
||
|
type: "spring",
|
||
|
stiffness: 300,
|
||
|
damping: 30
|
||
|
}}
|
||
|
style={{ zIndex: 100 }}
|
||
|
className="absolute right-0 mt-2 w-56 origin-top-right
|
||
|
bg-white rounded-xl shadow-lg ring-1 ring-black/5
|
||
|
overflow-hidden"
|
||
|
>
|
||
|
<div className="p-4 border-b border-gray-100">
|
||
|
<h4 className="text-sm font-semibold text-gray-900">
|
||
|
{user?.showname}
|
||
|
</h4>
|
||
|
<p className="text-xs text-tertiary-300 mt-1">
|
||
|
{user?.username}
|
||
|
</p>
|
||
|
</div>
|
||
|
|
||
|
<div className="p-2">
|
||
|
{menuItems.map((item, index) => (
|
||
|
<motion.button
|
||
|
key={index}
|
||
|
whileHover={{ x: 4, backgroundColor: '#F3F4F6' }}
|
||
|
onClick={item.action}
|
||
|
className={`flex items-center gap-3 w-full p-2.5
|
||
|
rounded-lg text-sm font-medium
|
||
|
transition-colors duration-200
|
||
|
${item.color} hover:bg-gray-100`}
|
||
|
>
|
||
|
{item.icon}
|
||
|
<span>{item.label}</span>
|
||
|
</motion.button>
|
||
|
))}
|
||
|
</div>
|
||
|
</motion.div>
|
||
|
)}
|
||
|
</AnimatePresence>
|
||
|
</div>
|
||
|
);
|
||
|
}
|