mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-09 03:06:37 +08:00
2.0版本测试
This commit is contained in:
+348
-139
@@ -103,6 +103,10 @@ interface DiagnosisResult {
|
||||
message?: string;
|
||||
averageTime?: number;
|
||||
packetLoss?: number;
|
||||
fromChainType?: number; // 1: 入口, 2: 链, 3: 出口
|
||||
fromInx?: number;
|
||||
toChainType?: number;
|
||||
toInx?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -196,7 +200,6 @@ export default function ForwardPage() {
|
||||
|
||||
// 表单验证错误
|
||||
const [errors, setErrors] = useState<{[key: string]: string}>({});
|
||||
const [selectedTunnel, setSelectedTunnel] = useState<Tunnel | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -421,16 +424,6 @@ export default function ForwardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (form.inPort !== null && (form.inPort < 1 || form.inPort > 65535)) {
|
||||
newErrors.inPort = '端口号必须在1-65535之间';
|
||||
}
|
||||
|
||||
if (selectedTunnel && selectedTunnel.inNodePortSta && selectedTunnel.inNodePortEnd && form.inPort) {
|
||||
if (form.inPort < selectedTunnel.inNodePortSta || form.inPort > selectedTunnel.inNodePortEnd) {
|
||||
newErrors.inPort = `端口号必须在${selectedTunnel.inNodePortSta}-${selectedTunnel.inNodePortEnd}范围内`;
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
@@ -446,7 +439,6 @@ export default function ForwardPage() {
|
||||
interfaceName: '',
|
||||
strategy: 'fifo'
|
||||
});
|
||||
setSelectedTunnel(null);
|
||||
setErrors({});
|
||||
setModalOpen(true);
|
||||
};
|
||||
@@ -464,8 +456,6 @@ export default function ForwardPage() {
|
||||
interfaceName: forward.interfaceName || '',
|
||||
strategy: forward.strategy || 'fifo'
|
||||
});
|
||||
const tunnel = tunnels.find(t => t.id === forward.tunnelId);
|
||||
setSelectedTunnel(tunnel || null);
|
||||
setErrors({});
|
||||
setModalOpen(true);
|
||||
};
|
||||
@@ -511,8 +501,6 @@ export default function ForwardPage() {
|
||||
|
||||
// 处理隧道选择变化
|
||||
const handleTunnelChange = (tunnelId: string) => {
|
||||
const tunnel = tunnels.find(t => t.id === parseInt(tunnelId));
|
||||
setSelectedTunnel(tunnel || null);
|
||||
setForm(prev => ({ ...prev, tunnelId: parseInt(tunnelId) }));
|
||||
};
|
||||
|
||||
@@ -538,9 +526,7 @@ export default function ForwardPage() {
|
||||
userId: form.userId,
|
||||
name: form.name,
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
interfaceName: form.interfaceName,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
res = await updateForward(updateData);
|
||||
@@ -549,9 +535,7 @@ export default function ForwardPage() {
|
||||
const createData = {
|
||||
name: form.name,
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
interfaceName: form.interfaceName,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
res = await createForward(createData);
|
||||
@@ -694,13 +678,28 @@ export default function ForwardPage() {
|
||||
|
||||
// 格式化入口地址
|
||||
const formatInAddress = (ipString: string, port: number): string => {
|
||||
if (!ipString || !port) return '';
|
||||
if (!ipString) return '';
|
||||
|
||||
const ips = ipString.split(',').map(ip => ip.trim()).filter(ip => ip);
|
||||
if (ips.length === 0) return '';
|
||||
const items = ipString.split(',').map(item => item.trim()).filter(item => item);
|
||||
if (items.length === 0) return '';
|
||||
|
||||
if (ips.length === 1) {
|
||||
const ip = ips[0];
|
||||
// 检查第一项是否已经包含端口(格式: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 {
|
||||
@@ -708,7 +707,7 @@ export default function ForwardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const firstIp = ips[0];
|
||||
const firstIp = items[0];
|
||||
let formattedFirstIp;
|
||||
if (firstIp.includes(':') && !firstIp.startsWith('[')) {
|
||||
formattedFirstIp = `[${firstIp}]`;
|
||||
@@ -716,7 +715,7 @@ export default function ForwardPage() {
|
||||
formattedFirstIp = firstIp;
|
||||
}
|
||||
|
||||
return `${formattedFirstIp}:${port} (+${ips.length - 1})`;
|
||||
return `${formattedFirstIp}:${port} (+${items.length - 1}个)`;
|
||||
};
|
||||
|
||||
// 格式化远程地址
|
||||
@@ -744,18 +743,27 @@ export default function ForwardPage() {
|
||||
let addresses: string[];
|
||||
if (port !== null) {
|
||||
// 入口地址处理
|
||||
const ips = addressString.split(',').map(ip => ip.trim()).filter(ip => ip);
|
||||
if (ips.length <= 1) {
|
||||
const items = addressString.split(',').map(item => item.trim()).filter(item => item);
|
||||
if (items.length <= 1) {
|
||||
copyToClipboard(formatInAddress(addressString, port), title);
|
||||
return;
|
||||
}
|
||||
addresses = ips.map(ip => {
|
||||
if (ip.includes(':') && !ip.startsWith('[')) {
|
||||
return `[${ip}]:${port}`;
|
||||
} else {
|
||||
return `${ip}:${port}`;
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否已经包含端口
|
||||
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);
|
||||
@@ -1588,6 +1596,8 @@ export default function ForwardPage() {
|
||||
isInvalid={!!errors.tunnelId}
|
||||
errorMessage={errors.tunnelId}
|
||||
variant="bordered"
|
||||
isDisabled={isEdit}
|
||||
description={isEdit ? "编辑时无法修改关联隧道" : undefined}
|
||||
>
|
||||
{tunnels.map((tunnel) => (
|
||||
<SelectItem key={tunnel.id} >
|
||||
@@ -1596,25 +1606,6 @@ export default function ForwardPage() {
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
label="入口端口"
|
||||
placeholder="留空自动分配"
|
||||
type="number"
|
||||
value={form.inPort?.toString() || ''}
|
||||
onChange={(e) => setForm(prev => ({
|
||||
...prev,
|
||||
inPort: e.target.value ? parseInt(e.target.value) : null
|
||||
}))}
|
||||
isInvalid={!!errors.inPort}
|
||||
errorMessage={errors.inPort}
|
||||
variant="bordered"
|
||||
description={
|
||||
selectedTunnel && selectedTunnel.inNodePortSta && selectedTunnel.inNodePortEnd
|
||||
? `允许范围: ${selectedTunnel.inNodePortSta}-${selectedTunnel.inNodePortEnd}`
|
||||
: '留空将自动分配可用端口'
|
||||
}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="远程地址"
|
||||
placeholder="请输入远程地址,多个地址用换行分隔 例如: 192.168.1.100:8080 example.com:3000"
|
||||
@@ -1628,17 +1619,6 @@ export default function ForwardPage() {
|
||||
maxRows={6}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="出口网卡名或IP"
|
||||
placeholder="请输入出口网卡名或IP"
|
||||
value={form.interfaceName}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, interfaceName: e.target.value }))}
|
||||
isInvalid={!!errors.interfaceName}
|
||||
errorMessage={errors.interfaceName}
|
||||
variant="bordered"
|
||||
description="用于多IP服务器指定使用那个IP请求远程地址,不懂的默认为空就行"
|
||||
/>
|
||||
|
||||
{getAddressCount(form.remoteAddr) > 1 && (
|
||||
<Select
|
||||
label="负载策略"
|
||||
@@ -2017,16 +1997,21 @@ export default function ForwardPage() {
|
||||
<Modal
|
||||
isOpen={diagnosisModalOpen}
|
||||
onOpenChange={setDiagnosisModalOpen}
|
||||
|
||||
size="2xl"
|
||||
scrollBehavior="outside"
|
||||
backdrop="blur"
|
||||
placement="center"
|
||||
size="4xl"
|
||||
scrollBehavior="inside"
|
||||
backdrop="blur"
|
||||
placement="center"
|
||||
classNames={{
|
||||
base: "rounded-2xl",
|
||||
header: "rounded-t-2xl",
|
||||
body: "rounded-none",
|
||||
footer: "rounded-b-2xl"
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<ModalHeader className="flex flex-col gap-1 bg-content1 border-b border-divider">
|
||||
<h2 className="text-xl font-bold">转发诊断结果</h2>
|
||||
{currentDiagnosisForward && (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@@ -2042,89 +2027,313 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<ModalBody className="bg-content1">
|
||||
{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>
|
||||
<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>
|
||||
{/* 统计摘要 */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="text-center p-3 bg-default-100 dark:bg-gray-800 rounded-lg border border-divider">
|
||||
<div className="text-2xl font-bold text-foreground">{diagnosisResult.results.length}</div>
|
||||
<div className="text-xs text-default-500 mt-1">总测试数</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-success-50 dark:bg-success-900/20 rounded-lg border border-success-200 dark:border-success-700">
|
||||
<div className="text-2xl font-bold text-success-600 dark:text-success-400">
|
||||
{diagnosisResult.results.filter(r => r.success).length}
|
||||
</div>
|
||||
<div className="text-xs text-success-600 dark:text-success-400/80 mt-1">成功</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-danger-50 dark:bg-danger-900/20 rounded-lg border border-danger-200 dark:border-danger-700">
|
||||
<div className="text-2xl font-bold text-danger-600 dark:text-danger-400">
|
||||
{diagnosisResult.results.filter(r => !r.success).length}
|
||||
</div>
|
||||
<div className="text-xs text-danger-600 dark:text-danger-400/80 mt-1">失败</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 桌面端表格展示 */}
|
||||
<div className="hidden md:block space-y-3">
|
||||
{(() => {
|
||||
// 使用后端返回的 chainType 和 inx 字段进行分组
|
||||
const groupedResults = {
|
||||
entry: diagnosisResult.results.filter(r => r.fromChainType === 1),
|
||||
chains: {} as Record<number, typeof diagnosisResult.results>,
|
||||
exit: diagnosisResult.results.filter(r => r.fromChainType === 3)
|
||||
};
|
||||
|
||||
// 按 inx 分组链路测试
|
||||
diagnosisResult.results.forEach(r => {
|
||||
if (r.fromChainType === 2 && r.fromInx != null) {
|
||||
if (!groupedResults.chains[r.fromInx]) {
|
||||
groupedResults.chains[r.fromInx] = [];
|
||||
}
|
||||
groupedResults.chains[r.fromInx].push(r);
|
||||
}
|
||||
});
|
||||
|
||||
const renderTableSection = (title: string, results: typeof diagnosisResult.results) => {
|
||||
if (results.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={title} className="border border-divider rounded-lg overflow-hidden bg-white dark:bg-gray-800">
|
||||
<div className="bg-primary/10 dark:bg-primary/20 px-3 py-2 border-b border-divider">
|
||||
<h3 className="text-sm font-semibold text-primary">{title}</h3>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-default-100 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold text-xs">路径</th>
|
||||
<th className="px-3 py-2 text-center font-semibold text-xs w-20">状态</th>
|
||||
<th className="px-3 py-2 text-center font-semibold text-xs w-24">延迟(ms)</th>
|
||||
<th className="px-3 py-2 text-center font-semibold text-xs w-24">丢包率</th>
|
||||
<th className="px-3 py-2 text-center font-semibold text-xs w-20">质量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-divider bg-white dark:bg-gray-800">
|
||||
{results.map((result, index) => {
|
||||
const quality = getQualityDisplay(result.averageTime, result.packetLoss);
|
||||
|
||||
return (
|
||||
<tr key={index} className={`hover:bg-default-50 dark:hover:bg-gray-700/50 ${
|
||||
result.success ? 'bg-white dark:bg-gray-800' : 'bg-danger-50 dark:bg-danger-900/30'
|
||||
}`}>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${
|
||||
result.success
|
||||
? 'bg-success text-white'
|
||||
: 'bg-danger text-white'
|
||||
}`}>
|
||||
{result.success ? '✓' : '✗'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-foreground truncate">{result.description}</div>
|
||||
<div className="text-xs text-default-500 truncate">
|
||||
{result.targetIp}:{result.targetPort}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
<Chip
|
||||
color={result.success ? 'success' : 'danger'}
|
||||
variant="flat"
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="min-w-[50px]"
|
||||
>
|
||||
{result.success ? '连接成功' : '连接失败'}
|
||||
{result.success ? '成功' : '失败'}
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{result.success ? (
|
||||
<span className="font-semibold text-primary">
|
||||
{result.averageTime?.toFixed(0)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-default-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{result.success ? (
|
||||
<span className={`font-semibold ${
|
||||
(result.packetLoss || 0) > 0 ? 'text-warning' : 'text-success'
|
||||
}`}>
|
||||
{result.packetLoss?.toFixed(1)}%
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-default-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
{result.success && quality ? (
|
||||
<Chip
|
||||
color={quality.color as any}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
>
|
||||
{quality.text}
|
||||
</Chip>
|
||||
) : (
|
||||
<span className="text-default-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardHeader>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 入口测试 */}
|
||||
{renderTableSection('🚪 入口测试', groupedResults.entry)}
|
||||
|
||||
{/* 链路测试(按跳数排序) */}
|
||||
{Object.keys(groupedResults.chains)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map(hop => renderTableSection(`🔗 转发链 - 第${hop}跳`, groupedResults.chains[hop]))}
|
||||
|
||||
{/* 出口测试 */}
|
||||
{renderTableSection('🚀 出口测试', groupedResults.exit)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 移动端卡片展示 */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{(() => {
|
||||
// 使用后端返回的 chainType 和 inx 字段进行分组
|
||||
const groupedResults = {
|
||||
entry: diagnosisResult.results.filter(r => r.fromChainType === 1),
|
||||
chains: {} as Record<number, typeof diagnosisResult.results>,
|
||||
exit: diagnosisResult.results.filter(r => r.fromChainType === 3)
|
||||
};
|
||||
|
||||
// 按 inx 分组链路测试
|
||||
diagnosisResult.results.forEach(r => {
|
||||
if (r.fromChainType === 2 && r.fromInx != null) {
|
||||
if (!groupedResults.chains[r.fromInx]) {
|
||||
groupedResults.chains[r.fromInx] = [];
|
||||
}
|
||||
groupedResults.chains[r.fromInx].push(r);
|
||||
}
|
||||
});
|
||||
|
||||
const renderCardSection = (title: string, results: typeof diagnosisResult.results) => {
|
||||
if (results.length === 0) return null;
|
||||
|
||||
<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>
|
||||
</>
|
||||
return (
|
||||
<div key={title} className="space-y-2">
|
||||
<div className="px-2 py-1.5 bg-primary/10 dark:bg-primary/20 rounded-lg border border-primary/30">
|
||||
<h3 className="text-sm font-semibold text-primary">{title}</h3>
|
||||
</div>
|
||||
{results.map((result, index) => {
|
||||
const quality = getQualityDisplay(result.averageTime, result.packetLoss);
|
||||
|
||||
return (
|
||||
<div key={index} className={`border rounded-lg p-3 ${
|
||||
result.success
|
||||
? 'border-divider bg-white dark:bg-gray-800'
|
||||
: 'border-danger-200 dark:border-danger-300/30 bg-danger-50 dark:bg-danger-900/30'
|
||||
}`}>
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-xs flex-shrink-0 ${
|
||||
result.success ? 'bg-success text-white' : 'bg-danger text-white'
|
||||
}`}>
|
||||
{result.success ? '✓' : '✗'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-sm text-foreground break-words">
|
||||
{result.description}
|
||||
</div>
|
||||
<div className="text-xs text-default-500 mt-0.5 break-all">
|
||||
{result.targetIp}:{result.targetPort}
|
||||
</div>
|
||||
</div>
|
||||
<Chip
|
||||
color={result.success ? 'success' : 'danger'}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{result.success ? '成功' : '失败'}
|
||||
</Chip>
|
||||
</div>
|
||||
|
||||
{result.success ? (
|
||||
<div className="grid grid-cols-3 gap-2 mt-2 pt-2 border-t border-divider">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold text-primary">
|
||||
{result.averageTime?.toFixed(0)}
|
||||
</div>
|
||||
<div className="text-xs text-default-500">延迟(ms)</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-lg font-bold ${
|
||||
(result.packetLoss || 0) > 0 ? 'text-warning' : 'text-success'
|
||||
}`}>
|
||||
{result.packetLoss?.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-xs text-default-500">丢包率</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
{quality && (
|
||||
<>
|
||||
<Chip
|
||||
color={quality.color as any}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
>
|
||||
{quality.text}
|
||||
</Chip>
|
||||
<div className="text-xs text-default-500 mt-0.5">质量</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 pt-2 border-t border-divider">
|
||||
<div className="text-xs text-danger">
|
||||
{result.message || '连接失败'}
|
||||
</div>
|
||||
</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.targetPort ? ':' + result.targetPort : ''}`}>
|
||||
{result.targetIp}{result.targetPort ? ':' + result.targetPort : ''}
|
||||
</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.targetPort ? ':' + result.targetPort : ''}`}>
|
||||
{result.targetIp}{result.targetPort ? ':' + result.targetPort : ''}
|
||||
</code>
|
||||
</div>
|
||||
<Alert
|
||||
color="danger"
|
||||
variant="flat"
|
||||
title="错误详情"
|
||||
description={result.message}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 入口测试 */}
|
||||
{renderCardSection('🚪 入口测试', groupedResults.entry)}
|
||||
|
||||
{/* 链路测试(按跳数排序) */}
|
||||
{Object.keys(groupedResults.chains)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.map(hop => renderCardSection(`🔗 转发链 - 第${hop}跳`, groupedResults.chains[hop]))}
|
||||
|
||||
{/* 出口测试 */}
|
||||
{renderCardSection('🚀 出口测试', groupedResults.exit)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */}
|
||||
{diagnosisResult.results.some(r => !r.success) && (
|
||||
<div className="space-y-2 hidden md:block">
|
||||
<h4 className="text-sm font-semibold text-danger">失败详情</h4>
|
||||
<div className="space-y-2">
|
||||
{diagnosisResult.results.filter(r => !r.success).map((result, index) => (
|
||||
<Alert
|
||||
key={index}
|
||||
color="danger"
|
||||
variant="flat"
|
||||
title={result.description}
|
||||
description={result.message || '连接失败'}
|
||||
className="text-xs"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
@@ -2137,7 +2346,7 @@ export default function ForwardPage() {
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalFooter className="bg-content1 border-t border-divider">
|
||||
<Button variant="light" onPress={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user