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< number | null >(null); // 导入相关状态 const [importModalOpen, setImportModalOpen] = useState(false); const [importData, setImportData] = useState(""); const [importLoading, setImportLoading] = useState(false); const [selectedTunnelForImport, setSelectedTunnelForImport] = useState< number | null >(null); const [importResults, setImportResults] = useState< Array<{ line: string; success: boolean; message: string; forwardName?: string; }> >([]); // 表单状态 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)} /> {statusDisplay.text}
{/* 地址信息 */}
showAddressModal(forward.inIp, forward.inPort, '入口端口')} >
入口: {formatInAddress(forward.inIp, forward.inPort)}
{hasMultipleAddresses(forward.inIp) && ( )}
showAddressModal(forward.remoteAddr, null, '目标地址')} >
目标: {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 }))} /> { const value = e.target.value; setForm(prev => ({ ...prev, inPort: value ? parseInt(value) : null })); }} />