106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import { TEST_NODES, LatencyResult, getLatencyColor, getLatencyLevel } from '@shared/types'
|
|
import { useLanguage } from '../contexts/LanguageContext'
|
|
import './ResultsPanel.css'
|
|
|
|
interface ResultsPanelProps {
|
|
results: Map<string, LatencyResult>
|
|
selectedNodeId: string | null
|
|
onNodeSelect: (nodeId: string | null) => void
|
|
}
|
|
|
|
export default function ResultsPanel({ results, selectedNodeId, onNodeSelect }: ResultsPanelProps) {
|
|
const { t } = useLanguage()
|
|
|
|
if (results.size === 0) return null
|
|
|
|
const sortedNodes = [...TEST_NODES].sort((a, b) => {
|
|
const aResult = results.get(a.id)
|
|
const bResult = results.get(b.id)
|
|
const aLatency = aResult?.latency ?? Infinity
|
|
const bLatency = bResult?.latency ?? Infinity
|
|
return aLatency - bLatency
|
|
})
|
|
|
|
const completedResults = sortedNodes
|
|
.map((node) => ({ node, result: results.get(node.id) }))
|
|
.filter(({ result }) => result?.status === 'success' || result?.status === 'failed')
|
|
|
|
const avgLatency =
|
|
completedResults.length > 0
|
|
? Math.round(
|
|
completedResults
|
|
.filter(({ result }) => result?.latency !== null)
|
|
.reduce((sum, { result }) => sum + (result?.latency ?? 0), 0) /
|
|
completedResults.filter(({ result }) => result?.latency !== null).length
|
|
)
|
|
: null
|
|
|
|
const regionMap: Record<string, string> = {
|
|
'North America': '北美',
|
|
'Europe': '欧洲',
|
|
'Asia': '亚洲',
|
|
'Middle East': '中东',
|
|
'South America': '南美',
|
|
'Africa': '非洲',
|
|
'Oceania': '大洋洲'
|
|
}
|
|
|
|
return (
|
|
<div className="results-panel">
|
|
<div className="results-header">
|
|
<h2>{t('测试结果', 'Test Results')}</h2>
|
|
<div className="results-header-right">
|
|
<div className="latency-legend">
|
|
<div className="legend-gradient"></div>
|
|
<div className="legend-labels">
|
|
<span>0ms</span>
|
|
<span>50</span>
|
|
<span>100</span>
|
|
<span>150</span>
|
|
<span>200</span>
|
|
<span>250+</span>
|
|
</div>
|
|
</div>
|
|
{avgLatency !== null && (
|
|
<div className="avg-latency">
|
|
{t('平均', 'Avg')}: <span style={{ color: getLatencyColor(avgLatency) }}>{avgLatency}ms</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="results-grid">
|
|
{sortedNodes.map((node) => {
|
|
const result = results.get(node.id)
|
|
const isTesting = result?.status === 'testing'
|
|
const hasResult = result?.status === 'success' || result?.status === 'failed'
|
|
|
|
const isSelected = selectedNodeId === node.id
|
|
|
|
return (
|
|
<div
|
|
key={node.id}
|
|
className={`result-card ${hasResult ? getLatencyLevel(result?.latency ?? null) : ''} ${isSelected ? 'selected' : ''}`}
|
|
onClick={() => onNodeSelect(isSelected ? null : node.id)}
|
|
>
|
|
<div className="result-region">{t(regionMap[node.region] || node.region, node.region)}</div>
|
|
<div className="result-name">{node.name}</div>
|
|
<div className="result-latency">
|
|
{isTesting ? (
|
|
<span className="testing-indicator">{t('测试中...', 'Testing...')}</span>
|
|
) : hasResult ? (
|
|
<span style={{ color: getLatencyColor(result?.latency ?? null) }}>
|
|
{result?.latency !== null ? `${result.latency}ms` : t('超时', 'Timeout')}
|
|
</span>
|
|
) : (
|
|
<span className="pending">—</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|