Files
jh-monitor/src/app/(root)/edge/page.tsx
2025-10-16 18:11:29 +08:00

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>
)
}