264 lines
7.9 KiB
TypeScript
264 lines
7.9 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { Pagination } from '@/components/ui/pagination'
|
|
import { getEdgeNodes, type Edge } from '@/actions/stats'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { useForm } from 'react-hook-form'
|
|
import { z } from 'zod'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'
|
|
import { Input } from '@/components/ui/input'
|
|
import { cn } from '@/lib/utils'
|
|
import { Page } from '@/components/page'
|
|
import { DataTable } from '@/components/data-table'
|
|
|
|
// 定义表单验证规则
|
|
const filterSchema = z.object({
|
|
macaddr: z.string(),
|
|
public: z.string(),
|
|
city: z.string(),
|
|
isp: z.string(),
|
|
})
|
|
|
|
type FilterFormValues = z.infer<typeof filterSchema>
|
|
|
|
export default function Edge() {
|
|
const [data, setData] = useState<Edge[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// 分页状态
|
|
const [page, setPage] = useState(1)
|
|
const [size, setSize] = useState(100)
|
|
const [total, setTotal] = useState(0)
|
|
|
|
// 初始化表单
|
|
const form = useForm<FilterFormValues>({
|
|
resolver: zodResolver(filterSchema),
|
|
defaultValues: {
|
|
macaddr: '',
|
|
public: '',
|
|
city: '',
|
|
isp: '',
|
|
},
|
|
})
|
|
|
|
const fetchData = async (page: number, size: number) => {
|
|
const filters = form.getValues()
|
|
|
|
setLoading(true)
|
|
try {
|
|
const result = await getEdgeNodes(page, size, filters)
|
|
if (!result.success) {
|
|
throw new Error(result.error)
|
|
}
|
|
const data = result.data
|
|
console.log(data)
|
|
setData(data.items)
|
|
setTotal(data.total)
|
|
setPage(data.page)
|
|
setSize(data.size)
|
|
setError(null)
|
|
}
|
|
catch (error) {
|
|
setError('获取边缘节点数据失败' + (error instanceof Error ? `: ${error.message}` : ''))
|
|
}
|
|
finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const onSubmit = () => {
|
|
fetchData(page, size)
|
|
}
|
|
|
|
// 处理页码变化
|
|
const handlePageChange = (page: number) => {
|
|
setPage(page)
|
|
fetchData(page, size)
|
|
}
|
|
|
|
// 处理每页显示数量变化
|
|
const handleSizeChange = (size: number) => {
|
|
setPage(1)
|
|
setSize(size)
|
|
fetchData(1, size)
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchData(page, size)
|
|
}, [])
|
|
|
|
if (loading) return (
|
|
<div className="bg-white w-full shadow 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 w-full shadow 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(page, size)}
|
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors mx-auto block"
|
|
>
|
|
重试
|
|
</button>
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<Page className="gap-3">
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="macaddr"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>MAC地址</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="输入MAC地址" {...field} />
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
name="public"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>公网IP</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="输入公网IP" {...field} />
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
name="city"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>城市</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="输入城市名称" {...field} />
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
name="isp"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>运营商</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="输入运营商" {...field} />
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<Button type="submit" className="mt-5 py-2 bg-blue-600 hover:bg-blue-700">
|
|
查询
|
|
</Button>
|
|
<Button type="button" variant="outline" className="mt-5 py-2" onClick={() => form.reset()}>
|
|
重置
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
<DataTable
|
|
data={data}
|
|
columns={[
|
|
{
|
|
label: 'MAC地址',
|
|
props: 'macaddr',
|
|
},
|
|
{
|
|
label: '城市',
|
|
props: 'city',
|
|
},
|
|
{
|
|
label: '公网IP',
|
|
props: 'public',
|
|
},
|
|
{
|
|
label: '运营商',
|
|
render: (val) => {
|
|
const isp = val.isp as string
|
|
return (
|
|
<span className={cn('px-2 py-1 rounded-full text-xs', 'bg-gray-100 text-gray-800',
|
|
{ 移动: 'bg-blue-100 text-blue-800',
|
|
电信: 'bg-purple-100 text-purple-800',
|
|
联通: 'bg-red-100 text-red-800',
|
|
}[isp])}>
|
|
{isp}
|
|
</span>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
label: '多IP节点',
|
|
render: (val) => {
|
|
const single = val.single as number
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
single === 1 ? 'bg-red-100 text-red-800' : single === 0 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
|
|
{single === 1 ? '是' : single === 0 ? '否' : '未知'}
|
|
</span>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
label: '独享IP',
|
|
render: (val) => {
|
|
const sole = val.sole as number
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
sole === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
|
|
{sole === 1 ? '是' : '否'}
|
|
</span>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
label: '设备类型',
|
|
render: (val) => {
|
|
const arch = val.arch as number
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
arch === 0 ? 'bg-blue-100 text-blue-800'
|
|
: arch === 1 ? 'bg-green-100 text-green-800'
|
|
: arch === 2 ? 'bg-purple-100 text-purple-800'
|
|
: arch === 3 ? 'bg-orange-100 text-orange-800'
|
|
: 'bg-gray-100 text-gray-800'}`}>
|
|
{arch === 0 ? '一代' : arch === 1 ? '二代' : arch === 2 ? 'AMD64' : arch === 3 ? 'x86' : `未知 (${arch})`}
|
|
</span>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
label: '在线时长',
|
|
render: (val) => {
|
|
const seconds = val.online as number
|
|
return seconds < 60 ? `${seconds}秒`
|
|
: seconds < 3600 ? `${Math.floor(seconds / 60)}分钟`
|
|
: seconds < 86400 ? `${Math.floor(seconds / 3600)}小时`
|
|
: `${Math.floor(seconds / 86400)}天`
|
|
},
|
|
},
|
|
]}
|
|
/>
|
|
|
|
{/* 分页 */}
|
|
<Pagination
|
|
page={page}
|
|
size={size}
|
|
total={total}
|
|
onPageChange={handlePageChange}
|
|
onSizeChange={handleSizeChange}
|
|
/>
|
|
</Page>
|
|
)
|
|
}
|