更新布局和微调整页面样式
This commit is contained in:
@@ -153,9 +153,10 @@ async function getAllocationStatus() {
|
||||
|
||||
// 获取节点信息
|
||||
async function getEdgeNodes(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const threshold = searchParams.get('threshold') || '20'
|
||||
const threshold = searchParams.get('threshold') || '0'
|
||||
const limit = searchParams.get('limit') || '100'
|
||||
|
||||
// 使用参数化查询防止SQL注入
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react'
|
||||
import { formatNumber, validateNumber } from '@/lib/formatters'
|
||||
import LoadingCard from '@/components/ui/loadingCard'
|
||||
import ErrorCard from '@/components/ui/errorCard'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
|
||||
interface AllocationStatus {
|
||||
city: string
|
||||
@@ -66,6 +67,11 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
return filterDate.toISOString().slice(0, 19).replace('T', ' ')
|
||||
}, [timeFilter, customTime])
|
||||
|
||||
// 计算超额量
|
||||
const calculateOverage = (assigned: number, count: number) => {
|
||||
const overage = assigned - count;
|
||||
return Math.max(0, overage);
|
||||
}
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
@@ -86,7 +92,9 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
assigned: validateNumber(item.assigned),
|
||||
}))
|
||||
|
||||
setData(validatedData)
|
||||
const sortedData = validatedData.sort((a, b) => b.count - a.count)
|
||||
|
||||
setData(sortedData)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch allocation status:', error)
|
||||
setError(error instanceof Error ? error.message : 'Unknown error')
|
||||
@@ -102,10 +110,10 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
if (loading) return <LoadingCard title="节点分配状态" />
|
||||
if (error) return <ErrorCard title="节点分配状态" error={error} onRetry={fetchData} />
|
||||
|
||||
const problematicCities = data.filter(item => item.count < item.count)
|
||||
const problematicCities = data.filter(item => item.assigned > item.count)
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 overflow-hidden ">
|
||||
<h2 className="text-lg font-semibold mb-4">节点分配状态</h2>
|
||||
|
||||
{/* 时间筛选器 */}
|
||||
@@ -140,45 +148,54 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
onClick={fetchData}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||
>
|
||||
刷新
|
||||
查询
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex flex-3 w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="px-4 py-2 text-left">城市</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left">可用IP量</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left">分配IP量</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left">超额量</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, index) => {
|
||||
const overage = calculateOverage(item.assigned, item.count)
|
||||
return (
|
||||
<TableRow
|
||||
key={index}
|
||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
||||
>
|
||||
<TableCell className="px-4 py-2">{item.city}</TableCell>
|
||||
<TableCell className="px-4 py-2">{formatNumber(item.count)}</TableCell>
|
||||
<TableCell className="px-4 py-2">{formatNumber(item.assigned)}</TableCell>
|
||||
<TableCell className="px-4 py-2">
|
||||
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
|
||||
{formatNumber(overage)}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<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-orange-50 p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-orange-600">{formatNumber(problematicCities.length)}</div>
|
||||
<div className="text-sm text-orange-800">需关注城市</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>
|
||||
|
||||
{detailed && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full table-auto">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-4 py-2 text-left">城市</th>
|
||||
<th className="px-4 py-2 text-left">可用IP量</th>
|
||||
<th className="px-4 py-2 text-left">分配IP量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, index) => {
|
||||
return (
|
||||
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-4 py-2">{item.city}</td>
|
||||
<td className="px-4 py-2">{formatNumber(item.count)}</td>
|
||||
<td className="px-4 py-2">{formatNumber(item.assigned)}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
|
||||
interface CityNode {
|
||||
city: string
|
||||
@@ -32,7 +33,7 @@ export default function CityNodeStats() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-6">
|
||||
<div className="bg-white rounded-lg p-6 overflow-hidden">
|
||||
<h2 className="text-lg font-semibold mb-4">城市节点数量分布</h2>
|
||||
<div className="text-gray-600">加载中...</div>
|
||||
</div>
|
||||
@@ -40,7 +41,7 @@ export default function CityNodeStats() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-6">
|
||||
<div className="flex flex-col bg-white rounded-lg p-6 overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold">城市节点数量分布</h2>
|
||||
<span className="text-sm text-gray-500">
|
||||
@@ -48,35 +49,37 @@ export default function CityNodeStats() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600">城市</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600">节点数量</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600">Hash</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600">标签</th>
|
||||
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600">轮换顺位</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div className="flex overflow-hidden ">
|
||||
<div className='flex w-full'>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">城市</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">节点数量</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">Hash</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">标签</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">轮换顺位</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, index) => (
|
||||
<tr key={index} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm font-medium">{item.city}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<span className="font-semibold text-gray-700">{item.count}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500 font-mono">{item.hash}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<TableRow
|
||||
key={index}
|
||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
||||
>
|
||||
<TableCell className="px-4 py-2">{item.city}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.count}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.hash}</TableCell>
|
||||
<TableCell className="px-4 py-2">
|
||||
<span className="bg-gray-100 px-2 py-1 rounded text-gray-700">
|
||||
{item.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm font-semibold">{item.offset}</td>
|
||||
</tr>
|
||||
{item.label}</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.offset}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,14 +2,8 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { validateNumber } from '@/lib/formatters'
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination"
|
||||
import { Pagination } from '@/components/ui/pagination'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
|
||||
interface Edge {
|
||||
id: number
|
||||
@@ -27,8 +21,8 @@ export default function Edge() {
|
||||
const [data, setData] = useState<Edge[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [idThreshold, setIdThreshold] = useState(20)
|
||||
const [limit, setLimit] = useState(100)
|
||||
const [idThreshold,] = useState(0)
|
||||
const [limit,] = useState(100)
|
||||
|
||||
// 分页状态
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
@@ -43,7 +37,6 @@ export default function Edge() {
|
||||
try {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
|
||||
const response = await fetch(`/api/stats?type=edge_nodes&threshold=${threshold}&limit=${resultLimit}`)
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
|
||||
@@ -84,11 +77,6 @@ export default function Edge() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
fetchData(idThreshold, limit)
|
||||
}
|
||||
|
||||
const formatBoolean = (value: boolean | number): string => {
|
||||
return value ? '是' : '否'
|
||||
}
|
||||
@@ -104,38 +92,16 @@ export default function Edge() {
|
||||
const indexOfLastItem = currentPage * itemsPerPage
|
||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage
|
||||
const currentItems = data.slice(indexOfFirstItem, indexOfLastItem)
|
||||
const totalPages = Math.ceil(totalItems / itemsPerPage)
|
||||
|
||||
// 生成页码按钮
|
||||
const renderPageNumbers = () => {
|
||||
const pageNumbers = []
|
||||
const maxVisiblePages = 5
|
||||
|
||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2))
|
||||
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1)
|
||||
|
||||
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||
startPage = Math.max(1, endPage - maxVisiblePages + 1)
|
||||
// 处理页码变化
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pageNumbers.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
isActive={currentPage === i}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(i)
|
||||
}}
|
||||
>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
|
||||
return pageNumbers
|
||||
// 处理每页显示数量变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
setItemsPerPage(size)
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
@@ -160,57 +126,6 @@ export default function Edge() {
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800">节点列表</h2>
|
||||
<button
|
||||
onClick={() => fetchData()}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
刷新数据
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 查询表单 */}
|
||||
<form onSubmit={handleSubmit} className="bg-gray-50 p-4 rounded-lg mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label htmlFor="threshold" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
ID阈值 (ID大于此值)
|
||||
</label>
|
||||
<input
|
||||
id="threshold"
|
||||
type="number"
|
||||
value={idThreshold}
|
||||
onChange={(e) => setIdThreshold(Number(e.target.value))}
|
||||
min="0"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="limit" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
返回结果数量
|
||||
</label>
|
||||
<input
|
||||
id="limit"
|
||||
type="number"
|
||||
value={limit}
|
||||
onChange={(e) => setLimit(Number(e.target.value))}
|
||||
min="1"
|
||||
max="1000"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2 bg-green-600 text-white font-medium rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
||||
>
|
||||
查询
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{data.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-4xl mb-4">📋</div>
|
||||
@@ -218,60 +133,30 @@ export default function Edge() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-blue-50 p-4 rounded-lg mb-6">
|
||||
<p className="text-blue-800">
|
||||
共找到 <span className="font-bold">{totalItems}</span> 个节点
|
||||
{idThreshold > 0 && ` (ID大于${idThreshold})`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 每页显示数量选择器 */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-gray-700">每页显示</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => {
|
||||
setItemsPerPage(Number(e.target.value))
|
||||
setCurrentPage(1)
|
||||
}}
|
||||
className="border border-gray-300 rounded-md px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span className="text-sm text-gray-700">条</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<table className="min-w-full table-auto border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">ID</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">MAC地址</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">城市</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">公网IP</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">运营商</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">多IP节点</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">独享IP</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">设备类型</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">在线时长</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex-3 w-full overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">ID</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">MAC地址</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">城市</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">公网IP</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">运营商</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">多IP节点</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">独享IP</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">设备类型</TableHead>
|
||||
<TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">在线时长</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{currentItems.map((item, index) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={`hover:bg-gray-50 transition-colors ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">{item.id}</td>
|
||||
<td className="px-4 py-3 text-sm font-mono text-blue-600">{item.macaddr}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700">{item.city}</td>
|
||||
<td className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
<TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<TableCell className="px-4 py-3 text-sm text-gray-900">{item.id}</TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm font-mono text-blue-600">{item.macaddr}</TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm text-gray-700">{item.city}</TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm text-gray-700">
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${
|
||||
item.isp === '移动' ? 'bg-blue-100 text-blue-800' :
|
||||
item.isp === '电信' ? 'bg-purple-100 text-purple-800' :
|
||||
@@ -279,74 +164,39 @@ export default function Edge() {
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{item.isp}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
</span></TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm text-center">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
item.single ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{formatBoolean(item.single)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
</span></TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm text-center">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
item.sole ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{formatBoolean(item.sole)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
</span></TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm text-center">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
{item.arch}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
{formatOnlineTime(item.online)}
|
||||
</td>
|
||||
</tr>
|
||||
</span></TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm text-gray-700">{formatOnlineTime(item.online)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页控件 */}
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
显示 {indexOfFirstItem + 1} 到 {Math.min(indexOfLastItem, totalItems)} 条,共 {totalItems} 条记录
|
||||
</div>
|
||||
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
if (currentPage > 1) setCurrentPage(currentPage - 1)
|
||||
}}
|
||||
className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
|
||||
{/* 使用 Pagination 组件 */}
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
size={itemsPerPage}
|
||||
total={totalItems}
|
||||
onPageChange={handlePageChange}
|
||||
onSizeChange={handleSizeChange}
|
||||
className="mt-4"
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{renderPageNumbers()}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
if (currentPage < totalPages) setCurrentPage(currentPage + 1)
|
||||
}}
|
||||
className={currentPage === totalPages ? "pointer-events-none opacity-50" : ""}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
|
||||
<div className="text-sm text-gray-600">
|
||||
更新时间: {new Date().toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import { useEffect, useState, Suspense } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
|
||||
interface GatewayConfig {
|
||||
id: number
|
||||
@@ -28,25 +29,24 @@ function GatewayConfigContent() {
|
||||
setMacAddress(urlMac)
|
||||
fetchData(urlMac)
|
||||
} else {
|
||||
// 如果没有mac参数,显示空状态或默认查询
|
||||
setData([])
|
||||
setSuccess('请输入MAC地址查询网关配置信息')
|
||||
// 如果没有mac参数,显示所有网关配置
|
||||
setMacAddress('')
|
||||
fetchData('')
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const fetchData = async (mac: string) => {
|
||||
if (!mac.trim()) {
|
||||
setError('请输入MAC地址')
|
||||
setSuccess('')
|
||||
setData([])
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setSuccess('')
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/stats?type=gateway_config&mac=${encodeURIComponent(mac)}`)
|
||||
// 构建API URL - 如果有MAC地址则添加参数,否则获取全部
|
||||
const apiUrl = mac.trim()
|
||||
? `/api/stats?type=gateway_config&mac=${encodeURIComponent(mac)}`
|
||||
: `/api/stats?type=gateway_config`
|
||||
|
||||
const response = await fetch(apiUrl)
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -55,7 +55,11 @@ function GatewayConfigContent() {
|
||||
|
||||
// 检查返回的数据是否有效
|
||||
if (!result || result.length === 0) {
|
||||
if (mac.trim()) {
|
||||
setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
|
||||
} else {
|
||||
setError('未找到任何网关配置信息')
|
||||
}
|
||||
setData([])
|
||||
return
|
||||
}
|
||||
@@ -79,7 +83,6 @@ function GatewayConfigContent() {
|
||||
}))
|
||||
|
||||
setData(validatedData)
|
||||
setSuccess(`成功查询到 ${validatedData.length} 条网关配置信息`)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch gateway config:', error)
|
||||
setError(error instanceof Error ? error.message : '获取网关配置失败')
|
||||
@@ -91,19 +94,17 @@ function GatewayConfigContent() {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (macAddress.trim()) {
|
||||
fetchData(macAddress)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
value === 1
|
||||
value === 0
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{value === 1 ? trueText : falseText}
|
||||
{value === 0 ? trueText : falseText}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -120,7 +121,7 @@ function GatewayConfigContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 overflow-hidden">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-800">网关配置状态</h2>
|
||||
@@ -138,7 +139,7 @@ function GatewayConfigContent() {
|
||||
type="text"
|
||||
value={macAddress}
|
||||
onChange={(e) => setMacAddress(e.target.value)}
|
||||
placeholder="请输入MAC地址"
|
||||
placeholder="输入MAC地址查询"
|
||||
className="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
@@ -179,8 +180,53 @@ function GatewayConfigContent() {
|
||||
</div>
|
||||
) : data.length > 0 ? (
|
||||
<>
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
{/* 详细表格 */}
|
||||
<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">
|
||||
<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>
|
||||
@@ -204,78 +250,10 @@ function GatewayConfigContent() {
|
||||
<div className="text-sm text-purple-800">覆盖城市</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细表格 */}
|
||||
<div className="overflow-hidden rounded-lg shadow-sm border border-gray-200">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">城市</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">内部账号</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">内网入口</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">配置更新</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">在线状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{data.map((item, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50 transition-colors">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="font-mono text-sm text-blue-600 font-medium">
|
||||
{item.edge}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
|
||||
{item.city}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{item.user}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="font-mono text-sm text-green-600">
|
||||
{item.public}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="font-mono text-sm text-purple-600">
|
||||
{item.inner_ip}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getStatusBadge(item.ischange, '已更新', '未更新')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
{getOnlineStatus(item.isonline)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 分页信息 */}
|
||||
<div className="mt-4 flex justify-between items-center text-sm text-gray-600">
|
||||
<span>显示 1 到 {data.length} 条,共 {data.length} 条记录</span>
|
||||
<button
|
||||
onClick={() => fetchData(macAddress)}
|
||||
className="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors"
|
||||
>
|
||||
刷新数据
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-4xl mb-4">🔍</div>
|
||||
<p className="text-gray-600">暂无数据,请输入MAC地址查询网关配置信息</p>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
您可以通过上方的搜索框查询特定MAC地址的网关配置
|
||||
</p>
|
||||
</div>
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
import { Form, FormField } from '@/components/ui/form'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { z } from 'zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
|
||||
interface GatewayInfo {
|
||||
macaddr: string
|
||||
@@ -10,29 +16,74 @@ interface GatewayInfo {
|
||||
enable: number
|
||||
}
|
||||
|
||||
const filterSchema = z.object({
|
||||
status: z.string(),
|
||||
})
|
||||
|
||||
type FilterSchema = z.infer<typeof filterSchema>
|
||||
|
||||
// IP地址排序函数
|
||||
const sortByIpAddress = (a: string, b: string): number => {
|
||||
const ipToNumber = (ip: string): number => {
|
||||
const parts = ip.split('.').map(part => parseInt(part, 10));
|
||||
return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
|
||||
};
|
||||
|
||||
return ipToNumber(a) - ipToNumber(b);
|
||||
}
|
||||
|
||||
export default function Gatewayinfo() {
|
||||
const [data, setData] = useState<GatewayInfo[]>([])
|
||||
const [filteredData, setFilteredData] = useState<GatewayInfo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const router = useRouter()
|
||||
|
||||
const form = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
status: 'all',
|
||||
},
|
||||
})
|
||||
|
||||
const { watch } = form
|
||||
const statusFilter = watch('status')
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!data.length) return
|
||||
|
||||
if (statusFilter === 'all') {
|
||||
setFilteredData(data)
|
||||
} else {
|
||||
const filterValue = statusFilter || 'all'
|
||||
if (filterValue === 'all') {
|
||||
setFilteredData(data)
|
||||
} else {
|
||||
const enableValue = parseInt(filterValue)
|
||||
setFilteredData(data.filter(item => item.enable === enableValue))
|
||||
}
|
||||
}
|
||||
}, [data, statusFilter])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
const response = await fetch('/api/stats?type=gateway_info')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取网关信息失败')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
setData(result)
|
||||
// const sortedData = result.sort(( a, b) => Number(a.inner_ip) - Number(b.inner_ip))
|
||||
const sortedData = result.sort((a: GatewayInfo, b: GatewayInfo) =>
|
||||
sortByIpAddress(a.inner_ip, b.inner_ip)
|
||||
)
|
||||
setData(sortedData)
|
||||
setFilteredData(sortedData) // 初始化时设置filteredData
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch gateway info:', error)
|
||||
setError(error instanceof Error ? error.message : '获取网关信息失败')
|
||||
@@ -53,7 +104,7 @@ export default function Gatewayinfo() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<div className="bg-white shadow rounded-lg p-6 overflow-hidden">
|
||||
<h2 className="text-lg font-semibold mb-4">网关基本信息</h2>
|
||||
<div className="text-center py-8">加载网关信息中...</div>
|
||||
</div>
|
||||
@@ -70,10 +121,82 @@ export default function Gatewayinfo() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">网关基本信息</h2>
|
||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 overflow-hidden">
|
||||
<div className='flex gap-6'>
|
||||
<div className='flex flex-3 justify-between '>
|
||||
<span className="text-lg pt-2 font-semibold mb-4">网关基本信息</span>
|
||||
<Form {...form}>
|
||||
<form className="flex items-center gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm mr-2">状态筛选:</span>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue="all"
|
||||
>
|
||||
<SelectTrigger className="h-9 w-36">
|
||||
<SelectValue placeholder="选择状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="1">启用</SelectItem>
|
||||
<SelectItem value="0">禁用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<div className='flex flex-1'></div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex-3 w-full flex">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="px-4 py-2 text-left">MAC地址</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left">内网IP</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left">配置版本</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left">状态</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
||||
>
|
||||
<TableCell className="px-4 py-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push(`/dashboard?tab=gateway&mac=${item.macaddr}`);
|
||||
}}
|
||||
className="font-mono text-blue-600 hover:text-blue-800 hover:underline cursor-pointer"
|
||||
>
|
||||
{item.macaddr}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.inner_ip}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.setid}</TableCell>
|
||||
<TableCell className="px-4 py-2">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(item.enable)}`}>
|
||||
{getStatusText(item.enable)}
|
||||
</span>
|
||||
</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">
|
||||
<div className="text-2xl font-bold text-blue-600">{data.length}</div>
|
||||
<div className="text-sm text-blue-800">网关总数</div>
|
||||
@@ -97,41 +220,6 @@ export default function Gatewayinfo() {
|
||||
<div className="text-sm text-purple-800">配置版本数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full table-auto">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-4 py-2 text-left">MAC地址</th>
|
||||
<th className="px-4 py-2 text-left">内网IP</th>
|
||||
<th className="px-4 py-2 text-left">配置版本</th>
|
||||
<th className="px-4 py-2 text-left">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, index) => (
|
||||
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-4 py-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push(`/dashboard?tab=gateway&mac=${item.macaddr}`);
|
||||
}}
|
||||
className="font-mono text-blue-600 hover:text-blue-800 hover:underline cursor-pointer"
|
||||
>
|
||||
{item.macaddr}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-2">{item.inner_ip}</td>
|
||||
<td className="px-4 py-2">{item.setid}</td>
|
||||
<td className="px-4 py-2">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusClass(item.enable)}`}>
|
||||
{getStatusText(item.enable)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -9,14 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { User, Lock, Search, Trash2, Plus, X } from 'lucide-react'
|
||||
import { toast, Toaster } from 'sonner'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"
|
||||
|
||||
// 用户类型定义
|
||||
interface UserData {
|
||||
@@ -152,7 +145,7 @@ const handleDeleteUser = async (userId: number) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white p-4 md:p-8">
|
||||
<div className="bg-white p-4 md:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">用户管理</h1>
|
||||
@@ -271,13 +264,14 @@ const handleDeleteUser = async (userId: number) => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用户列表</CardTitle>
|
||||
<CardDescription>
|
||||
管理系统中的所有用户账户
|
||||
</CardDescription>
|
||||
<div className="relative mt-4 max-w-sm">
|
||||
{/* 用户列表直接显示在页面上 */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">用户列表</h2>
|
||||
<p className="text-muted-foreground">管理系统中的所有用户账户</p>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-sm mt-4">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索用户..."
|
||||
@@ -286,8 +280,8 @@ const handleDeleteUser = async (userId: number) => {
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -299,7 +293,7 @@ const handleDeleteUser = async (userId: number) => {
|
||||
<TableBody>
|
||||
{filteredUsers.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-4">
|
||||
<TableCell colSpan={3} className="text-center py-4">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -309,14 +303,13 @@ const handleDeleteUser = async (userId: number) => {
|
||||
<TableCell className="font-medium">{user.account}</TableCell>
|
||||
<TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-5 border-0 hover:bg-transparent"
|
||||
onClick={() => handleDeleteUser(Number(user.id))}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
><Trash2 className="h-4 w-4" /></Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -324,8 +317,8 @@ const handleDeleteUser = async (userId: number) => {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
|
||||
@@ -64,8 +64,8 @@ function DashboardContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<nav className="bg-white shadow-sm">
|
||||
<div className=" bg-gray-100 w-screen h-screen flex flex-col">
|
||||
<nav className="bg-white flex-none h-16 shadow-sm">
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16 items-center">
|
||||
<div className="flex items-center">
|
||||
@@ -85,14 +85,14 @@ function DashboardContent() {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-3 overflow-hidden px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<nav className="flex flex-col w-64 -mb-px space-x-8">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
className={`py-2 px-1 h-12 border-b-2 font-medium text-sm ${
|
||||
activeTab === tab.id
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
@@ -104,7 +104,7 @@ function DashboardContent() {
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 flex-auto">
|
||||
{activeTab === 'gatewayInfo' && <Gatewayinfo />}
|
||||
{activeTab === 'gateway' && <GatewayConfig />}
|
||||
{activeTab === 'city' && <CityNodeStats />}
|
||||
@@ -120,7 +120,7 @@ function DashboardContent() {
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<div className=" bg-gray-100 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">加载中...</p>
|
||||
|
||||
@@ -1,20 +1,188 @@
|
||||
import * as React from "react"
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import {useState, useEffect} from 'react'
|
||||
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
} from 'lucide-react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import {cn} from '@/lib/utils'
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from './select'
|
||||
|
||||
export interface PaginationProps {
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
sizeOptions?: number[]
|
||||
onPageChange?: (page: number) => void
|
||||
onSizeChange?: (size: number) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
function Pagination({
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
sizeOptions = [10, 20, 50, 100],
|
||||
onPageChange,
|
||||
onSizeChange,
|
||||
className,
|
||||
}: PaginationProps) {
|
||||
const [currentPage, setCurrentPage] = useState(page)
|
||||
const totalPages = Math.ceil(total / size)
|
||||
|
||||
// 同步外部 page 变化
|
||||
useEffect(() => {
|
||||
setCurrentPage(page)
|
||||
}, [page])
|
||||
|
||||
// 分页器逻辑
|
||||
const generatePaginationItems = () => {
|
||||
// 最多显示7个页码,其余用省略号
|
||||
const SIBLINGS = 1 // 当前页左右各显示的页码数
|
||||
const DOTS = -1 // 省略号标记
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// 总页数少于7,全部显示
|
||||
return Array.from({length: totalPages}, (_, i) => i + 1)
|
||||
}
|
||||
|
||||
// 是否需要显示左边的省略号
|
||||
const showLeftDots = currentPage > 2 + SIBLINGS
|
||||
|
||||
// 是否需要显示右边的省略号
|
||||
const showRightDots = currentPage < totalPages - (2 + SIBLINGS)
|
||||
|
||||
if (showLeftDots && showRightDots) {
|
||||
// 两边都有省略号
|
||||
const leftSiblingIndex = Math.max(currentPage - SIBLINGS, 1)
|
||||
const rightSiblingIndex = Math.min(currentPage + SIBLINGS, totalPages)
|
||||
|
||||
return [1, DOTS, ...Array.from(
|
||||
{length: rightSiblingIndex - leftSiblingIndex + 1},
|
||||
(_, i) => leftSiblingIndex + i,
|
||||
), DOTS, totalPages]
|
||||
}
|
||||
|
||||
if (!showLeftDots && showRightDots) {
|
||||
// 只有右边有省略号
|
||||
return [...Array.from({length: 3 + SIBLINGS * 2}, (_, i) => i + 1), DOTS, totalPages]
|
||||
}
|
||||
|
||||
if (showLeftDots && !showRightDots) {
|
||||
// 只有左边有省略号
|
||||
return [1, DOTS, ...Array.from(
|
||||
{length: 3 + SIBLINGS * 2},
|
||||
(_, i) => totalPages - (3 + SIBLINGS * 2) + i + 1,
|
||||
)]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage < 1 || newPage > totalPages || newPage === currentPage) {
|
||||
return
|
||||
}
|
||||
setCurrentPage(newPage)
|
||||
onPageChange?.(newPage)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (newSize: string) => {
|
||||
const parsedSize = parseInt(newSize, 10)
|
||||
if (onSizeChange) {
|
||||
onSizeChange(parsedSize)
|
||||
}
|
||||
}
|
||||
|
||||
const paginationItems = generatePaginationItems()
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap items-center justify-end gap-4 ${className || ''}`}>
|
||||
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
|
||||
共
|
||||
{' '}
|
||||
{total}
|
||||
{' '}
|
||||
条记录,每页
|
||||
<Select
|
||||
value={size.toString()}
|
||||
onValueChange={handlePageSizeChange}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-20">
|
||||
<SelectValue/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sizeOptions.map(option => (
|
||||
<SelectItem key={option} value={option.toString()}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
条
|
||||
</div>
|
||||
|
||||
<PaginationLayout>
|
||||
<PaginationContent>
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
className={currentPage === 1 ? 'opacity-50 pointer-events-none' : ''}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{paginationItems.map((pageNum, index) => {
|
||||
if (pageNum === -1) {
|
||||
return (
|
||||
<PaginationItem key={`dots-${index}`}>
|
||||
<PaginationEllipsis/>
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PaginationItem key={pageNum}>
|
||||
<PaginationLink
|
||||
isActive={pageNum === currentPage}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
})}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
className={currentPage === totalPages ? 'opacity-50 pointer-events-none' : ''}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
</PaginationContent>
|
||||
</PaginationLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationLayout({className, ...props}: React.ComponentProps<'nav'>) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
className={cn('flex-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -23,42 +191,39 @@ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
}: React.ComponentProps<'ul'>) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
function PaginationItem({...props}: React.ComponentProps<'li'>) {
|
||||
return <li data-slot="pagination-item" {...props}/>
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
} & React.ComponentProps<'a'>
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
'inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-9 w-9 rounded-md border border-input hover:bg-secondary hover:text-secondary-foreground',
|
||||
`bg-card`,
|
||||
isActive && 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -72,12 +237,10 @@ function PaginationPrevious({
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon/>
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
@@ -89,11 +252,9 @@ function PaginationNext({
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon/>
|
||||
</PaginationLink>
|
||||
)
|
||||
@@ -102,12 +263,12 @@ function PaginationNext({
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4"/>
|
||||
@@ -118,6 +279,7 @@ function PaginationEllipsis({
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationLayout,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
|
||||
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
className={cn("[&_tr]:border-b sticky top-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors h-10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -83,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"p-2 h-10 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user