256 lines
9.6 KiB
TypeScript
256 lines
9.6 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { validateNumber } from '@/lib/formatters'
|
|
import { Pagination } from '@/components/ui/pagination'
|
|
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
|
|
|
interface Edge {
|
|
id: number
|
|
macaddr: string
|
|
city: string
|
|
public: string
|
|
isp: string
|
|
single: number | boolean
|
|
sole: number | boolean
|
|
arch: number
|
|
online: number
|
|
}
|
|
|
|
export default function Edge() {
|
|
const [data, setData] = useState<Edge[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// 分页状态
|
|
const [currentPage, setCurrentPage] = useState(1)
|
|
const [itemsPerPage, setItemsPerPage] = useState(100) // 默认100条
|
|
const [totalItems, setTotalItems] = useState(0)
|
|
|
|
useEffect(() => {
|
|
fetchData()
|
|
}, [currentPage, itemsPerPage]) // 监听页码和每页数量的变化
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
setError(null)
|
|
setLoading(true)
|
|
|
|
// 计算偏移量
|
|
const offset = (currentPage - 1) * itemsPerPage
|
|
|
|
const response = await fetch(`/api/stats?type=edge_nodes&offset=${offset}&limit=${itemsPerPage}`)
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
|
|
|
const result = await response.json()
|
|
type ResultEdge = {
|
|
id: number
|
|
macaddr: string
|
|
city: string
|
|
public: string
|
|
isp: string
|
|
single: number | boolean
|
|
sole: number | boolean
|
|
arch: number
|
|
online: number
|
|
}
|
|
|
|
const validatedData = (result.data as ResultEdge[]).map((item) => ({
|
|
id: validateNumber(item.id),
|
|
macaddr: item.macaddr || '',
|
|
city: item.city || '',
|
|
public: item.public || '',
|
|
isp: item.isp || '',
|
|
single: item.single,
|
|
sole: item.sole,
|
|
arch: validateNumber(item.arch),
|
|
online: validateNumber(item.online)
|
|
}))
|
|
|
|
setData(validatedData)
|
|
setTotalItems(result.totalCount || 0)
|
|
} catch (error) {
|
|
console.error('Failed to fetch edge nodes:', error)
|
|
setError(error instanceof Error ? error.message : '获取边缘节点数据失败')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
// 多IP节点格式化
|
|
const formatMultiIP = (value: number | boolean): string => {
|
|
if (typeof value === 'number') {
|
|
switch (value) {
|
|
case 1: return '是'
|
|
case 0: return '否'
|
|
case -1: return '未知'
|
|
default: return `未知 (${value})`
|
|
}
|
|
}
|
|
return value ? '是' : '否'
|
|
}
|
|
|
|
// 独享IP节点格式化
|
|
const formatExclusiveIP = (value: number | boolean): string => {
|
|
if (typeof value === 'number') {
|
|
return value === 1 ? '是' : '否'
|
|
}
|
|
return value ? '是' : '否'
|
|
}
|
|
|
|
// 多IP节点颜色
|
|
const getMultiIPColor = (value: number | boolean): string => {
|
|
if (typeof value === 'number') {
|
|
switch (value) {
|
|
case 1: return 'bg-red-100 text-red-800'
|
|
case 0: return 'bg-green-100 text-green-800'
|
|
case -1: return 'bg-gray-100 text-gray-800'
|
|
default: return 'bg-gray-100 text-gray-800'
|
|
}
|
|
}
|
|
return value ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
|
|
}
|
|
|
|
// 独享IP节点颜色
|
|
const getExclusiveIPColor = (value: number | boolean): string => {
|
|
if (typeof value === 'number') {
|
|
return value === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
|
}
|
|
return value ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
|
}
|
|
|
|
const formatArchType = (arch: number): string => {
|
|
switch (arch) {
|
|
case 0: return '一代'
|
|
case 1: return '二代'
|
|
case 2: return 'AMD64'
|
|
case 3: return 'x86'
|
|
default: return `未知 (${arch})`
|
|
}
|
|
}
|
|
|
|
const getArchColor = (arch: number): string => {
|
|
switch (arch) {
|
|
case 0: return 'bg-blue-100 text-blue-800'
|
|
case 1: return 'bg-green-100 text-green-800'
|
|
case 2: return 'bg-purple-100 text-purple-800'
|
|
case 3: return 'bg-orange-100 text-orange-800'
|
|
default: return 'bg-gray-100 text-gray-800'
|
|
}
|
|
}
|
|
|
|
const formatOnlineTime = (seconds: number): string => {
|
|
if (seconds < 60) return `${seconds}秒`
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`
|
|
if (seconds < 86400) return `${Math.floor(seconds / 3600)}小时`
|
|
return `${Math.floor(seconds / 86400)}天`
|
|
}
|
|
|
|
// 处理页码变化
|
|
const handlePageChange = (page: number) => {
|
|
setCurrentPage(page)
|
|
}
|
|
|
|
// 处理每页显示数量变化
|
|
const handleSizeChange = (size: number) => {
|
|
setItemsPerPage(size)
|
|
setCurrentPage(1) // 重置到第一页
|
|
}
|
|
|
|
if (loading) return (
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">节点列表</h2>
|
|
<div className="text-center py-8">加载节点数据中...</div>
|
|
</div>
|
|
)
|
|
|
|
if (error) return (
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">节点列表</h2>
|
|
<div className="text-center py-8 text-red-600">{error}</div>
|
|
<button
|
|
onClick={() => fetchData()}
|
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mx-auto block"
|
|
>
|
|
重试
|
|
</button>
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<div className="flex bg-white flex-col shadow overflow-hidden rounded-lg p-6">
|
|
{data.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<div className="text-gray-400 text-4xl mb-4">📋</div>
|
|
<p className="text-gray-600">暂无节点数据</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className='flex gap-6 overflow-hidden'>
|
|
<div className="flex-3 w-full overflow-y-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-gray-50">
|
|
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">MAC地址</TableHead>
|
|
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">城市</TableHead>
|
|
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">公网IP</TableHead>
|
|
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">运营商</TableHead>
|
|
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">多IP节点</TableHead>
|
|
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">独享IP</TableHead>
|
|
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">设备类型</TableHead>
|
|
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">在线时长</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.map((item, index) => (
|
|
<TableRow key={item.id} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
|
<TableCell className="px-4 py-3 text-sm font-mono text-blue-600">{item.macaddr}</TableCell>
|
|
<TableCell className="px-4 py-3 text-sm text-gray-700">{item.city}</TableCell>
|
|
<TableCell className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</TableCell>
|
|
<TableCell className="px-4 py-3 text-sm text-gray-700">
|
|
<span className={`px-2 py-1 rounded-full text-xs ${
|
|
item.isp === '移动' ? 'bg-blue-100 text-blue-800' :
|
|
item.isp === '电信' ? 'bg-purple-100 text-purple-800' :
|
|
item.isp === '联通' ? 'bg-red-100 text-red-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{item.isp}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="px-4 py-3 text-sm text-center">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getMultiIPColor(item.single)}`}>
|
|
{formatMultiIP(item.single)}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="px-4 py-3 text-sm text-center">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getExclusiveIPColor(item.sole)}`}>
|
|
{formatExclusiveIP(item.sole)}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="px-4 py-3 text-sm text-center">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getArchColor(item.arch)}`}>
|
|
{formatArchType(item.arch)}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="px-4 py-3 text-sm text-gray-700">{formatOnlineTime(item.online)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 分页 */}
|
|
<Pagination
|
|
page={currentPage}
|
|
size={itemsPerPage}
|
|
total={totalItems}
|
|
onPageChange={handlePageChange}
|
|
onSizeChange={handleSizeChange}
|
|
className="mt-4"
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
} |