import { useState, useEffect, useMemo, useRef } 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 {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from "@heroui/modal";
import { Chip } from "@heroui/chip";
import { Switch } from "@heroui/switch";
import { Spinner } from "@heroui/spinner";
import { Alert } from "@heroui/alert";
import { Progress } from "@heroui/progress";
import { Accordion, AccordionItem } from "@heroui/accordion";
import { Select, SelectItem } from "@heroui/select";
import { Checkbox } from "@heroui/checkbox";
import toast from "react-hot-toast";
import axios from "axios";
import {
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
type DragEndEvent,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
arrayMove,
rectSortingStrategy,
sortableKeyboardCoordinates,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
createNode,
getNodeList,
updateNode,
deleteNode,
getNodeInstallCommand,
updateNodeOrder,
batchDeleteNodes,
upgradeNode,
batchUpgradeNodes,
getNodeReleases,
rollbackNode,
} from "@/api";
interface Node {
id: number;
inx?: number;
name: string;
ip: string;
serverIp: string;
serverIpV4?: string;
serverIpV6?: string;
port: string;
tcpListenAddr?: string;
udpListenAddr?: string;
version?: string;
http?: number; // 0 关 1 开
tls?: number; // 0 关 1 开
socks?: number; // 0 关 1 开
status: number;
isRemote?: number;
remoteUrl?: string;
syncError?: string;
connectionStatus: "online" | "offline";
systemInfo?: {
cpuUsage: number;
memoryUsage: number;
uploadTraffic: number;
downloadTraffic: number;
uploadSpeed: number;
downloadSpeed: number;
uptime: number;
} | null;
copyLoading?: boolean;
upgradeLoading?: boolean;
rollbackLoading?: boolean;
}
interface NodeForm {
id: number | null;
name: string;
serverHost: string;
serverIpV4: string;
serverIpV6: string;
port: string;
tcpListenAddr: string;
udpListenAddr: string;
interfaceName: string;
http: number; // 0 关 1 开
tls: number; // 0 关 1 开
socks: number; // 0 关 1 开
}
const SortableItem = ({
id,
children,
}: {
id: number;
children: (listeners: any) => any;
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: transform ? CSS.Transform.toString(transform) : undefined,
transition: isDragging ? undefined : transition || undefined,
opacity: isDragging ? 0.5 : 1,
willChange: "transform",
};
return (
{children(listeners)}
);
};
export default function NodePage() {
const [nodeList, setNodeList] = useState([]);
const [nodeOrder, setNodeOrder] = useState([]);
const [loading, setLoading] = useState(false);
const [wsConnected, setWsConnected] = useState(false);
const [wsConnecting, setWsConnecting] = useState(false);
const [dialogVisible, setDialogVisible] = useState(false);
const [dialogTitle, setDialogTitle] = useState("");
const [isEdit, setIsEdit] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [nodeToDelete, setNodeToDelete] = useState(null);
const [protocolDisabled, setProtocolDisabled] = useState(false);
const [protocolDisabledReason, setProtocolDisabledReason] = useState("");
const [form, setForm] = useState({
id: null,
name: "",
serverHost: "",
serverIpV4: "",
serverIpV6: "",
port: "1000-65535",
tcpListenAddr: "[::]",
udpListenAddr: "[::]",
interfaceName: "",
http: 0,
tls: 0,
socks: 0,
});
const [errors, setErrors] = useState>({});
const [selectMode, setSelectMode] = useState(false);
const [selectedIds, setSelectedIds] = useState>(new Set());
const [batchDeleteModalOpen, setBatchDeleteModalOpen] = useState(false);
const [batchLoading, setBatchLoading] = useState(false);
// 安装命令相关状态
const [installCommandModal, setInstallCommandModal] = useState(false);
const [installCommand, setInstallCommand] = useState("");
const [currentNodeName, setCurrentNodeName] = useState("");
// 升级相关状态
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
const [upgradeTarget, setUpgradeTarget] = useState<"single" | "batch">(
"single",
);
const [upgradeTargetNodeId, setUpgradeTargetNodeId] = useState(
null,
);
const [releases, setReleases] = useState<
Array<{
version: string;
name: string;
publishedAt: string;
prerelease: boolean;
}>
>([]);
const [releasesLoading, setReleasesLoading] = useState(false);
const [selectedVersion, setSelectedVersion] = useState("");
const [batchUpgradeLoading, setBatchUpgradeLoading] = useState(false);
const [upgradeProgress, setUpgradeProgress] = useState<
Record
>({});
const websocketRef = useRef(null);
const reconnectTimerRef = useRef(null);
const reconnectAttemptsRef = useRef(0);
const maxReconnectAttempts = 5;
const offlineTimersRef = useRef