diff --git a/web/src/components/business/NodeSwitcher.tsx b/web/src/components/business/NodeSwitcher.tsx index 6a820eb..f41ed7a 100644 --- a/web/src/components/business/NodeSwitcher.tsx +++ b/web/src/components/business/NodeSwitcher.tsx @@ -20,15 +20,6 @@ export function NodeSwitcher() { const [mounted, setMounted] = useState(false) const buttonRef = useRef(null) - useEffect(() => { - setMounted(true) - // 在客户端初始化时从 localStorage 读取选中的节点 - setSelected(activeInterconnectNodeId || 'local') - }, []) - - // SSR 时返回 null,避免 hydration 错误 - if (!mounted) return null - const fetchNodes = useCallback(async () => { try { const res = await getNodes() @@ -48,15 +39,23 @@ export function NodeSwitcher() { } else { setNodes([]) setSelected('local') - const traveling = !!document.cookie.match(/(?:^| )active_interconnect_node_id=([^;]*)/) - if (activeInterconnectNodeId && !traveling) { - setActiveInterconnectNodeId('') + 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 { @@ -85,47 +84,25 @@ export function NodeSwitcher() { } }, [isMaster, fetchNodes]) - if (!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 || '本机节点' - - 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 + 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 @@ -148,6 +125,28 @@ export function NodeSwitcher() { } }, [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 (