网关配置增加分页功能&更新分配装填布局
This commit is contained in:
@@ -54,17 +54,76 @@ async function getGatewayInfo() {
|
||||
async function getGatewayConfig(request: NextRequest) {
|
||||
try {
|
||||
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`
|
||||
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
|
||||
WHERE gateway.macaddr = ${macAddress};
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
// 定义类型接口
|
||||
interface GatewayRecord {
|
||||
edge: string | null;
|
||||
city: string | null;
|
||||
user: string | null;
|
||||
public: string | null;
|
||||
inner_ip: string | null;
|
||||
ischange: boolean | number | null;
|
||||
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) {
|
||||
console.error('Gateway config query error:', error)
|
||||
return NextResponse.json({ error: '查询网关配置失败' }, { status: 500 })
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
</div>
|
||||
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex flex-3 w-full">
|
||||
<div className="flex w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
@@ -159,17 +159,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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>
|
||||
)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { useEffect, useState, Suspense } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
import { Pagination } from '@/components/ui/pagination'
|
||||
|
||||
interface GatewayConfig {
|
||||
id: number
|
||||
city: string
|
||||
edge: string
|
||||
user: string
|
||||
@@ -14,79 +14,83 @@ interface GatewayConfig {
|
||||
isonline: number
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
data: GatewayConfig[]
|
||||
totalCount: number
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
function GatewayConfigContent() {
|
||||
const [data, setData] = useState<GatewayConfig[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [macAddress, setMacAddress] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
// 监听URL的mac参数变化:同步到输入框并触发查询
|
||||
// 分页状态
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [itemsPerPage, setItemsPerPage] = useState(100)
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
|
||||
// 判断是否为MAC地址查询(用于控制分页显示)
|
||||
const isMacQuery = !!macAddress
|
||||
|
||||
// 监听URL的mac参数变化
|
||||
useEffect(() => {
|
||||
const urlMac = searchParams.get('mac')
|
||||
if (urlMac) {
|
||||
setMacAddress(urlMac)
|
||||
fetchData(urlMac)
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
fetchData(urlMac, 1, itemsPerPage)
|
||||
} else {
|
||||
// 如果没有mac参数,显示所有网关配置
|
||||
setMacAddress('')
|
||||
fetchData('')
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
fetchData('', 1, itemsPerPage)
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const fetchData = async (mac: string) => {
|
||||
const fetchData = async (mac: string, page: number = 1, limit: number = itemsPerPage) => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
try {
|
||||
// 构建API URL - 如果有MAC地址则添加参数,否则获取全部
|
||||
const apiUrl = mac.trim()
|
||||
? `/api/stats?type=gateway_config&mac=${encodeURIComponent(mac)}`
|
||||
: `/api/stats?type=gateway_config`
|
||||
// 计算偏移量
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
const response = await fetch(apiUrl)
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || '查询失败')
|
||||
// 构建API URL
|
||||
let apiUrl = `/api/stats?type=gateway_config&offset=${offset}&limit=${limit}`
|
||||
if (mac.trim()) {
|
||||
apiUrl += `&mac=${encodeURIComponent(mac)}`
|
||||
}
|
||||
|
||||
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()) {
|
||||
setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
|
||||
} else {
|
||||
setError('未找到任何网关配置信息')
|
||||
}
|
||||
setData([])
|
||||
setTotalItems(0)
|
||||
return
|
||||
}
|
||||
|
||||
const validatedData = result.map((item: {
|
||||
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(result.data)
|
||||
|
||||
setData(validatedData)
|
||||
setTotalItems(result.totalCount || 0)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch gateway config:', error)
|
||||
console.error('获取网关配置失败:', error)
|
||||
setError(error instanceof Error ? error.message : '获取网关配置失败')
|
||||
setData([])
|
||||
setTotalItems(0)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -94,7 +98,21 @@ function GatewayConfigContent() {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
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 = '否') => {
|
||||
@@ -128,7 +146,7 @@ function GatewayConfigContent() {
|
||||
|
||||
{/* 查询表单 */}
|
||||
<form onSubmit={handleSubmit} className="mb-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={macAddress}
|
||||
@@ -154,17 +172,6 @@ function GatewayConfigContent() {
|
||||
</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>
|
||||
|
||||
{loading ? (
|
||||
@@ -174,80 +181,86 @@ function GatewayConfigContent() {
|
||||
</div>
|
||||
) : data.length > 0 ? (
|
||||
<>
|
||||
{/* 详细表格 */}
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex-3 w-full flex">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50 TableRow ">
|
||||
<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">内部账号</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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, index) => (
|
||||
<TableRow
|
||||
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">
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex-3 w-full flex">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<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">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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, index) => (
|
||||
<TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<TableCell>
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
|
||||
{item.city}
|
||||
</span></TableCell>
|
||||
<TableCell className="px-6 whitespace-nowrap text-sm text-gray-900">{item.user}</TableCell>
|
||||
<TableCell className="px-6 whitespace-nowrap">
|
||||
<div className="font-mono text-sm text-green-600">{item.public}</div>
|
||||
</TableCell>
|
||||
<TableCell className="px-6 whitespace-nowrap">
|
||||
<div className="font-mono text-sm text-purple-600">{item.inner_ip}</div>
|
||||
</TableCell>
|
||||
<TableCell className="px-6 whitespace-nowrap">
|
||||
<div className="font-mono text-sm text-green-600">{getStatusBadge(item.ischange, '正常', '更新')}</div>
|
||||
</TableCell>
|
||||
<TableCell className="px-6 whitespace-nowrap">
|
||||
<div className="font-mono text-sm text-purple-600">{getOnlineStatus(item.isonline)}</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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">{data.length}</div>
|
||||
<div className="text-sm text-blue-800">网关数量</div>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-mono text-sm text-blue-600 font-medium">{item.edge}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-mono text-sm text-green-600">{item.public}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-900">{item.user}</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-mono text-sm text-purple-600">{item.inner_ip}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getStatusBadge(item.ischange, '正常', '更新')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getOnlineStatus(item.isonline)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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 className="bg-green-50 p-4 rounded-lg border border-green-100">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{data.filter(item => item.isonline === 1).length}
|
||||
</div>
|
||||
<div className="text-sm text-green-800">在线网关</div>
|
||||
<div className="text-sm text-green-800">在用节点</div>
|
||||
</div>
|
||||
<div className="bg-orange-50 p-4 rounded-lg border border-orange-100">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{data.filter(item => item.ischange === 1).length}
|
||||
</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 className="text-sm text-orange-800">需要更新</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>
|
||||
)
|
||||
|
||||
@@ -213,12 +213,6 @@ useEffect(() => {
|
||||
</div>
|
||||
<div className="text-sm text-red-800">禁用网关</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>
|
||||
|
||||
Reference in New Issue
Block a user