Files
jh-monitor/src/app/(root)/gatewayConfig/page.tsx
2025-10-15 11:43:47 +08:00

356 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useState, Suspense, useCallback } from 'react'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
import { getGatewayInfo, getGatewayConfig, type GatewayConfig, type GatewayInfo } from '@/actions/stats'
import { Pagination } from '@/components/ui/pagination'
import { toast } from 'sonner'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import { Form, FormField, FormItem, FormLabel } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { SearchIcon } from 'lucide-react'
import { Page } from '@/components/page'
function GatewayConfigContent() {
const [data, setData] = useState<GatewayConfig[]>([])
const [loading, setLoading] = useState(false)
const [infoData, setInfoData] = useState<GatewayInfo[]>([])
// 分页状态
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
// 定义表单验证规则
const filterSchema = z.object({
macaddr: z.string().optional(),
public: z.string().optional(),
city: z.string().optional(),
inner_ip: z.string().optional(),
user: z.string().optional(),
})
type FilterFormValues = z.infer<typeof filterSchema>
// 初始化表单
const form = useForm<FilterFormValues>({
resolver: zodResolver(filterSchema),
defaultValues: {
macaddr: '',
public: '',
city: '',
inner_ip: '',
user: '',
},
})
// 初始化调用
useEffect(() => {
const initData = async () => {
setLoading(true)
try {
// 获取网关基本信息
const infoResult = await getGatewayInfo()
if (!infoResult.success) {
throw new Error(infoResult.error || '查询网关信息失败')
}
setInfoData(infoResult.data)
}
catch (error) {
toast.error((error as Error).message || '获取数据失败')
}
finally {
setLoading(false)
}
}
initData()
fetchData({}, 1)
}, [])
const { handleSubmit: formHandleSubmit } = form
// 网关配置数据查询函数(用于表单查询)
const fetchData = async (filters: {
mac?: string
public?: string
city?: string
user?: string
inner_ip?: string
}, page: number = 1) => {
setLoading(true)
try {
const result = await getGatewayConfig(page, filters)
if (!result.success) {
throw new Error(result.error || '查询网关配置失败')
}
const shrink = ['黔东南', '延边']
result.data.items.forEach((item) => {
shrink.forEach((s) => {
if (item.city?.startsWith(s)) {
item.city = s
}
})
})
setData(result.data.items)
setTotal(result.data.total)
}
catch (error) {
toast.error((error as Error).message || '获取网关配置失败')
}
finally {
setLoading(false)
}
}
const onSubmit = (data: FilterFormValues) => {
setPage(1)
const filters = {
mac: data.macaddr || '',
public: data.public || '',
city: data.city || '',
user: data.user || '',
inner_ip: data.inner_ip || '',
}
fetchData(filters, 1)
}
// 处理页码变化
const handlePageChange = (page: number) => {
setPage(page)
const formValues = form.getValues()
const filters = {
mac: formValues.macaddr || undefined,
public: formValues.public || undefined,
city: formValues.city || undefined,
user: formValues.user || undefined,
inner_ip: formValues.inner_ip || undefined,
}
fetchData(filters, page)
}
// 处理每页显示数量变化
const handleSizeChange = (size: number) => {
setPage(1)
const formValues = form.getValues()
const filters = {
mac: formValues.macaddr || undefined,
public: formValues.public || undefined,
city: formValues.city || undefined,
user: formValues.user || undefined,
inner_ip: formValues.inner_ip || undefined,
}
fetchData(filters, 1)
}
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
// 0是正常1是更新正常绿+ 更新(红)
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${value === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{value === 0 ? trueText : falseText}
</span>
)
}
const getOnlineStatus = (isonline: number) => {
// 0是空闲1是在用在用+ 空闲(绿)
return (
<div className="flex items-center">
<div className={`${isonline === 0 ? 'bg-green-500' : 'bg-red-500'}`} />
{getStatusBadge(isonline, '空闲', '在用')}
</div>
)
}
// 当前选中的mac
const [selectedMac, setSelectedMac] = useState<string>('')
const handleMacClick = useCallback(async (macaddr: string) => {
setSelectedMac(macaddr)
await fetchData({ mac: macaddr }, 1)
}, [])
return (
<Page className="flex flex-row gap-6">
{/* 查询表单 */}
<div className="flex-1 flex flex-col gap-2">
<Form {...form}>
<form onSubmit={formHandleSubmit(onSubmit)}>
<FormField
name="macaddr"
render={({ field }) => (
<FormItem className="flex-1 relative">
<div className="relative">
<Input
placeholder="搜索MAC地址..."
{...field}
className="w-full pr-10"
/>
<SearchIcon
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 cursor-pointer"
onClick={(e) => {
e.preventDefault()
onSubmit(form.getValues())
}}
/>
</div>
</FormItem>
)}
/>
</form>
</Form>
<div className="flex gap-4">
<div className="flex items-center">
<div className="w-3 h-3 bg-green-500 rounded-full mr-2"></div>
<span className="text-sm font-medium">线 {infoData.filter(item => item.enable === 1).length}</span>
</div>
<div className="flex items-center">
<div className="w-3 h-3 bg-red-500 rounded-full mr-2"></div>
<span className="text-sm font-medium">线 {infoData.filter(item => item.enable === 0).length}</span>
</div>
</div>
<div className="overflow-auto border-t flex flex-col">
{infoData.map((item, index) => (
<div
key={index}
className={cn('p-4 flex-col ', index !== infoData.length - 1 ? 'border-b' : '')}>
<div className="flex flex-col">
<div className="flex items-center justify-between mb-2">
<div
className={cn('font-medium cursor-pointer',
selectedMac === item.macaddr ? 'text-blue-700' : 'text-gray-900')}
onClick={() => handleMacClick(item.macaddr)}
>
{item.macaddr}
</div>
<div className="flex items-center">
<div
className={cn(
'w-2 h-2 rounded-full mr-2',
item.enable === 1 ? 'bg-green-500' : 'bg-red-500',
)}
/>
<span className={cn(
'text-xs font-medium',
item.enable === 1 ? 'text-green-700' : 'text-red-700',
)}>
{item.enable === 1 ? '在线' : '离线'}
</span>
</div>
</div>
</div>
<div className="text-sm text-gray-600 mb-3">
{item.inner_ip || '未配置IP'}
</div>
<div className="flex gap-2 space-y-1 text-xs text-gray-500">
<div>: {item.setid || 'N/A'}</div>
</div>
</div>
))}
</div>
</div>
<div className="flex-3 overflow-hidden flex flex-col gap-3">
<Form {...form}>
<form className="flex gap-4" onSubmit={formHandleSubmit(onSubmit)}>
<FormField
name="public"
render={({ field }) => (
<FormItem>
<FormLabel>IP地址</FormLabel>
<Input placeholder="输入IP地址" {...field} />
</FormItem>
)}
/>
<FormField
name="user"
render={({ field }) => (
<FormItem>
<FormLabel>线</FormLabel>
<Input placeholder="输入线路" {...field} />
</FormItem>
)}
/>
<FormField
name="inner_ip"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Input placeholder="输入端口" {...field} />
</FormItem>
)}
/>
<FormField
name="city"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Input placeholder="输入城市" {...field} />
</FormItem>
)}
/>
<Button type="submit" className="ml-2 mt-6 px-4 py-2 bg-blue-600 hover:bg-blue-700">
</Button>
<Button type="button" variant="outline" className="ml-2 mt-6 px-4 py-2" onClick={() => form.reset()}>
</Button>
</form>
</Form>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>线</TableHead>
<TableHead></TableHead>
<TableHead>MAC</TableHead>
<TableHead>IP</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((item, index) => (
<TableRow key={index}>
<TableCell>{item.inner_ip}</TableCell>
<TableCell>{item.user}</TableCell>
<TableCell>{item.city}</TableCell>
<TableCell>{item.edge}</TableCell>
<TableCell>{item.public}</TableCell>
<TableCell>
{getStatusBadge(item.ischange, '正常', '更新')}
</TableCell>
<TableCell>
{getOnlineStatus(item.isonline)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* 分页组件 */}
<Pagination
total={total}
page={page}
size={250}
sizeOptions={[250]}
onPageChange={handlePageChange}
onSizeChange={handleSizeChange}
/>
</div>
</Page>
)
}
export default function GatewayConfig() {
return (
<Suspense fallback={(
<div className="bg-white shadow p-6">
<div className="text-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-gray-600">...</p>
</div>
</div>
)}>
<GatewayConfigContent />
</Suspense>
)
}