网关配置增加分页功能&更新分配装填布局

This commit is contained in:
wmp
2025-09-22 18:40:41 +08:00
parent fd8fede301
commit 0288855002
4 changed files with 192 additions and 137 deletions

View File

@@ -54,17 +54,76 @@ async function getGatewayInfo() {
async function getGatewayConfig(request: NextRequest) { async function getGatewayConfig(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const macAddress = searchParams.get('mac') || '000C29DF1647' const macAddress = searchParams.get('mac') || ''
const offset = parseInt(searchParams.get('offset') || '0')
const limit = parseInt(searchParams.get('limit') || '100')
// 使用参数化查询防止SQL注入 // 定义类型接口
const result = await prisma.$queryRaw` interface GatewayRecord {
SELECT edge, city, user, public, inner_ip, ischange, isonline edge: string | null;
FROM gateway city: string | null;
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash user: string | null;
LEFT JOIN edge ON edge.macaddr = gateway.edge public: string | null;
WHERE gateway.macaddr = ${macAddress}; inner_ip: string | null;
` ischange: boolean | number | null;
return NextResponse.json(safeSerialize(result)) isonline: boolean | number | null;
}
// 获取总数
let totalCountQuery = ''
let totalCountParams: (string | number)[] = []
if (macAddress) {
totalCountQuery = `
SELECT COUNT(*) as total
FROM gateway
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
LEFT JOIN edge ON edge.macaddr = gateway.edge
WHERE gateway.macaddr = ?
`
totalCountParams = [macAddress]
} else {
totalCountQuery = `
SELECT COUNT(*) as total
FROM gateway
`
}
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
totalCountQuery,
...totalCountParams
)
const totalCount = Number(totalCountResult[0]?.total || 0)
// 获取分页数据
let query = `
select edge, city, user, public, inner_ip, ischange, isonline
from
gateway
left join cityhash
on cityhash.hash = gateway.cityhash
left join edge
on edge.macaddr = gateway.edge
`
let params: (string | number)[] = []
if (macAddress) {
query += ' WHERE gateway.macaddr = ?'
params = [macAddress]
} else {
query += ' LIMIT ? OFFSET ?'
params.push(limit, offset)
}
// 指定返回类型
const result = await prisma.$queryRawUnsafe<GatewayRecord[]>(query, ...params)
return NextResponse.json({
data: safeSerialize(result),
totalCount: totalCount,
currentPage: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(totalCount / limit)
})
} catch (error) { } catch (error) {
console.error('Gateway config query error:', error) console.error('Gateway config query error:', error)
return NextResponse.json({ error: '查询网关配置失败' }, { status: 500 }) return NextResponse.json({ error: '查询网关配置失败' }, { status: 500 })

View File

@@ -127,7 +127,7 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
</div> </div>
<div className='flex gap-6 overflow-hidden'> <div className='flex gap-6 overflow-hidden'>
<div className="flex flex-3 w-full"> <div className="flex w-full">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-gray-50"> <TableRow className="bg-gray-50">
@@ -159,17 +159,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
<div className="flex flex-1 flex-col gap-4 mb-6">
<div className="bg-blue-50 p-4 rounded-lg">
<div className="text-2xl font-bold text-blue-600">{formatNumber(data.length)}</div>
<div className="text-sm text-blue-800"></div>
</div>
<div className="bg-red-50 p-4 rounded-lg">
<div className="text-2xl font-bold text-red-600">{formatNumber(problematicCities.length)}</div>
<div className="text-sm text-red-800"></div>
</div>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -2,9 +2,9 @@
import { useEffect, useState, Suspense } from 'react' import { useEffect, useState, Suspense } from 'react'
import { useSearchParams } from 'next/navigation' import { useSearchParams } from 'next/navigation'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table' import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
import { Pagination } from '@/components/ui/pagination'
interface GatewayConfig { interface GatewayConfig {
id: number
city: string city: string
edge: string edge: string
user: string user: string
@@ -14,79 +14,83 @@ interface GatewayConfig {
isonline: number isonline: number
} }
interface ApiResponse {
data: GatewayConfig[]
totalCount: number
currentPage: number
totalPages: number
}
function GatewayConfigContent() { function GatewayConfigContent() {
const [data, setData] = useState<GatewayConfig[]>([]) const [data, setData] = useState<GatewayConfig[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [macAddress, setMacAddress] = useState('') const [macAddress, setMacAddress] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const searchParams = useSearchParams() const searchParams = useSearchParams()
// 分页状态
const [currentPage, setCurrentPage] = useState(1)
const [itemsPerPage, setItemsPerPage] = useState(100)
const [totalItems, setTotalItems] = useState(0)
// 监听URL的mac参数变化同步到输入框并触发查询 // 判断是否为MAC地址查询用于控制分页显示
const isMacQuery = !!macAddress
// 监听URL的mac参数变化
useEffect(() => { useEffect(() => {
const urlMac = searchParams.get('mac') const urlMac = searchParams.get('mac')
if (urlMac) { if (urlMac) {
setMacAddress(urlMac) setMacAddress(urlMac)
fetchData(urlMac) setCurrentPage(1) // 重置到第一页
fetchData(urlMac, 1, itemsPerPage)
} else { } else {
// 如果没有mac参数显示所有网关配置
setMacAddress('') setMacAddress('')
fetchData('') setCurrentPage(1) // 重置到第一页
fetchData('', 1, itemsPerPage)
} }
}, [searchParams]) }, [searchParams])
const fetchData = async (mac: string) => { const fetchData = async (mac: string, page: number = 1, limit: number = itemsPerPage) => {
setLoading(true) setLoading(true)
setError('') setError('')
setSuccess('')
try { try {
// 构建API URL - 如果有MAC地址则添加参数否则获取全部 // 计算偏移量
const apiUrl = mac.trim() const offset = (page - 1) * limit
? `/api/stats?type=gateway_config&mac=${encodeURIComponent(mac)}`
: `/api/stats?type=gateway_config`
const response = await fetch(apiUrl) // 构建API URL
const result = await response.json() let apiUrl = `/api/stats?type=gateway_config&offset=${offset}&limit=${limit}`
if (mac.trim()) {
if (!response.ok) { apiUrl += `&mac=${encodeURIComponent(mac)}`
throw new Error(result.error || '查询失败')
} }
const response = await fetch(apiUrl)
if (!response.ok) {
throw new Error(`HTTP错误! 状态: ${response.status}`)
}
const result: ApiResponse = await response.json()
// 检查返回的数据是否有效 // 检查返回的数据是否有效
if (!result || result.length === 0) { if (!result.data || result.data.length === 0) {
if (mac.trim()) { if (mac.trim()) {
setError(`未找到MAC地址为 ${mac} 的网关配置信息`) setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
} else { } else {
setError('未找到任何网关配置信息') setError('未找到任何网关配置信息')
} }
setData([]) setData([])
setTotalItems(0)
return return
} }
const validatedData = result.map((item: { setData(result.data)
city: string
edge: string
user: string
public: string
inner_ip: string
ischange: number
isonline: number
}) => ({
city: item.city,
edge: item.edge,
user: item.user,
public: item.public,
inner_ip: item.inner_ip,
ischange: item.ischange,
isonline: item.isonline,
}))
setData(validatedData) setTotalItems(result.totalCount || 0)
} catch (error) { } catch (error) {
console.error('Failed to fetch gateway config:', error) console.error('获取网关配置失败:', error)
setError(error instanceof Error ? error.message : '获取网关配置失败') setError(error instanceof Error ? error.message : '获取网关配置失败')
setData([]) setData([])
setTotalItems(0)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -94,7 +98,21 @@ function GatewayConfigContent() {
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
fetchData(macAddress) setCurrentPage(1) // 重置到第一页
fetchData(macAddress, 1, itemsPerPage)
}
// 处理页码变化
const handlePageChange = (page: number) => {
setCurrentPage(page)
fetchData(macAddress, page, itemsPerPage)
}
// 处理每页显示数量变化
const handleSizeChange = (size: number) => {
setItemsPerPage(size)
setCurrentPage(1)
fetchData(macAddress, 1, size)
} }
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => { const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
@@ -128,7 +146,7 @@ function GatewayConfigContent() {
{/* 查询表单 */} {/* 查询表单 */}
<form onSubmit={handleSubmit} className="mb-6"> <form onSubmit={handleSubmit} className="mb-6">
<div className="flex items-center"> <div className="flex items-center gap-2">
<input <input
type="text" type="text"
value={macAddress} value={macAddress}
@@ -154,17 +172,6 @@ function GatewayConfigContent() {
</div> </div>
</div> </div>
)} )}
{success && !error && (
<div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-md">
<div className="flex items-center text-green-800">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
{success}
</div>
</div>
)}
</form> </form>
{loading ? ( {loading ? (
@@ -174,80 +181,86 @@ function GatewayConfigContent() {
</div> </div>
) : data.length > 0 ? ( ) : data.length > 0 ? (
<> <>
{/* 详细表格 */} <div className='flex gap-6 overflow-hidden'>
<div className='flex gap-6 overflow-hidden'> <div className="flex-3 w-full flex">
<div className="flex-3 w-full flex"> <Table>
<Table> <TableHeader>
<TableHeader> <TableRow className="bg-gray-50">
<TableRow className="bg-gray-50 TableRow "> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">线</TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody>
<TableBody> {data.map((item, index) => (
{data.map((item, index) => ( <TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<TableRow <TableCell>
key={index}
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50 '}
>
<TableCell className="hover:bg-gray-50 transition-colors">
<div className="font-mono text-sm text-blue-600 font-medium">{item.edge}</div>
</TableCell>
<TableCell className="px-6 whitespace-nowrap">
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs"> <span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
{item.city} {item.city}
</span></TableCell> </span>
<TableCell className="px-6 whitespace-nowrap text-sm text-gray-900">{item.user}</TableCell> </TableCell>
<TableCell className="px-6 whitespace-nowrap"> <TableCell>
<div className="font-mono text-sm text-green-600">{item.public}</div> <div className="font-mono text-sm text-blue-600 font-medium">{item.edge}</div>
</TableCell> </TableCell>
<TableCell className="px-6 whitespace-nowrap"> <TableCell>
<div className="font-mono text-sm text-purple-600">{item.inner_ip}</div> <div className="font-mono text-sm text-green-600">{item.public}</div>
</TableCell> </TableCell>
<TableCell className="px-6 whitespace-nowrap"> <TableCell className="text-sm text-gray-900">{item.user}</TableCell>
<div className="font-mono text-sm text-green-600">{getStatusBadge(item.ischange, '正常', '更新')}</div> <TableCell>
</TableCell> <div className="font-mono text-sm text-purple-600">{item.inner_ip}</div>
<TableCell className="px-6 whitespace-nowrap"> </TableCell>
<div className="font-mono text-sm text-purple-600">{getOnlineStatus(item.isonline)}</div> <TableCell>
</TableCell> {getStatusBadge(item.ischange, '正常', '更新')}
</TableRow> </TableCell>
))} <TableCell>
</TableBody> {getOnlineStatus(item.isonline)}
</Table> </TableCell>
</div> </TableRow>
<div className="flex flex-1 flex-col gap-4 mb-6"> ))}
<div className="bg-blue-50 p-4 rounded-lg border border-blue-100"> </TableBody>
<div className="text-2xl font-bold text-blue-600">{data.length}</div> </Table>
<div className="text-sm text-blue-800"></div> </div>
<div className="flex flex-1 flex-col gap-4 mb-6">
<div className="bg-blue-50 p-4 rounded-lg border border-blue-100">
<div className="text-2xl font-bold text-blue-600">{totalItems}</div>
<div className="text-sm text-blue-800"></div>
</div> </div>
<div className="bg-green-50 p-4 rounded-lg border border-green-100"> <div className="bg-green-50 p-4 rounded-lg border border-green-100">
<div className="text-2xl font-bold text-green-600"> <div className="text-2xl font-bold text-green-600">
{data.filter(item => item.isonline === 1).length} {data.filter(item => item.isonline === 1).length}
</div> </div>
<div className="text-sm text-green-800">线</div> <div className="text-sm text-green-800"></div>
</div> </div>
<div className="bg-orange-50 p-4 rounded-lg border border-orange-100"> <div className="bg-orange-50 p-4 rounded-lg border border-orange-100">
<div className="text-2xl font-bold text-orange-600"> <div className="text-2xl font-bold text-orange-600">
{data.filter(item => item.ischange === 1).length} {data.filter(item => item.ischange === 1).length}
</div> </div>
<div className="text-sm text-orange-800"></div> <div className="text-sm text-orange-800"></div>
</div>
<div className="bg-purple-50 p-4 rounded-lg border border-purple-100">
<div className="text-2xl font-bold text-purple-600">
{new Set(data.map(item => item.city)).size}
</div>
<div className="text-sm text-purple-800"></div>
</div> </div>
</div>
</div> </div>
</div>
{/* 分页组件 - 仅在非MAC查询时显示 */}
{!isMacQuery && (
<Pagination
page={currentPage}
size={itemsPerPage}
total={totalItems}
onPageChange={handlePageChange}
onSizeChange={handleSizeChange}
className="mt-4"
/>
)}
</> </>
) : ( ) : (
<></> <div className="text-center py-12">
<div className="text-gray-400 text-4xl mb-4">📋</div>
<p className="text-gray-600"></p>
</div>
)} )}
</div> </div>
) )

View File

@@ -213,12 +213,6 @@ useEffect(() => {
</div> </div>
<div className="text-sm text-red-800"></div> <div className="text-sm text-red-800"></div>
</div> </div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="text-2xl font-bold text-purple-600">
{new Set(data.map(item => item.setid)).size}
</div>
<div className="text-sm text-purple-800"></div>
</div>
</div> </div>
</div> </div>
</div> </div>