diff --git a/web/src/components/business/NodeSwitcher.tsx b/web/src/components/business/NodeSwitcher.tsx index 5e6806f..d6ffe0b 100644 --- a/web/src/components/business/NodeSwitcher.tsx +++ b/web/src/components/business/NodeSwitcher.tsx @@ -1,5 +1,6 @@ -import { useCallback, useEffect, useState } from 'react' -import { MonitorDot } from 'lucide-react' +import { useCallback, useEffect, useState, useRef } from 'react' +import { MonitorDot, ChevronDown } from 'lucide-react' +import { createPortal } from 'react-dom' import { activeInterconnectNodeId, api, @@ -15,6 +16,8 @@ export function NodeSwitcher() { const [selected, setSelected] = useState(activeInterconnectNodeId || 'local') const [isMaster, setIsMaster] = useState(false) const [open, setOpen] = useState(false) + const [menuStyle, setMenuStyle] = useState({}) + const buttonRef = useRef(null) const fetchNodes = useCallback(async () => { try { @@ -93,9 +96,52 @@ export function NodeSwitcher() { const currentLabel = allOptions.find((o) => o.value === selected)?.label || '本机节点' + const updateMenuPosition = () => { + const el = buttonRef.current + if (!el) return + const rect = el.getBoundingClientRect() + const viewportH = window.innerHeight + const spaceBelow = viewportH - rect.bottom + const openUp = spaceBelow < 220 && rect.top > spaceBelow + setMenuStyle({ + position: 'fixed', + left: rect.left, + width: Math.max(rect.width, 140), + zIndex: 80, + ...(openUp + ? { bottom: viewportH - rect.top + 6, top: 'auto' } + : { top: rect.bottom + 6, bottom: 'auto' }), + }) + } + + useEffect(() => { + if (!open) return + updateMenuPosition() + const onDoc = (e: MouseEvent) => { + const t = e.target as Node + if (buttonRef.current?.contains(t)) return + setOpen(false) + } + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOpen(false) + } + const onReposition = () => updateMenuPosition() + document.addEventListener('mousedown', onDoc) + document.addEventListener('keydown', onKey) + window.addEventListener('resize', onReposition) + window.addEventListener('scroll', onReposition, true) + return () => { + document.removeEventListener('mousedown', onDoc) + document.removeEventListener('keydown', onKey) + window.removeEventListener('resize', onReposition) + window.removeEventListener('scroll', onReposition, true) + } + }, [open]) + return (
- {open && ( -
- {allOptions.map((opt) => ( - - ))} -
- )} - - {open && ( -
setOpen(false)} /> - )} + {open && + createPortal( +
+ {allOptions.map((opt) => ( + + ))} +
, + document.body, + )}
) -} \ No newline at end of file +}