import { useCallback, useEffect, useState, useRef } from 'react' import { MonitorDot, ChevronDown } from 'lucide-react' import { createPortal } from 'react-dom' import { activeInterconnectNodeId, api, setActiveInterconnectNodeId, } from '@/api' import { getNodes, type InterconnectNode } from '@/api/interconnect' import { useEventBus } from '@/hooks/useEventBus' import { SYSTEM_EVENTS } from '@/constants' import { cn } from '@/lib/utils' export function NodeSwitcher() { const [nodes, setNodes] = useState([]) const [selected, setSelected] = useState('local') const [isMaster, setIsMaster] = useState(false) const [open, setOpen] = useState(false) const [menuStyle, setMenuStyle] = useState({}) const [mounted, setMounted] = useState(false) const buttonRef = useRef(null) const fetchNodes = useCallback(async () => { try { const res = await getNodes() setNodes(Array.isArray(res) ? res : []) } catch { setNodes([]) } }, []) const updateRoleState = useCallback( (role: string) => { const master = role === 'master' setIsMaster(master) if (master) { setSelected(activeInterconnectNodeId || 'local') fetchNodes() } else { setNodes([]) setSelected('local') if (typeof document !== 'undefined') { const traveling = !!document.cookie.match(/(?:^| )active_interconnect_node_id=([^;]*)/) if (activeInterconnectNodeId && !traveling) { setActiveInterconnectNodeId('') } } } }, [fetchNodes], ) useEffect(() => { setMounted(true) // 在客户端初始化时从 localStorage 读取选中的节点 setSelected(activeInterconnectNodeId || 'local') }, []) useEffect(() => { ;(async () => { try { const role = await api.settings.get('interconnect', 'interconnect_role') updateRoleState(role || 'none') } catch { updateRoleState('none') } })() }, [updateRoleState]) useEventBus(SYSTEM_EVENTS.INTERCONNECT_ROLE_CHANGED, (role) => { updateRoleState(String(role || 'none')) }) useEffect(() => { if (!isMaster) return const t = window.setInterval(fetchNodes, 120000) const onVis = () => { if (document.visibilityState === 'visible') fetchNodes() } document.addEventListener('visibilitychange', onVis) return () => { clearInterval(t) document.removeEventListener('visibilitychange', onVis) } }, [isMaster, fetchNodes]) useEffect(() => { if (!open) return 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' }), }) } 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]) // SSR 时或非 master 节点时返回 null if (!mounted || !isMaster) return null const handleSelect = (val: string) => { setSelected(val) setOpen(false) if (val === 'local') { setActiveInterconnectNodeId('') } else { const name = nodes.find((n) => n.id === val)?.name || '' setActiveInterconnectNodeId(val, name) } window.location.reload() } const allOptions = [ { value: 'local', label: '本机节点' }, ...nodes.map((n) => ({ value: n.id, label: n.name })), ] const currentLabel = allOptions.find((o) => o.value === selected)?.label || '本机节点' return (
{open && mounted && createPortal(
{allOptions.map((opt) => ( ))}
, document.body, )}
) }