import { useState, useEffect, useMemo } from "react"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Button } from "@heroui/button"; import { Input } from "@heroui/input"; import { Textarea } from "@heroui/input"; import { Select, SelectItem } from "@heroui/select"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal"; import { Chip } from "@heroui/chip"; import { Spinner } from "@heroui/spinner"; import { Switch } from "@heroui/switch"; import { Alert } from "@heroui/alert"; import { Accordion, AccordionItem } from "@heroui/accordion"; import toast from 'react-hot-toast'; import { DndContext, closestCenter, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy, } from '@dnd-kit/sortable'; import { useSortable, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { createForward, getForwardList, updateForward, deleteForward, forceDeleteForward, userTunnel, pauseForwardService, resumeForwardService, diagnoseForward, updateForwardOrder } from "@/api"; import { JwtUtil } from "@/utils/jwt"; interface Forward { id: number; name: string; tunnelId: number; tunnelName: string; inIp: string; inPort: number; remoteAddr: string; interfaceName?: string; strategy: string; status: number; inFlow: number; outFlow: number; serviceRunning: boolean; createdTime: string; userName?: string; userId?: number; inx?: number; } interface Tunnel { id: number; name: string; inNodePortSta?: number; inNodePortEnd?: number; } interface ForwardForm { id?: number; userId?: number; name: string; tunnelId: number | null; inPort: number | null; remoteAddr: string; interfaceName?: string; strategy: string; } interface AddressItem { id: number; address: string; copying: boolean; } interface DiagnosisResult { forwardName: string; timestamp: number; results: Array<{ success: boolean; description: string; nodeName: string; nodeId: string; targetIp: string; targetPort?: number; message?: string; averageTime?: number; packetLoss?: number; fromChainType?: number; // 1: 入口, 2: 链, 3: 出口 fromInx?: number; toChainType?: number; toInx?: number; }>; } // 添加分组接口 interface UserGroup { userId: number | null; userName: string; tunnelGroups: TunnelGroup[]; } interface TunnelGroup { tunnelId: number; tunnelName: string; forwards: Forward[]; } export default function ForwardPage() { const [loading, setLoading] = useState(true); const [forwards, setForwards] = useState([]); const [tunnels, setTunnels] = useState([]); // 检测是否为移动端 const [isMobile, setIsMobile] = useState(false); useEffect(() => { const checkMobile = () => { setIsMobile(window.innerWidth < 768); }; checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); // 显示模式状态 - 从localStorage读取,默认为平铺显示 const [viewMode, setViewMode] = useState<'grouped' | 'direct'>(() => { try { const savedMode = localStorage.getItem('forward-view-mode'); return (savedMode as 'grouped' | 'direct') || 'direct'; } catch { return 'direct'; } }); // 拖拽排序相关状态 const [forwardOrder, setForwardOrder] = useState([]); // 模态框状态 const [modalOpen, setModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [addressModalOpen, setAddressModalOpen] = useState(false); const [diagnosisModalOpen, setDiagnosisModalOpen] = useState(false); const [isEdit, setIsEdit] = useState(false); const [submitLoading, setSubmitLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const [diagnosisLoading, setDiagnosisLoading] = useState(false); const [forwardToDelete, setForwardToDelete] = useState(null); const [currentDiagnosisForward, setCurrentDiagnosisForward] = useState(null); const [diagnosisResult, setDiagnosisResult] = useState(null); const [addressModalTitle, setAddressModalTitle] = useState(''); const [addressList, setAddressList] = useState([]); // 导出相关状态 const [exportModalOpen, setExportModalOpen] = useState(false); const [exportData, setExportData] = useState(''); const [exportLoading, setExportLoading] = useState(false); const [selectedTunnelForExport, setSelectedTunnelForExport] = useState(null); // 导入相关状态 const [importModalOpen, setImportModalOpen] = useState(false); const [importData, setImportData] = useState(''); const [importLoading, setImportLoading] = useState(false); const [selectedTunnelForImport, setSelectedTunnelForImport] = useState(null); const [importResults, setImportResults] = useState>([]); // 表单状态 const [form, setForm] = useState({ name: '', tunnelId: null, inPort: null, remoteAddr: '', interfaceName: '', strategy: 'fifo' }); // 表单验证错误 const [errors, setErrors] = useState<{[key: string]: string}>({}); useEffect(() => { loadData(); }, []); // 切换显示模式并保存到localStorage const handleViewModeChange = () => { const newMode = viewMode === 'grouped' ? 'direct' : 'grouped'; setViewMode(newMode); try { localStorage.setItem('forward-view-mode', newMode); // 切换到直接显示模式时,初始化拖拽排序顺序 if (newMode === 'direct') { // 在平铺模式下,只对当前用户的转发进行排序 const currentUserId = JwtUtil.getUserIdFromToken(); let userForwards = forwards; if (currentUserId !== null) { userForwards = forwards.filter((f: Forward) => f.userId === currentUserId); } // 检查数据库中是否有排序信息 const hasDbOrdering = userForwards.some((f: Forward) => f.inx !== undefined && f.inx !== 0); if (hasDbOrdering) { // 使用数据库中的排序信息 const dbOrder = userForwards .sort((a: Forward, b: Forward) => (a.inx ?? 0) - (b.inx ?? 0)) .map((f: Forward) => f.id); setForwardOrder(dbOrder); // 同步到localStorage try { localStorage.setItem('forward-order', JSON.stringify(dbOrder)); } catch (error) { console.warn('无法保存排序到localStorage:', error); } } else { // 使用本地存储的顺序 const savedOrder = localStorage.getItem('forward-order'); if (savedOrder) { try { const orderIds = JSON.parse(savedOrder); const validOrder = orderIds.filter((id: number) => userForwards.some((f: Forward) => f.id === id) ); userForwards.forEach((forward: Forward) => { if (!validOrder.includes(forward.id)) { validOrder.push(forward.id); } }); setForwardOrder(validOrder); } catch { setForwardOrder(userForwards.map((f: Forward) => f.id)); } } else { setForwardOrder(userForwards.map((f: Forward) => f.id)); } } } } catch (error) { console.warn('无法保存显示模式到localStorage:', error); } }; // 加载所有数据 const loadData = async (lod = true) => { setLoading(lod); try { const [forwardsRes, tunnelsRes] = await Promise.all([ getForwardList(), userTunnel() ]); if (forwardsRes.code === 0) { const forwardsData = forwardsRes.data?.map((forward: any) => ({ ...forward, serviceRunning: forward.status === 1 })) || []; setForwards(forwardsData); // 初始化拖拽排序顺序 if (viewMode === 'direct') { // 在平铺模式下,只对当前用户的转发进行排序 const currentUserId = JwtUtil.getUserIdFromToken(); let userForwards = forwardsData; if (currentUserId !== null) { userForwards = forwardsData.filter((f: Forward) => f.userId === currentUserId); } // 检查数据库中是否有排序信息 const hasDbOrdering = userForwards.some((f: Forward) => f.inx !== undefined && f.inx !== 0); if (hasDbOrdering) { // 使用数据库中的排序信息 const dbOrder = userForwards .sort((a: Forward, b: Forward) => (a.inx ?? 0) - (b.inx ?? 0)) .map((f: Forward) => f.id); setForwardOrder(dbOrder); // 同步到localStorage try { localStorage.setItem('forward-order', JSON.stringify(dbOrder)); } catch (error) { console.warn('无法保存排序到localStorage:', error); } } else { // 使用本地存储的顺序 const savedOrder = localStorage.getItem('forward-order'); if (savedOrder) { try { const orderIds = JSON.parse(savedOrder); // 验证保存的顺序是否仍然有效(只包含当前用户的转发) const validOrder = orderIds.filter((id: number) => userForwards.some((f: Forward) => f.id === id) ); // 添加新的转发ID(如果存在) userForwards.forEach((forward: Forward) => { if (!validOrder.includes(forward.id)) { validOrder.push(forward.id); } }); setForwardOrder(validOrder); } catch { setForwardOrder(userForwards.map((f: Forward) => f.id)); } } else { setForwardOrder(userForwards.map((f: Forward) => f.id)); } } } } else { toast.error(forwardsRes.msg || '获取转发列表失败'); } if (tunnelsRes.code === 0) { setTunnels(tunnelsRes.data || []); } else { console.warn('获取隧道列表失败:', tunnelsRes.msg); } } catch (error) { console.error('加载数据失败:', error); toast.error('加载数据失败'); } finally { setLoading(false); } }; // 按用户和隧道分组转发数据 const groupForwardsByUserAndTunnel = (): UserGroup[] => { const userMap = new Map(); // 获取排序后的转发列表 sortedForwards.forEach(forward => { const userKey = forward.userId ? forward.userId.toString() : 'unknown'; const userName = forward.userName || '未知用户'; if (!userMap.has(userKey)) { userMap.set(userKey, { userId: forward.userId || null, userName, tunnelGroups: [] }); } const userGroup = userMap.get(userKey)!; let tunnelGroup = userGroup.tunnelGroups.find(tg => tg.tunnelId === forward.tunnelId); if (!tunnelGroup) { tunnelGroup = { tunnelId: forward.tunnelId, tunnelName: forward.tunnelName, forwards: [] }; userGroup.tunnelGroups.push(tunnelGroup); } tunnelGroup.forwards.push(forward); }); // 排序:先按用户名,再按隧道名 const result = Array.from(userMap.values()); result.sort((a, b) => a.userName.localeCompare(b.userName)); result.forEach(userGroup => { userGroup.tunnelGroups.sort((a, b) => a.tunnelName.localeCompare(b.tunnelName)); }); return result; }; // 表单验证 const validateForm = (): boolean => { const newErrors: {[key: string]: string} = {}; if (!form.name.trim()) { newErrors.name = '请输入转发名称'; } else if (form.name.length < 2 || form.name.length > 50) { newErrors.name = '转发名称长度应在2-50个字符之间'; } if (!form.tunnelId) { newErrors.tunnelId = '请选择关联隧道'; } // 验证入口端口(可选,如果填写则验证) if (form.inPort !== null && form.inPort !== undefined) { const port = Number(form.inPort); if (isNaN(port) || port < 1 || port > 65535) { newErrors.inPort = '端口必须在 1-65535 之间'; } } if (!form.remoteAddr.trim()) { newErrors.remoteAddr = '请输入远程地址'; } else { // 验证地址格式 const addresses = form.remoteAddr.split('\n').map(addr => addr.trim()).filter(addr => addr); const ipv4Pattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):\d+$/; const ipv6FullPattern = /^\[((([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:))|(([0-9a-fA-F]{1,4}:){6}(:[0-9a-fA-F]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){5}(((:[0-9a-fA-F]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-fA-F]{1,4}:){4}(((:[0-9a-fA-F]{1,4}){1,3})|((:[0-9a-fA-F]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){3}(((:[0-9a-fA-F]{1,4}){1,4})|((:[0-9a-fA-F]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){2}(((:[0-9a-fA-F]{1,4}){1,5})|((:[0-9a-fA-F]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-fA-F]{1,4}:){1}(((:[0-9a-fA-F]{1,4}){1,6})|((:[0-9a-fA-F]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-fA-F]{1,4}){1,7})|((:[0-9a-fA-F]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))\]:\d+$/; const domainPattern = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*:\d+$/; for (let i = 0; i < addresses.length; i++) { const addr = addresses[i]; if (!ipv4Pattern.test(addr) && !ipv6FullPattern.test(addr) && !domainPattern.test(addr)) { newErrors.remoteAddr = `第${i + 1}行地址格式错误`; break; } } } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // 新增转发 const handleAdd = () => { setIsEdit(false); setForm({ name: '', tunnelId: null, inPort: null, remoteAddr: '', interfaceName: '', strategy: 'fifo' }); setErrors({}); setModalOpen(true); }; // 编辑转发 const handleEdit = (forward: Forward) => { setIsEdit(true); setForm({ id: forward.id, userId: forward.userId, name: forward.name, tunnelId: forward.tunnelId, inPort: forward.inPort, remoteAddr: forward.remoteAddr.split(',').join('\n'), interfaceName: forward.interfaceName || '', strategy: forward.strategy || 'fifo' }); setErrors({}); setModalOpen(true); }; // 显示删除确认 const handleDelete = (forward: Forward) => { setForwardToDelete(forward); setDeleteModalOpen(true); }; // 确认删除转发 const confirmDelete = async () => { if (!forwardToDelete) return; setDeleteLoading(true); try { const res = await deleteForward(forwardToDelete.id); if (res.code === 0) { toast.success('删除成功'); setDeleteModalOpen(false); loadData(); } else { // 删除失败,询问是否强制删除 const confirmed = window.confirm(`常规删除失败:${res.msg || '删除失败'}\n\n是否需要强制删除?\n\n⚠️ 注意:强制删除不会去验证节点端是否已经删除对应的转发服务。`); if (confirmed) { const forceRes = await forceDeleteForward(forwardToDelete.id); if (forceRes.code === 0) { toast.success('强制删除成功'); setDeleteModalOpen(false); loadData(); } else { toast.error(forceRes.msg || '强制删除失败'); } } } } catch (error) { console.error('删除失败:', error); toast.error('删除失败'); } finally { setDeleteLoading(false); } }; // 处理隧道选择变化 const handleTunnelChange = (tunnelId: string) => { setForm(prev => ({ ...prev, tunnelId: parseInt(tunnelId) })); }; // 提交表单 const handleSubmit = async () => { if (!validateForm()) return; setSubmitLoading(true); try { const processedRemoteAddr = form.remoteAddr .split('\n') .map(addr => addr.trim()) .filter(addr => addr) .join(','); const addressCount = processedRemoteAddr.split(',').length; let res; if (isEdit) { // 更新时确保包含必要字段 const updateData = { id: form.id, userId: form.userId, name: form.name, tunnelId: form.tunnelId, inPort: form.inPort, remoteAddr: processedRemoteAddr, strategy: addressCount > 1 ? form.strategy : 'fifo' }; res = await updateForward(updateData); } else { // 创建时不需要id和userId(后端会自动设置) const createData = { name: form.name, tunnelId: form.tunnelId, inPort: form.inPort, remoteAddr: processedRemoteAddr, strategy: addressCount > 1 ? form.strategy : 'fifo' }; res = await createForward(createData); } if (res.code === 0) { toast.success(isEdit ? '修改成功' : '创建成功'); setModalOpen(false); loadData(); } else { toast.error(res.msg || '操作失败'); } } catch (error) { console.error('提交失败:', error); toast.error('操作失败'); } finally { setSubmitLoading(false); } }; // 处理服务开关 const handleServiceToggle = async (forward: Forward) => { if (forward.status !== 1 && forward.status !== 0) { toast.error('转发状态异常,无法操作'); return; } const targetState = !forward.serviceRunning; try { // 乐观更新UI setForwards(prev => prev.map(f => f.id === forward.id ? { ...f, serviceRunning: targetState } : f )); let res; if (targetState) { res = await resumeForwardService(forward.id); } else { res = await pauseForwardService(forward.id); } if (res.code === 0) { toast.success(targetState ? '服务已启动' : '服务已暂停'); // 更新转发状态 setForwards(prev => prev.map(f => f.id === forward.id ? { ...f, status: targetState ? 1 : 0 } : f )); } else { // 操作失败,恢复UI状态 setForwards(prev => prev.map(f => f.id === forward.id ? { ...f, serviceRunning: !targetState } : f )); toast.error(res.msg || '操作失败'); } } catch (error) { // 操作失败,恢复UI状态 setForwards(prev => prev.map(f => f.id === forward.id ? { ...f, serviceRunning: !targetState } : f )); console.error('服务开关操作失败:', error); toast.error('网络错误,操作失败'); } }; // 诊断转发 const handleDiagnose = async (forward: Forward) => { setCurrentDiagnosisForward(forward); setDiagnosisModalOpen(true); setDiagnosisLoading(true); setDiagnosisResult(null); try { const response = await diagnoseForward(forward.id); if (response.code === 0) { setDiagnosisResult(response.data); } else { toast.error(response.msg || '诊断失败'); setDiagnosisResult({ forwardName: forward.name, timestamp: Date.now(), results: [{ success: false, description: '诊断失败', nodeName: '-', nodeId: '-', targetIp: forward.remoteAddr.split(',')[0] || '-', message: response.msg || '诊断过程中发生错误' }] }); } } catch (error) { console.error('诊断失败:', error); toast.error('网络错误,请重试'); setDiagnosisResult({ forwardName: forward.name, timestamp: Date.now(), results: [{ success: false, description: '网络错误', nodeName: '-', nodeId: '-', targetIp: forward.remoteAddr.split(',')[0] || '-', message: '无法连接到服务器' }] }); } finally { setDiagnosisLoading(false); } }; // 获取连接质量 const getQualityDisplay = (averageTime?: number, packetLoss?: number) => { if (averageTime === undefined || packetLoss === undefined) return null; if (averageTime < 30 && packetLoss === 0) return { text: '🚀 优秀', color: 'success' }; if (averageTime < 50 && packetLoss === 0) return { text: '✨ 很好', color: 'success' }; if (averageTime < 100 && packetLoss < 1) return { text: '👍 良好', color: 'primary' }; if (averageTime < 150 && packetLoss < 2) return { text: '😐 一般', color: 'warning' }; if (averageTime < 200 && packetLoss < 5) return { text: '😟 较差', color: 'warning' }; return { text: '😵 很差', color: 'danger' }; }; // 格式化流量 const formatFlow = (value: number): string => { if (value === 0) return '0 B'; if (value < 1024) return value + ' B'; if (value < 1024 * 1024) return (value / 1024).toFixed(2) + ' KB'; if (value < 1024 * 1024 * 1024) return (value / (1024 * 1024)).toFixed(2) + ' MB'; return (value / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; }; // 格式化入口地址 const formatInAddress = (ipString: string, port: number): string => { if (!ipString) return ''; const items = ipString.split(',').map(item => item.trim()).filter(item => item); if (items.length === 0) return ''; // 检查第一项是否已经包含端口(格式:IP:端口) const firstItem = items[0]; const hasPort = /:\d+$/.test(firstItem); if (hasPort) { // inIp 已经包含完整的 IP:Port 组合 if (items.length === 1) { return items[0]; } return `${items[0]} (+${items.length - 1}个)`; } // inIp 只包含IP,需要添加端口(兼容旧数据) if (!port) return ''; if (items.length === 1) { const ip = items[0]; if (ip.includes(':') && !ip.startsWith('[')) { return `[${ip}]:${port}`; } else { return `${ip}:${port}`; } } const firstIp = items[0]; let formattedFirstIp; if (firstIp.includes(':') && !firstIp.startsWith('[')) { formattedFirstIp = `[${firstIp}]`; } else { formattedFirstIp = firstIp; } return `${formattedFirstIp}:${port} (+${items.length - 1}个)`; }; // 格式化远程地址 const formatRemoteAddress = (addressString: string): string => { if (!addressString) return ''; const addresses = addressString.split(',').map(addr => addr.trim()).filter(addr => addr); if (addresses.length === 0) return ''; if (addresses.length === 1) return addresses[0]; return `${addresses[0]} (+${addresses.length - 1})`; }; // 检查是否有多个地址 const hasMultipleAddresses = (addressString: string): boolean => { if (!addressString) return false; const addresses = addressString.split(',').map(addr => addr.trim()).filter(addr => addr); return addresses.length > 1; }; // 显示地址列表弹窗 const showAddressModal = (addressString: string, port: number | null, title: string) => { if (!addressString) return; let addresses: string[]; if (port !== null) { // 入口地址处理 const items = addressString.split(',').map(item => item.trim()).filter(item => item); if (items.length <= 1) { copyToClipboard(formatInAddress(addressString, port), title); return; } // 检查是否已经包含端口 const hasPort = /:\d+$/.test(items[0]); if (hasPort) { // 已经包含完整的 IP:Port 组合,直接使用 addresses = items; } else { // 只包含IP,需要添加端口 addresses = items.map(ip => { if (ip.includes(':') && !ip.startsWith('[')) { return `[${ip}]:${port}`; } else { return `${ip}:${port}`; } }); } } else { // 远程地址处理 addresses = addressString.split(',').map(addr => addr.trim()).filter(addr => addr); if (addresses.length <= 1) { copyToClipboard(addressString, title); return; } } setAddressList(addresses.map((address, index) => ({ id: index, address, copying: false }))); setAddressModalTitle(`${title} (${addresses.length}个)`); setAddressModalOpen(true); }; // 复制到剪贴板 const copyToClipboard = async (text: string, label: string = '内容') => { try { await navigator.clipboard.writeText(text); toast.success(`已复制${label}`); } catch (error) { toast.error('复制失败'); } }; // 复制地址 const copyAddress = async (addressItem: AddressItem) => { try { setAddressList(prev => prev.map(item => item.id === addressItem.id ? { ...item, copying: true } : item )); await copyToClipboard(addressItem.address, '地址'); } catch (error) { toast.error('复制失败'); } finally { setAddressList(prev => prev.map(item => item.id === addressItem.id ? { ...item, copying: false } : item )); } }; // 复制所有地址 const copyAllAddresses = async () => { if (addressList.length === 0) return; const allAddresses = addressList.map(item => item.address).join('\n'); await copyToClipboard(allAddresses, '所有地址'); }; // 导出转发数据 const handleExport = () => { setSelectedTunnelForExport(null); setExportData(''); setExportModalOpen(true); }; // 执行导出 const executeExport = () => { if (!selectedTunnelForExport) { toast.error('请选择要导出的隧道'); return; } setExportLoading(true); try { // 根据当前显示模式获取要导出的转发列表 let forwardsToExport: Forward[] = []; if (viewMode === 'grouped') { // 分组模式下,获取指定隧道的转发 const userGroups = groupForwardsByUserAndTunnel(); forwardsToExport = userGroups.flatMap(userGroup => userGroup.tunnelGroups .filter(tunnelGroup => tunnelGroup.tunnelId === selectedTunnelForExport) .flatMap(tunnelGroup => tunnelGroup.forwards) ); } else { // 直接显示模式下,过滤指定隧道的转发 forwardsToExport = sortedForwards.filter(forward => forward.tunnelId === selectedTunnelForExport); } if (forwardsToExport.length === 0) { toast.error('所选隧道没有转发数据'); setExportLoading(false); return; } // 格式化导出数据:remoteAddr|name|inPort const exportLines = forwardsToExport.map(forward => { return `${forward.remoteAddr}|${forward.name}|${forward.inPort}`; }); const exportText = exportLines.join('\n'); setExportData(exportText); } catch (error) { console.error('导出失败:', error); toast.error('导出失败'); } finally { setExportLoading(false); } }; // 复制导出数据 const copyExportData = async () => { await copyToClipboard(exportData, '转发数据'); }; // 导入转发数据 const handleImport = () => { setImportData(''); setImportResults([]); setSelectedTunnelForImport(null); setImportModalOpen(true); }; // 执行导入 const executeImport = async () => { if (!importData.trim()) { toast.error('请输入要导入的数据'); return; } if (!selectedTunnelForImport) { toast.error('请选择要导入的隧道'); return; } setImportLoading(true); setImportResults([]); // 清空之前的结果 try { const lines = importData.trim().split('\n').filter(line => line.trim()); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); const parts = line.split('|'); if (parts.length < 2) { setImportResults(prev => [{ line, success: false, message: '格式错误:需要至少包含目标地址和转发名称' }, ...prev]); continue; } const [remoteAddr, name, inPort] = parts; if (!remoteAddr.trim() || !name.trim()) { setImportResults(prev => [{ line, success: false, message: '目标地址和转发名称不能为空' }, ...prev]); continue; } // 验证远程地址格式 - 支持单个地址或多个地址用逗号分隔 const addresses = remoteAddr.trim().split(','); const addressPattern = /^[^:]+:\d+$/; const isValidFormat = addresses.every(addr => addressPattern.test(addr.trim())); if (!isValidFormat) { setImportResults(prev => [{ line, success: false, message: '目标地址格式错误,应为 地址:端口 格式,多个地址用逗号分隔' }, ...prev]); continue; } try { // 处理入口端口 let portNumber: number | null = null; if (inPort && inPort.trim()) { const port = parseInt(inPort.trim()); if (isNaN(port) || port < 1 || port > 65535) { setImportResults(prev => [{ line, success: false, message: '入口端口格式错误,应为1-65535之间的数字' }, ...prev]); continue; } portNumber = port; } // 调用创建转发接口 const response = await createForward({ name: name.trim(), tunnelId: selectedTunnelForImport, // 使用用户选择的隧道 inPort: portNumber, // 使用指定端口或自动分配 remoteAddr: remoteAddr.trim(), strategy: 'fifo' }); if (response.code === 0) { setImportResults(prev => [{ line, success: true, message: '创建成功', forwardName: name.trim() }, ...prev]); } else { setImportResults(prev => [{ line, success: false, message: response.msg || '创建失败' }, ...prev]); } } catch (error) { setImportResults(prev => [{ line, success: false, message: '网络错误,创建失败' }, ...prev]); } } toast.success(`导入执行完成`); // 导入完成后刷新转发列表 await loadData(false); } catch (error) { console.error('导入失败:', error); toast.error('导入过程中发生错误'); } finally { setImportLoading(false); } }; // 获取状态显示 const getStatusDisplay = (status: number) => { switch (status) { case 1: return { color: 'success', text: '正常' }; case 0: return { color: 'warning', text: '暂停' }; case -1: return { color: 'danger', text: '异常' }; default: return { color: 'default', text: '未知' }; } }; // 获取策略显示 const getStrategyDisplay = (strategy: string) => { switch (strategy) { case 'fifo': return { color: 'primary', text: '主备' }; case 'round': return { color: 'success', text: '轮询' }; case 'rand': return { color: 'warning', text: '随机' }; default: return { color: 'default', text: '未知' }; } }; // 获取地址数量 const getAddressCount = (addressString: string): number => { if (!addressString) return 0; const addresses = addressString.split('\n').map(addr => addr.trim()).filter(addr => addr); return addresses.length; }; // 处理拖拽结束 const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (!active || !over || active.id === over.id) return; // 确保 forwardOrder 存在且有效 if (!forwardOrder || forwardOrder.length === 0) return; const activeId = Number(active.id); const overId = Number(over.id); // 检查 ID 是否有效 if (isNaN(activeId) || isNaN(overId)) return; const oldIndex = forwardOrder.indexOf(activeId); const newIndex = forwardOrder.indexOf(overId); if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) { const newOrder = arrayMove(forwardOrder, oldIndex, newIndex); setForwardOrder(newOrder); // 保存到localStorage try { localStorage.setItem('forward-order', JSON.stringify(newOrder)); } catch (error) { console.warn('无法保存排序到localStorage:', error); } // 持久化到数据库 try { const forwardsToUpdate = newOrder.map((id, index) => ({ id, inx: index })); const response = await updateForwardOrder({ forwards: forwardsToUpdate }); if (response.code === 0) { // 更新本地数据中的 inx 字段 setForwards(prev => prev.map(forward => { const updatedForward = forwardsToUpdate.find(f => f.id === forward.id); if (updatedForward) { return { ...forward, inx: updatedForward.inx }; } return forward; })); } else { toast.error('保存排序失败:' + (response.msg || '未知错误')); } } catch (error) { console.error('保存排序到数据库失败:', error); toast.error('保存排序失败,请重试'); } } }; // 传感器配置 - 使用默认配置避免错误 const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 8, }, }), useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 8, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const tokenUserId = JwtUtil.getUserIdFromToken(); // 根据排序顺序获取转发列表 const sortedForwards = useMemo((): Forward[] => { // 确保 forwards 数组存在且有效 if (!forwards || forwards.length === 0) { return []; } // 在平铺模式下,只显示当前用户的转发 let filteredForwards = forwards; if (viewMode === 'direct') { if (tokenUserId !== null) { filteredForwards = forwards.filter(forward => forward.userId === tokenUserId); } } // 确保过滤后的转发列表有效 if (!filteredForwards || filteredForwards.length === 0) { return []; } // 优先使用数据库中的 inx 字段进行排序 const sortedByDb = [...filteredForwards].sort((a, b) => { const aInx = a.inx ?? 0; const bInx = b.inx ?? 0; return aInx - bInx; }); // 如果数据库中没有排序信息,则使用本地存储的顺序 if (forwardOrder && forwardOrder.length > 0 && sortedByDb.every(f => f.inx === undefined || f.inx === 0)) { const forwardMap = new Map(filteredForwards.map(f => [f.id, f])); const localSortedForwards: Forward[] = []; forwardOrder.forEach(id => { const forward = forwardMap.get(id); if (forward) { localSortedForwards.push(forward); } }); // 添加不在排序列表中的转发(新添加的) filteredForwards.forEach(forward => { if (!forwardOrder.includes(forward.id)) { localSortedForwards.push(forward); } }); return localSortedForwards; } return sortedByDb; }, [forwards, forwardOrder, viewMode, tokenUserId]); const sortableForwardIds = useMemo( () => sortedForwards.map(f => f.id).filter(id => id > 0), [sortedForwards] ); // 可拖拽的转发卡片组件 const SortableForwardCard = ({ forward }: { forward: Forward }) => { // 确保 forward 对象有效 if (!forward || !forward.id) { return null; } const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: forward.id }); const style = { transform: transform ? CSS.Transform.toString(transform) : undefined, transition: isDragging ? undefined : transition || undefined, opacity: isDragging ? 0.5 : 1, willChange: 'transform', }; return (
{renderForwardCard(forward, listeners)}
); }; // 渲染转发卡片 const renderForwardCard = (forward: Forward, listeners?: any) => { const statusDisplay = getStatusDisplay(forward.status); const strategyDisplay = getStrategyDisplay(forward.strategy); return (

{forward.name}

{forward.tunnelName}

{viewMode === 'direct' && (
)} handleServiceToggle(forward)} isDisabled={forward.status !== 1 && forward.status !== 0} /> {statusDisplay.text}
{/* 地址信息 */}
showAddressModal(forward.inIp, forward.inPort, '入口端口')} title={formatInAddress(forward.inIp, forward.inPort)} >
入口: {formatInAddress(forward.inIp, forward.inPort)}
{hasMultipleAddresses(forward.inIp) && ( )}
showAddressModal(forward.remoteAddr, null, '目标地址')} title={formatRemoteAddress(forward.remoteAddr)} >
目标: {formatRemoteAddress(forward.remoteAddr)}
{hasMultipleAddresses(forward.remoteAddr) && ( )}
{/* 统计信息 */}
{strategyDisplay.text}
↑{formatFlow(forward.inFlow || 0)}
↓{formatFlow(forward.outFlow || 0)}
); }; if (loading) { return (
正在加载...
); } const userGroups = groupForwardsByUserAndTunnel(); return (
{/* 页面头部 */}
{/* 显示模式切换按钮 */} {/* 导入按钮 */} {/* 导出按钮 */}
{/* 根据显示模式渲染不同内容 */} {viewMode === 'grouped' ? ( /* 按用户和隧道分组的转发列表 */ userGroups.length > 0 ? (
{userGroups.map((userGroup) => (

{userGroup.userName}

{userGroup.tunnelGroups.length} 个隧道, {userGroup.tunnelGroups.reduce((total, tg) => total + tg.forwards.length, 0)} 个转发

用户
{userGroup.tunnelGroups.map((tunnelGroup) => (

{tunnelGroup.tunnelName}

{tunnelGroup.forwards.filter(f => f.serviceRunning).length}/{tunnelGroup.forwards.length}
} className="shadow-none border border-divider" >
{tunnelGroup.forwards.map((forward) => renderForwardCard(forward, undefined))}
))} ))}
) : ( /* 空状态 */

暂无转发配置

还没有创建任何转发配置,点击上方按钮开始创建

) ) : ( /* 直接显示模式 */ forwards.length > 0 ? ( {}} // 添加空的 onDragStart 处理器 >
{sortedForwards.map((forward) => ( forward && forward.id ? ( ) : null ))}
) : ( /* 空状态 */

暂无转发配置

还没有创建任何转发配置,点击上方按钮开始创建

) )} {/* 新增/编辑模态框 */} {(onClose) => ( <>

{isEdit ? '编辑转发' : '新增转发'}

{isEdit ? '修改现有转发配置的信息' : '创建新的转发配置'}

setForm(prev => ({ ...prev, name: e.target.value }))} isInvalid={!!errors.name} errorMessage={errors.name} variant="bordered" /> { const value = e.target.value; setForm(prev => ({ ...prev, inPort: value ? parseInt(value) : null })); }} isInvalid={!!errors.inPort} errorMessage={errors.inPort} variant="bordered" description="指定入口端口,留空则从节点可用端口中自动分配" />