import { useState, useEffect, useMemo } from "react"; 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 { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { Button } from "@/shadcn-bridge/heroui/button"; import { Input } from "@/shadcn-bridge/heroui/input"; import { Textarea } from "@/shadcn-bridge/heroui/input"; import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; import { Table, TableBody, TableCell, TableColumn, TableHeader, TableRow, } from "@/shadcn-bridge/heroui/table"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, } from "@/shadcn-bridge/heroui/modal"; import { Chip } from "@/shadcn-bridge/heroui/chip"; import { Spinner } from "@/shadcn-bridge/heroui/spinner"; import { Switch } from "@/shadcn-bridge/heroui/switch"; import { Alert } from "@/shadcn-bridge/heroui/alert"; import { Checkbox } from "@/shadcn-bridge/heroui/checkbox"; import { createForward, getForwardList, updateForward, deleteForward, forceDeleteForward, userTunnel, pauseForwardService, resumeForwardService, diagnoseForward, updateForwardOrder, } from "@/api"; import { type ForwardAddressItem, formatInAddress, formatRemoteAddress, hasMultipleAddresses, resolveForwardAddressAction, } from "@/pages/forward/address"; import { buildForwardDiagnosisFallbackResult, getForwardDiagnosisQualityDisplay, type ForwardDiagnosisResult, } from "@/pages/forward/diagnosis"; import { executeForwardBatchChangeTunnel, executeForwardBatchDelete, executeForwardBatchRedeploy, executeForwardBatchToggleService, } from "@/pages/forward/batch-actions"; import { buildForwardOrder, FORWARD_ORDER_KEY } from "@/pages/forward/order"; import { PageLoadingState } from "@/components/page-state"; import { useMobileBreakpoint } from "@/hooks/useMobileBreakpoint"; import { saveOrder } from "@/utils/order-storage"; 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 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 = useMobileBreakpoint(); // 显示模式状态 - 从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 }>({}); // 批量操作相关状态 const [selectMode, setSelectMode] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); const [batchDeleteModalOpen, setBatchDeleteModalOpen] = useState(false); const [batchChangeTunnelModalOpen, setBatchChangeTunnelModalOpen] = useState(false); const [batchTargetTunnelId, setBatchTargetTunnelId] = useState( null, ); const [batchLoading, setBatchLoading] = useState(false); 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(); const { order, fromDatabase } = buildForwardOrder( forwards, currentUserId, ); setForwardOrder(order); if (fromDatabase) { saveOrder(FORWARD_ORDER_KEY, order); } } } catch { } }; // 加载所有数据 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(); const { order, fromDatabase } = buildForwardOrder( forwardsData, currentUserId, ); setForwardOrder(order); if (fromDatabase) { saveOrder(FORWARD_ORDER_KEY, order); } } } else { toast.error(forwardsRes.msg || "获取转发列表失败"); } if (tunnelsRes.code === 0) { setTunnels(tunnelsRes.data || []); } else { } } catch { 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 { 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: { code: number; msg: string }; 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 { 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: { code: number; msg: string }; 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 { // 操作失败,恢复UI状态 setForwards((prev) => prev.map((f) => f.id === forward.id ? { ...f, serviceRunning: !targetState } : f, ), ); 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 as ForwardDiagnosisResult); } else { toast.error(response.msg || "诊断失败"); setDiagnosisResult( buildForwardDiagnosisFallbackResult({ forwardName: forward.name, remoteAddr: forward.remoteAddr, description: "诊断失败", message: response.msg || "诊断过程中发生错误", }), ); } } catch { toast.error("网络错误,请重试"); setDiagnosisResult( buildForwardDiagnosisFallbackResult({ forwardName: forward.name, remoteAddr: forward.remoteAddr, description: "网络错误", message: "无法连接到服务器", }), ); } finally { setDiagnosisLoading(false); } }; // 格式化流量 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 showAddressModal = ( addressString: string, port: number | null, title: string, ) => { const action = resolveForwardAddressAction(addressString, port, title); if (action.type === "none") { return; } if (action.type === "copy") { copyToClipboard(action.text, action.label); return; } setAddressList(action.items); setAddressModalTitle(action.title); setAddressModalOpen(true); }; // 复制到剪贴板 const copyToClipboard = async (text: string, label: string = "内容") => { try { await navigator.clipboard.writeText(text); toast.success(`已复制${label}`); } catch { toast.error("复制失败"); } }; // 复制地址 const copyAddress = async (addressItem: ForwardAddressItem) => { try { setAddressList((prev) => prev.map((item) => item.id === addressItem.id ? { ...item, copying: true } : item, ), ); await copyToClipboard(addressItem.address, "地址"); } catch { 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 { 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 { setImportResults((prev) => [ { line, success: false, message: "网络错误,创建失败", }, ...prev, ]); } } toast.success(`导入执行完成`); // 导入完成后刷新转发列表 await loadData(false); } catch { 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); saveOrder(FORWARD_ORDER_KEY, newOrder); // 持久化到数据库 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 { toast.error("保存排序失败,请重试"); } } }; const toggleSelectMode = () => { setSelectMode(!selectMode); if (selectMode) { setSelectedIds(new Set()); } }; const toggleSelect = (id: number) => { const newSet = new Set(selectedIds); if (newSet.has(id)) { newSet.delete(id); } else { newSet.add(id); } setSelectedIds(newSet); }; const selectAll = () => { const allIds = sortedForwards.map((f) => f.id); setSelectedIds(new Set(allIds)); }; const deselectAll = () => { setSelectedIds(new Set()); }; const handleBatchDelete = async () => { if (selectedIds.size === 0) return; setBatchLoading(true); try { const outcome = await executeForwardBatchDelete(Array.from(selectedIds)); if (outcome.toastVariant === "success") { toast.success(outcome.toastMessage); } else { toast.error(outcome.toastMessage); } if (outcome.shouldRefresh) { setSelectedIds(new Set()); setSelectMode(false); if (outcome.closeDeleteModal) { setBatchDeleteModalOpen(false); } loadData(false); } } finally { setBatchLoading(false); } }; const handleBatchToggleService = async (enable: boolean) => { if (selectedIds.size === 0) return; setBatchLoading(true); try { const outcome = await executeForwardBatchToggleService( Array.from(selectedIds), enable, ); if (outcome.toastVariant === "success") { toast.success(outcome.toastMessage); } else { toast.error(outcome.toastMessage); } if (outcome.shouldRefresh) { setSelectedIds(new Set()); setSelectMode(false); loadData(false); } } finally { setBatchLoading(false); } }; const handleBatchRedeploy = async () => { if (selectedIds.size === 0) return; setBatchLoading(true); try { const outcome = await executeForwardBatchRedeploy( Array.from(selectedIds), ); if (outcome.toastVariant === "success") { toast.success(outcome.toastMessage); } else { toast.error(outcome.toastMessage); } if (outcome.shouldRefresh) { setSelectedIds(new Set()); setSelectMode(false); loadData(false); } } finally { setBatchLoading(false); } }; const handleBatchChangeTunnel = async () => { if (selectedIds.size === 0 || !batchTargetTunnelId) return; setBatchLoading(true); try { const outcome = await executeForwardBatchChangeTunnel( Array.from(selectedIds), batchTargetTunnelId, ); if (outcome.toastVariant === "success") { toast.success(outcome.toastMessage); } else { toast.error(outcome.toastMessage); } if (outcome.shouldRefresh) { setSelectedIds(new Set()); setSelectMode(false); if (outcome.closeChangeTunnelModal) { setBatchChangeTunnelModalOpen(false); } if (outcome.resetTargetTunnel) { setBatchTargetTunnelId(null); } loadData(false); } } finally { setBatchLoading(false); } }; // 传感器配置 - 使用默认配置避免错误 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 }) => { 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 (
{selectMode && ( toggleSelect(forward.id)} /> )}

{forward.name}

{forward.tunnelName}

{viewMode === "direct" && (
)} handleServiceToggle(forward)} /> {statusDisplay.text}
{/* 地址信息 */}
{/* 统计信息 */}
{strategyDisplay.text}
↑{formatFlow(forward.inFlow || 0)}
↓{formatFlow(forward.outFlow || 0)}
); }; if (loading) { return ; } return (
{/* 页面头部 */}
{/* 显示模式切换按钮 */} {/* 导入按钮 */} {/* 导出按钮 */}
{selectMode && selectedIds.size > 0 && (
已选择 {selectedIds.size} 项
)} {/* 根据显示模式渲染不同内容 */} {viewMode === "grouped" ? ( sortedForwards.length > 0 ? (
{selectMode && 选择} 用户 隧道 名称 入口 目标 策略 流量 状态 操作 {(forward) => { const statusDisplay = getStatusDisplay(forward.status); const strategyDisplay = getStrategyDisplay(forward.strategy); return ( {selectMode && ( toggleSelect(forward.id)} /> )}
{(forward.userName || "未")[0].toUpperCase()}
{forward.userName || "未知用户"}
{forward.tunnelName} {forward.name} {strategyDisplay.text}
↑ {formatFlow(forward.inFlow || 0)} ↓ {formatFlow(forward.outFlow || 0)}
handleServiceToggle(forward)} /> {statusDisplay.text}
); }}
) : ( /* 空状态 */

暂无转发配置

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

) ) : /* 直接显示模式 */ 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, })); }} />