import { useState, useEffect } from 'react'; import { subscriptions as subsApi, nodes as nodesApi } from '../api'; export default function NodeSelector() { const [subs, setSubs] = useState([]); const [selectedSub, setSelectedSub] = useState(null); const [nodeList, setNodeList] = useState([]); const [regexInput, setRegexInput] = useState(''); const [dragIndex, setDragIndex] = useState(null); useEffect(() => { subsApi.list().then(data => { setSubs(data); if (data.length > 0 && !selectedSub) { setSelectedSub(data[0].id); } }); }, []); useEffect(() => { if (selectedSub) { subsApi.nodes(selectedSub).then(setNodeList).catch(console.error); } }, [selectedSub]); const handleToggle = async (id: number, enabled: number) => { await nodesApi.fetchedToggle(id, !enabled); if (selectedSub) { subsApi.nodes(selectedSub).then(setNodeList); } }; const handleSelectAll = async (enabled: boolean) => { const ids = nodeList.map(n => n.id); await nodesApi.fetchedBatch(ids, enabled); if (selectedSub) { subsApi.nodes(selectedSub).then(setNodeList); } }; const handleRegexBatch = async (enabled: boolean) => { if (!regexInput.trim()) return; try { const re = new RegExp(regexInput, 'i'); const matchedIds = nodeList.filter(n => re.test(n.name)).map(n => n.id); if (matchedIds.length === 0) return; await nodesApi.fetchedBatch(matchedIds, enabled); if (selectedSub) { subsApi.nodes(selectedSub).then(setNodeList); } } catch { alert('无效的正则表达式'); } }; const handleDrop = async (targetIndex: number) => { if (dragIndex === null || dragIndex === targetIndex) { setDragIndex(null); return; } const reordered = [...nodeList]; const [moved] = reordered.splice(dragIndex, 1); reordered.splice(targetIndex, 0, moved); setNodeList(reordered); setDragIndex(null); try { await nodesApi.fetchedReorder(reordered.map(n => n.id)); } catch (err) { console.error(err); if (selectedSub) subsApi.nodes(selectedSub).then(setNodeList); } }; const enabledCount = nodeList.filter(n => n.enabled).length; return (

节点选择

选择要包含在最终配置中的节点

{/* Subscription tabs */}
{subs.map(sub => ( ))}
{/* Batch controls */} {nodeList.length > 0 && (
{enabledCount}/{nodeList.length} 已选
setRegexInput(e.target.value)} style={{ width: 260, fontSize: 12 }} /> {regexInput && ( {(() => { try { const re = new RegExp(regexInput, 'i'); const matched = nodeList.filter(n => re.test(n.name)).length; return `匹配 ${matched} 个`; } catch { return '无效正则'; } })()} )}
)} {/* Node list */}

拖动 ⠿ 可调整节点顺序,输出配置会按此顺序排列

{nodeList.map((node, idx) => ( e.preventDefault()} onDrop={() => handleDrop(idx)} style={{ opacity: node.enabled ? 1 : 0.5, background: dragIndex === idx ? 'var(--bg-active)' : undefined, }} > ))} {nodeList.length === 0 && ( )}
启用 名称 协议 服务器 端口
setDragIndex(idx)} onDragEnd={() => setDragIndex(null)} style={{ cursor: 'grab', textAlign: 'center', color: 'var(--text-muted)' }} title="拖动排序" > ⠿ handleToggle(node.id, node.enabled)} /> {node.name} {node.type} {node.server} {node.port}
{subs.length === 0 ? '请先添加订阅源' : '请先抓取订阅源节点' }
); } const styles = { title: { fontFamily: 'var(--font-mono)' as const, fontSize: 16, fontWeight: 600 as const, color: 'var(--text-primary)', marginBottom: 4, }, subtitle: { fontSize: 12, color: 'var(--text-secondary)', marginBottom: 20, }, };