diff --git a/web/src/components/LayoutUserMenu.tsx b/web/src/components/LayoutUserMenu.tsx index 74a6497..8a5829b 100644 --- a/web/src/components/LayoutUserMenu.tsx +++ b/web/src/components/LayoutUserMenu.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef } from 'react' +import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react' +import { createPortal } from 'react-dom' import { Link, useLocation } from 'react-router-dom' import { AnimatePresence, motion } from 'framer-motion' import { Cast, ChevronDown, Clock, Heart, ListMusic, LogOut, Settings, UserCog } from 'lucide-react' @@ -8,6 +9,11 @@ import type { PlayProfile } from '../types' import { LayoutThemeToggle } from './LayoutThemeToggle' import type { ThemeMode } from './useThemeMode' +type MenuPosition = { + top: number + right: number +} + type LayoutUser = { username?: string role?: string @@ -43,72 +49,83 @@ export function LayoutUserMenu({ onThemeChange, }: LayoutUserMenuProps) { const location = useLocation() - const rootRef = useRef(null) - const lastLocationRef = useRef(`${location.pathname}${location.search}`) + const triggerRef = useRef(null) + const onCloseRef = useRef(onClose) + const [menuPosition, setMenuPosition] = useState(null) + + onCloseRef.current = onClose + + const updateMenuPosition = useCallback(() => { + const trigger = triggerRef.current + if (!trigger) return + const rect = trigger.getBoundingClientRect() + setMenuPosition({ + top: rect.bottom + 12, + right: Math.max(8, window.innerWidth - rect.right), + }) + }, []) + + useLayoutEffect(() => { + if (!isOpen) { + setMenuPosition(null) + return undefined + } + + updateMenuPosition() + window.addEventListener('resize', updateMenuPosition) + window.addEventListener('scroll', updateMenuPosition, true) + return () => { + window.removeEventListener('resize', updateMenuPosition) + window.removeEventListener('scroll', updateMenuPosition, true) + } + }, [isOpen, updateMenuPosition]) useEffect(() => { if (!isOpen) return undefined - const handlePointerDown = (event: PointerEvent) => { - const root = rootRef.current - const target = event.target - if (!root || !(target instanceof Node) || root.contains(target)) return - onClose() - } const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') onClose() + if (event.key === 'Escape') onCloseRef.current() } - document.addEventListener('pointerdown', handlePointerDown, true) document.addEventListener('keydown', handleKeyDown) return () => { - document.removeEventListener('pointerdown', handlePointerDown, true) document.removeEventListener('keydown', handleKeyDown) } - }, [isOpen, onClose]) + }, [isOpen]) useEffect(() => { - const nextLocation = `${location.pathname}${location.search}` - if (lastLocationRef.current === nextLocation) return - lastLocationRef.current = nextLocation - if (isOpen) onClose() - }, [isOpen, location.pathname, location.search, onClose]) + onCloseRef.current() + }, [location.pathname, location.search]) - return ( -
- - - - {isOpen && ( + const menuPortal = isOpen && menuPosition && typeof document !== 'undefined' + ? createPortal( +
- } label="管理观影 Profile" onClick={onClose} /> + } + label="管理观影 Profile" + onNavigate={onClose} + />
- )} - + , + document.body, + ) + : null + + return ( +
+ + {menuPortal}
) } @@ -162,17 +211,17 @@ function UserMenuLink({ to, icon, label, - onClick, + onNavigate, }: { to: string - icon: React.ReactNode + icon: ReactNode label: string - onClick: () => void + onNavigate: () => void }) { return ( {icon}