mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-01 16:46:36 +08:00
优化ui 添加转发诊断 移除vue前端
This commit is contained in:
+36
-31
@@ -1,5 +1,5 @@
|
||||
import { Route, Routes, Navigate, useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import IndexPage from "@/pages/index";
|
||||
import ChangePasswordPage from "@/pages/change-password";
|
||||
@@ -12,49 +12,54 @@ import LimitPage from "@/pages/limit";
|
||||
|
||||
import { isLoggedIn } from "@/utils/auth";
|
||||
|
||||
// 路由保护组件 - 使用useNavigate避免循环渲染
|
||||
// 简化的路由保护组件 - 使用浏览器重定向避免React循环
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [authenticated, setAuthenticated] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const authenticated = isLoggedIn();
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const loggedIn = isLoggedIn();
|
||||
|
||||
if (!loggedIn) {
|
||||
// 如果未登录,直接导航到登录页
|
||||
navigate('/', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
setAuthenticated(true);
|
||||
setChecking(false);
|
||||
};
|
||||
if (!authenticated) {
|
||||
// 使用浏览器原生重定向,避免React渲染循环
|
||||
window.location.replace('/');
|
||||
}
|
||||
}, [authenticated]);
|
||||
|
||||
checkAuth();
|
||||
}, [navigate]);
|
||||
|
||||
if (checking) {
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-lg">检查登陆状态...</div>
|
||||
<div className="text-lg">重定向中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return authenticated ? <>{children}</> : null;
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
// 登录页面路由组件 - 已登录则重定向到dashboard
|
||||
const LoginRoute = () => {
|
||||
const authenticated = isLoggedIn();
|
||||
|
||||
useEffect(() => {
|
||||
if (authenticated) {
|
||||
// 使用浏览器原生重定向
|
||||
window.location.replace('/dashboard');
|
||||
}
|
||||
}, [authenticated]);
|
||||
|
||||
if (authenticated) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-lg">重定向中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <IndexPage />;
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
isLoggedIn() ? <Navigate to="/dashboard" replace /> : <IndexPage />
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<LoginRoute />} />
|
||||
<Route
|
||||
path="/change-password"
|
||||
element={
|
||||
|
||||
@@ -28,6 +28,10 @@ export const getNodeList = () => Network.post("/node/list");
|
||||
export const updateNode = (data: any) => Network.post("/node/update", data);
|
||||
export const deleteNode = (id: number) => Network.post("/node/delete", { id });
|
||||
export const getNodeInstallCommand = (id: number) => Network.post("/node/install", { id });
|
||||
export const checkNodeStatus = (nodeId?: number) => {
|
||||
const params = nodeId ? { nodeId } : {};
|
||||
return Network.post("/node/check-status", params);
|
||||
};
|
||||
|
||||
// 隧道CRUD操作 - 全部使用POST请求
|
||||
export const createTunnel = (data: any) => Network.post("/tunnel/create", data);
|
||||
@@ -56,6 +60,9 @@ export const forceDeleteForward = (id: number) => Network.post("/forward/force-d
|
||||
export const pauseForwardService = (forwardId: number) => Network.post("/forward/pause", { id: forwardId });
|
||||
export const resumeForwardService = (forwardId: number) => Network.post("/forward/resume", { id: forwardId });
|
||||
|
||||
// 转发诊断操作
|
||||
export const diagnoseForward = (forwardId: number) => Network.post("/forward/diagnose", { forwardId });
|
||||
|
||||
// 限速规则CRUD操作 - 全部使用POST请求
|
||||
export const createSpeedLimit = (data: any) => Network.post("/speed-limit/create", data);
|
||||
export const getSpeedLimitList = () => Network.post("/speed-limit/list");
|
||||
|
||||
@@ -86,16 +86,6 @@ export default function AdminLayout({
|
||||
),
|
||||
adminOnly: true
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
label: '用户管理',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||
</svg>
|
||||
),
|
||||
adminOnly: true
|
||||
},
|
||||
{
|
||||
path: '/limit',
|
||||
label: '限速管理',
|
||||
@@ -105,6 +95,16 @@ export default function AdminLayout({
|
||||
</svg>
|
||||
),
|
||||
adminOnly: true
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
label: '用户管理',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
|
||||
</svg>
|
||||
),
|
||||
adminOnly: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -260,7 +260,7 @@ export default function AdminLayout({
|
||||
<Logo size={28} />
|
||||
<div>
|
||||
<h1 className="text-base font-bold text-foreground">哆啦A梦</h1>
|
||||
<p className="text-xs text-default-500">v1.0.3</p>
|
||||
<p className="text-xs text-default-500">v1.0.4</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@herou
|
||||
import { Chip } from "@heroui/chip";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import { Switch } from "@heroui/switch";
|
||||
import { Alert } from "@heroui/alert";
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import AdminLayout from "@/layouts/admin";
|
||||
@@ -19,7 +20,8 @@ import {
|
||||
forceDeleteForward,
|
||||
userTunnel,
|
||||
pauseForwardService,
|
||||
resumeForwardService
|
||||
resumeForwardService,
|
||||
diagnoseForward
|
||||
} from "@/api";
|
||||
|
||||
interface Forward {
|
||||
@@ -63,6 +65,21 @@ interface AddressItem {
|
||||
copying: boolean;
|
||||
}
|
||||
|
||||
interface DiagnosisResult {
|
||||
forwardName: string;
|
||||
timestamp: number;
|
||||
results: Array<{
|
||||
success: boolean;
|
||||
description: string;
|
||||
nodeName: string;
|
||||
nodeId: string;
|
||||
targetIp: string;
|
||||
message?: string;
|
||||
averageTime?: number;
|
||||
packetLoss?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default function ForwardPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [forwards, setForwards] = useState<Forward[]>([]);
|
||||
@@ -72,10 +89,14 @@ export default function ForwardPage() {
|
||||
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<Forward | null>(null);
|
||||
const [currentDiagnosisForward, setCurrentDiagnosisForward] = useState<Forward | null>(null);
|
||||
const [diagnosisResult, setDiagnosisResult] = useState<DiagnosisResult | null>(null);
|
||||
const [addressModalTitle, setAddressModalTitle] = useState('');
|
||||
const [addressList, setAddressList] = useState<AddressItem[]>([]);
|
||||
|
||||
@@ -359,6 +380,64 @@ export default function ForwardPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 诊断转发
|
||||
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';
|
||||
@@ -518,6 +597,13 @@ export default function ForwardPage() {
|
||||
return addresses.length;
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp: string | number): string => {
|
||||
if (!timestamp) return '-';
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<AdminLayout>
|
||||
@@ -596,9 +682,9 @@ export default function ForwardPage() {
|
||||
title={formatInAddress(forward.inIp, forward.inPort)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-medium text-default-600">入口:</span>
|
||||
<code className="text-xs font-mono text-foreground truncate">
|
||||
<div className="flex items-center gap-1.5 min-w-0 flex-1">
|
||||
<span className="text-xs font-medium text-default-600 flex-shrink-0">入口:</span>
|
||||
<code className="text-xs font-mono text-foreground truncate min-w-0">
|
||||
{formatInAddress(forward.inIp, forward.inPort)}
|
||||
</code>
|
||||
</div>
|
||||
@@ -620,9 +706,9 @@ export default function ForwardPage() {
|
||||
title={formatRemoteAddress(forward.remoteAddr)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-medium text-default-600">目标:</span>
|
||||
<code className="text-xs font-mono text-foreground truncate">
|
||||
<div className="flex items-center gap-1.5 min-w-0 flex-1">
|
||||
<span className="text-xs font-medium text-default-600 flex-shrink-0">目标:</span>
|
||||
<code className="text-xs font-mono text-foreground truncate min-w-0">
|
||||
{formatRemoteAddress(forward.remoteAddr)}
|
||||
</code>
|
||||
</div>
|
||||
@@ -666,6 +752,20 @@ export default function ForwardPage() {
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="warning"
|
||||
onPress={() => handleDiagnose(forward)}
|
||||
className="flex-1 min-h-8"
|
||||
startContent={
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
诊断
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
@@ -711,7 +811,7 @@ export default function ForwardPage() {
|
||||
isOpen={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
size="lg"
|
||||
scrollBehavior="inside"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
@@ -725,7 +825,7 @@ export default function ForwardPage() {
|
||||
</p>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 pb-4">
|
||||
<Input
|
||||
label="转发名称"
|
||||
placeholder="请输入转发名称"
|
||||
@@ -830,6 +930,7 @@ export default function ForwardPage() {
|
||||
isOpen={deleteModalOpen}
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
size="sm"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
@@ -863,7 +964,7 @@ export default function ForwardPage() {
|
||||
</Modal>
|
||||
|
||||
{/* 地址列表弹窗 */}
|
||||
<Modal isOpen={addressModalOpen} onClose={() => setAddressModalOpen(false)} size="lg">
|
||||
<Modal isOpen={addressModalOpen} onClose={() => setAddressModalOpen(false)} size="lg" scrollBehavior="outside">
|
||||
<ModalContent>
|
||||
<ModalHeader className="text-base">{addressModalTitle}</ModalHeader>
|
||||
<ModalBody className="pb-6">
|
||||
@@ -891,6 +992,142 @@ export default function ForwardPage() {
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* 诊断结果模态框 */}
|
||||
<Modal
|
||||
isOpen={diagnosisModalOpen}
|
||||
onOpenChange={setDiagnosisModalOpen}
|
||||
size="3xl"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h2 className="text-xl font-bold">转发诊断结果</h2>
|
||||
{currentDiagnosisForward && (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="text-small text-default-500 truncate flex-1 min-w-0">{currentDiagnosisForward.name}</span>
|
||||
<Chip
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
转发服务
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{diagnosisLoading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="flex items-center gap-3">
|
||||
<Spinner size="sm" />
|
||||
<span className="text-default-600">正在诊断转发连接...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : diagnosisResult ? (
|
||||
<div className="space-y-4">
|
||||
{diagnosisResult.results.map((result, index) => {
|
||||
const quality = getQualityDisplay(result.averageTime, result.packetLoss);
|
||||
|
||||
return (
|
||||
<Card key={index} className={`shadow-sm border ${result.success ? 'border-success' : 'border-danger'}`}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">{result.description}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-small text-default-500">节点: {result.nodeName}</span>
|
||||
<Chip
|
||||
color={result.success ? 'success' : 'danger'}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
>
|
||||
{result.success ? '连接成功' : '连接失败'}
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className="pt-0">
|
||||
{result.success ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-primary">{result.averageTime?.toFixed(0)}</div>
|
||||
<div className="text-small text-default-500">平均延迟(ms)</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-warning">{result.packetLoss?.toFixed(1)}</div>
|
||||
<div className="text-small text-default-500">丢包率(%)</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
{quality && (
|
||||
<>
|
||||
<Chip color={quality.color as any} variant="flat" size="lg">
|
||||
{quality.text}
|
||||
</Chip>
|
||||
<div className="text-small text-default-500 mt-1">连接质量</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-small text-default-500 flex items-center gap-1">
|
||||
<span className="flex-shrink-0">目标地址:</span>
|
||||
<code className="font-mono truncate min-w-0" title={result.targetIp}>{result.targetIp}</code>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="text-small text-default-500 flex items-center gap-1">
|
||||
<span className="flex-shrink-0">目标地址:</span>
|
||||
<code className="font-mono truncate min-w-0" title={result.targetIp}>{result.targetIp}</code>
|
||||
</div>
|
||||
<Alert
|
||||
color="danger"
|
||||
variant="flat"
|
||||
title="错误详情"
|
||||
description={result.message}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-16 h-16 bg-default-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-default-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground">暂无诊断数据</h3>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
{currentDiagnosisForward && (
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={() => handleDiagnose(currentDiagnosisForward)}
|
||||
isLoading={diagnosisLoading}
|
||||
>
|
||||
重新诊断
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
|
||||
@@ -337,7 +337,7 @@ export default function LimitPage() {
|
||||
isOpen={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
size="lg"
|
||||
scrollBehavior="inside"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
@@ -433,6 +433,7 @@ export default function LimitPage() {
|
||||
isOpen={deleteModalOpen}
|
||||
onOpenChange={setDeleteModalOpen}
|
||||
size="sm"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
getNodeList,
|
||||
updateNode,
|
||||
deleteNode,
|
||||
getNodeInstallCommand
|
||||
getNodeInstallCommand,
|
||||
checkNodeStatus
|
||||
} from "@/api";
|
||||
|
||||
interface Node {
|
||||
@@ -71,6 +72,11 @@ export default function NodePage() {
|
||||
const [installCommand, setInstallCommand] = useState('');
|
||||
const [currentNodeName, setCurrentNodeName] = useState('');
|
||||
|
||||
// 状态检查相关状态
|
||||
const [statusCheckLoading, setStatusCheckLoading] = useState(false);
|
||||
const [statusCheckResult, setStatusCheckResult] = useState<any>(null);
|
||||
const [statusCheckModal, setStatusCheckModal] = useState(false);
|
||||
|
||||
const websocketRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const reconnectAttemptsRef = useRef(0);
|
||||
@@ -446,6 +452,55 @@ export default function NodePage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 状态检查处理函数
|
||||
const handleCheckAllStatus = async () => {
|
||||
setStatusCheckLoading(true);
|
||||
try {
|
||||
const res = await checkNodeStatus();
|
||||
if (res.code === 0) {
|
||||
setStatusCheckResult(res.data);
|
||||
setStatusCheckModal(true);
|
||||
|
||||
// 刷新节点列表以显示最新状态
|
||||
await loadNodes();
|
||||
|
||||
const { totalNodes, connectedNodes, inconsistentNodes } = res.data;
|
||||
if (inconsistentNodes > 0) {
|
||||
toast.success(`状态检查完成!修复了 ${inconsistentNodes} 个节点的状态不一致问题`);
|
||||
} else {
|
||||
toast.success(`状态检查完成!${connectedNodes}/${totalNodes} 个节点在线`);
|
||||
}
|
||||
} else {
|
||||
toast.error(res.msg || '状态检查失败');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('网络错误,请重试');
|
||||
} finally {
|
||||
setStatusCheckLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckSingleNodeStatus = async (nodeId: number) => {
|
||||
try {
|
||||
const res = await checkNodeStatus(nodeId);
|
||||
if (res.code === 0) {
|
||||
// 刷新节点列表
|
||||
await loadNodes();
|
||||
|
||||
const result = res.data.details[0];
|
||||
if (result.fixed) {
|
||||
toast.success(`节点状态已修复:${result.message}`);
|
||||
} else {
|
||||
toast.success(result.message);
|
||||
}
|
||||
} else {
|
||||
toast.error(res.msg || '状态检查失败');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('网络错误,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return;
|
||||
@@ -522,17 +577,32 @@ export default function NodePage() {
|
||||
{/* 页面头部 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold text-foreground">节点管理</h1>
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleAdd}
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
新增节点
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="flat"
|
||||
onPress={handleCheckAllStatus}
|
||||
isLoading={statusCheckLoading}
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
状态检查
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleAdd}
|
||||
startContent={
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
新增节点
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 节点列表 */}
|
||||
@@ -588,14 +658,18 @@ export default function NodePage() {
|
||||
<CardBody className="pt-0 pb-3">
|
||||
{/* 基础信息 */}
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-default-600">入口IP</span>
|
||||
<div className="text-right text-xs">
|
||||
<div className="flex justify-between items-center text-sm min-w-0">
|
||||
<span className="text-default-600 flex-shrink-0">入口IP</span>
|
||||
<div className="text-right text-xs min-w-0 flex-1 ml-2">
|
||||
{node.ip ? (
|
||||
node.ip.split(',').length > 1 ? (
|
||||
<span className="font-mono">{node.ip.split(',')[0].trim()} +{node.ip.split(',').length - 1}个</span>
|
||||
<span className="font-mono truncate block" title={node.ip.split(',')[0].trim()}>
|
||||
{node.ip.split(',')[0].trim()} +{node.ip.split(',').length - 1}个
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-mono">{node.ip.trim()}</span>
|
||||
<span className="font-mono truncate block" title={node.ip.trim()}>
|
||||
{node.ip.trim()}
|
||||
</span>
|
||||
)
|
||||
) : '-'}
|
||||
</div>
|
||||
@@ -678,35 +752,48 @@ export default function NodePage() {
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="success"
|
||||
onPress={() => handleCopyInstallCommand(node)}
|
||||
isLoading={node.copyLoading}
|
||||
className="flex-1 min-h-8"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="primary"
|
||||
onPress={() => handleEdit(node)}
|
||||
className="flex-1 min-h-8"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
onPress={() => handleDelete(node)}
|
||||
className="flex-1 min-h-8"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="success"
|
||||
onPress={() => handleCopyInstallCommand(node)}
|
||||
isLoading={node.copyLoading}
|
||||
className="flex-1 min-h-8"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="primary"
|
||||
onPress={() => handleEdit(node)}
|
||||
className="flex-1 min-h-8"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="secondary"
|
||||
onPress={() => handleCheckSingleNodeStatus(node.id)}
|
||||
className="flex-1 min-h-8"
|
||||
>
|
||||
检查
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
color="danger"
|
||||
onPress={() => handleDelete(node)}
|
||||
className="flex-1 min-h-8"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
@@ -719,7 +806,7 @@ export default function NodePage() {
|
||||
isOpen={dialogVisible}
|
||||
onClose={() => setDialogVisible(false)}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>{dialogTitle}</ModalHeader>
|
||||
@@ -810,7 +897,7 @@ export default function NodePage() {
|
||||
isOpen={installCommandModal}
|
||||
onClose={() => setInstallCommandModal(false)}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>安装命令 - {currentNodeName}</ModalHeader>
|
||||
@@ -856,6 +943,106 @@ export default function NodePage() {
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* 状态检查结果模态框 */}
|
||||
<Modal
|
||||
isOpen={statusCheckModal}
|
||||
onClose={() => setStatusCheckModal(false)}
|
||||
size="3xl"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<span>节点状态检查结果</span>
|
||||
{statusCheckResult && (
|
||||
<div className="flex gap-4 text-sm font-normal">
|
||||
<span className="text-default-600">
|
||||
总节点: <span className="font-semibold">{statusCheckResult.totalNodes}</span>
|
||||
</span>
|
||||
<span className="text-success-600">
|
||||
在线: <span className="font-semibold">{statusCheckResult.connectedNodes}</span>
|
||||
</span>
|
||||
<span className="text-warning-600">
|
||||
修复: <span className="font-semibold">{statusCheckResult.inconsistentNodes}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{statusCheckResult && statusCheckResult.details && (
|
||||
<div className="space-y-3">
|
||||
{statusCheckResult.details.map((detail: any) => (
|
||||
<Card key={detail.nodeId} className="border border-divider">
|
||||
<CardBody className="py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-foreground">
|
||||
{detail.nodeName || `节点 ${detail.nodeId}`}
|
||||
</span>
|
||||
<span className="text-xs text-default-500">
|
||||
ID: {detail.nodeId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Chip
|
||||
size="sm"
|
||||
color={detail.connected ? 'success' : 'danger'}
|
||||
variant="flat"
|
||||
>
|
||||
{detail.connected ? '在线' : '离线'}
|
||||
</Chip>
|
||||
<Chip
|
||||
size="sm"
|
||||
color={detail.fixed ? 'warning' : 'default'}
|
||||
variant={detail.fixed ? 'flat' : 'bordered'}
|
||||
>
|
||||
{detail.fixed ? '已修复' : '正常'}
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-sm">
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="text-default-600">数据库状态:</span>
|
||||
<span className={`font-mono ${detail.databaseStatus === 1 ? 'text-success-600' : 'text-danger-600'}`}>
|
||||
{detail.databaseStatus === 1 ? '在线' : '离线'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs mt-1">
|
||||
<span className="text-default-600">实际状态:</span>
|
||||
<span className={`font-mono ${detail.actualStatus ? 'text-success-600' : 'text-danger-600'}`}>
|
||||
{detail.actualStatus ? '连接中' : '未连接'}
|
||||
</span>
|
||||
</div>
|
||||
{detail.fixed && (
|
||||
<div className="flex justify-between items-center text-xs mt-1">
|
||||
<span className="text-default-600">最终状态:</span>
|
||||
<span className={`font-mono ${detail.finalStatus === 1 ? 'text-success-600' : 'text-danger-600'}`}>
|
||||
{detail.finalStatus === 1 ? '在线' : '离线'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 p-2 bg-default-50 dark:bg-default-100 rounded text-xs">
|
||||
{detail.message}
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="flat"
|
||||
onPress={() => setStatusCheckModal(false)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
|
||||
@@ -608,7 +608,7 @@ export default function TunnelPage() {
|
||||
isOpen={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
@@ -896,7 +896,7 @@ export default function TunnelPage() {
|
||||
isOpen={diagnosisModalOpen}
|
||||
onOpenChange={setDiagnosisModalOpen}
|
||||
size="3xl"
|
||||
scrollBehavior="inside"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
|
||||
@@ -152,6 +152,14 @@ export default function UserPage() {
|
||||
const [editTunnelForm, setEditTunnelForm] = useState<UserTunnel | null>(null);
|
||||
const [editTunnelLoading, setEditTunnelLoading] = useState(false);
|
||||
|
||||
// 删除确认相关状态
|
||||
const { isOpen: isDeleteModalOpen, onOpen: onDeleteModalOpen, onClose: onDeleteModalClose } = useDisclosure();
|
||||
const [userToDelete, setUserToDelete] = useState<User | null>(null);
|
||||
|
||||
// 删除隧道权限确认相关状态
|
||||
const { isOpen: isDeleteTunnelModalOpen, onOpen: onDeleteTunnelModalOpen, onClose: onDeleteTunnelModalClose } = useDisclosure();
|
||||
const [tunnelToDelete, setTunnelToDelete] = useState<UserTunnel | null>(null);
|
||||
|
||||
// 其他数据
|
||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||
const [speedLimits, setSpeedLimits] = useState<SpeedLimit[]>([]);
|
||||
@@ -266,12 +274,21 @@ export default function UserPage() {
|
||||
onUserModalOpen();
|
||||
};
|
||||
|
||||
const handleDelete = async (user: User) => {
|
||||
const handleDelete = (user: User) => {
|
||||
setUserToDelete(user);
|
||||
onDeleteModalOpen();
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!userToDelete) return;
|
||||
|
||||
try {
|
||||
const response = await deleteUser(user.id);
|
||||
const response = await deleteUser(userToDelete.id);
|
||||
if (response.code === 0) {
|
||||
toast.success('删除成功');
|
||||
loadUsers();
|
||||
onDeleteModalClose();
|
||||
setUserToDelete(null);
|
||||
} else {
|
||||
toast.error(response.msg || '删除失败');
|
||||
}
|
||||
@@ -405,14 +422,23 @@ export default function UserPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTunnel = async (userTunnel: UserTunnel) => {
|
||||
const handleRemoveTunnel = (userTunnel: UserTunnel) => {
|
||||
setTunnelToDelete(userTunnel);
|
||||
onDeleteTunnelModalOpen();
|
||||
};
|
||||
|
||||
const handleConfirmRemoveTunnel = async () => {
|
||||
if (!tunnelToDelete) return;
|
||||
|
||||
try {
|
||||
const response = await removeUserTunnel({ id: userTunnel.id });
|
||||
const response = await removeUserTunnel({ id: tunnelToDelete.id });
|
||||
if (response.code === 0) {
|
||||
toast.success('删除成功');
|
||||
if (currentUser) {
|
||||
loadUserTunnels(currentUser.id);
|
||||
}
|
||||
onDeleteTunnelModalClose();
|
||||
setTunnelToDelete(null);
|
||||
} else {
|
||||
toast.error(response.msg || '删除失败');
|
||||
}
|
||||
@@ -646,7 +672,7 @@ export default function UserPage() {
|
||||
isOpen={isUserModalOpen}
|
||||
onClose={onUserModalClose}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
@@ -701,7 +727,7 @@ export default function UserPage() {
|
||||
value={userForm.expTime ? parseDate(userForm.expTime.toISOString().split('T')[0]) : null}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const jsDate = new Date(date.year, date.month - 1, date.day);
|
||||
const jsDate = new Date(date.year, date.month - 1, date.day, 23, 59, 59);
|
||||
setUserForm(prev => ({ ...prev, expTime: jsDate }));
|
||||
} else {
|
||||
setUserForm(prev => ({ ...prev, expTime: null }));
|
||||
@@ -743,7 +769,7 @@ export default function UserPage() {
|
||||
isOpen={isTunnelModalOpen}
|
||||
onClose={onTunnelModalClose}
|
||||
size="5xl"
|
||||
scrollBehavior="inside"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
@@ -822,7 +848,7 @@ export default function UserPage() {
|
||||
value={tunnelForm.expTime ? parseDate(tunnelForm.expTime.toISOString().split('T')[0]) : null}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const jsDate = new Date(date.year, date.month - 1, date.day);
|
||||
const jsDate = new Date(date.year, date.month - 1, date.day, 23, 59, 59);
|
||||
setTunnelForm(prev => ({ ...prev, expTime: jsDate }));
|
||||
} else {
|
||||
setTunnelForm(prev => ({ ...prev, expTime: null }));
|
||||
@@ -937,6 +963,7 @@ export default function UserPage() {
|
||||
isOpen={isEditTunnelModalOpen}
|
||||
onClose={onEditTunnelModalClose}
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
@@ -991,6 +1018,22 @@ export default function UserPage() {
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<DatePicker
|
||||
label="到期时间"
|
||||
value={editTunnelForm.expTime ? parseDate(new Date(editTunnelForm.expTime).toISOString().split('T')[0]) : null}
|
||||
onChange={(date) => {
|
||||
if (date) {
|
||||
const jsDate = new Date(date.year, date.month - 1, date.day, 23, 59, 59);
|
||||
setEditTunnelForm(prev => prev ? { ...prev, expTime: jsDate.getTime() } : null);
|
||||
} else {
|
||||
setEditTunnelForm(prev => prev ? { ...prev, expTime: Date.now() } : null);
|
||||
}
|
||||
}}
|
||||
showMonthAndYearPickers
|
||||
className="cursor-pointer"
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
@@ -1008,6 +1051,92 @@ export default function UserPage() {
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<Modal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={onDeleteModalClose}
|
||||
size="md"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
确认删除用户
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-danger-100 rounded-full flex items-center justify-center">
|
||||
<DeleteIcon className="w-6 h-6 text-danger" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-foreground">
|
||||
确定要删除用户 <span className="font-semibold text-danger">"{userToDelete?.user}"</span> 吗?
|
||||
</p>
|
||||
<p className="text-small text-default-500 mt-1">
|
||||
此操作不可撤销,用户的所有数据将被永久删除。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={onDeleteModalClose}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
onPress={handleConfirmDelete}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* 删除隧道权限确认对话框 */}
|
||||
<Modal
|
||||
isOpen={isDeleteTunnelModalOpen}
|
||||
onClose={onDeleteTunnelModalClose}
|
||||
size="md"
|
||||
scrollBehavior="outside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
确认删除隧道权限
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-danger-100 rounded-full flex items-center justify-center">
|
||||
<DeleteIcon className="w-6 h-6 text-danger" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-foreground">
|
||||
确定要删除用户 <span className="font-semibold">{currentUser?.user}</span> 对隧道 <span className="font-semibold text-danger">"{tunnelToDelete?.tunnelName}"</span> 的权限吗?
|
||||
</p>
|
||||
<p className="text-small text-default-500 mt-1">
|
||||
删除后该用户将无法使用此隧道创建转发,此操作不可撤销。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={onDeleteTunnelModalClose}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
onPress={handleConfirmRemoveTunnel}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user