更新布局和微调整页面样式

This commit is contained in:
wmp
2025-09-20 17:03:16 +08:00
parent 53feaa5e7d
commit 4f3671c8a6
10 changed files with 636 additions and 544 deletions

View File

@@ -153,9 +153,10 @@ async function getAllocationStatus() {
// 获取节点信息 // 获取节点信息
async function getEdgeNodes(request: NextRequest) { async function getEdgeNodes(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const threshold = searchParams.get('threshold') || '20' const threshold = searchParams.get('threshold') || '0'
const limit = searchParams.get('limit') || '100' const limit = searchParams.get('limit') || '100'
// 使用参数化查询防止SQL注入 // 使用参数化查询防止SQL注入

View File

@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react'
import { formatNumber, validateNumber } from '@/lib/formatters' import { formatNumber, validateNumber } from '@/lib/formatters'
import LoadingCard from '@/components/ui/loadingCard' import LoadingCard from '@/components/ui/loadingCard'
import ErrorCard from '@/components/ui/errorCard' import ErrorCard from '@/components/ui/errorCard'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
interface AllocationStatus { interface AllocationStatus {
city: string city: string
@@ -66,8 +67,13 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
return filterDate.toISOString().slice(0, 19).replace('T', ' ') return filterDate.toISOString().slice(0, 19).replace('T', ' ')
}, [timeFilter, customTime]) }, [timeFilter, customTime])
// 计算超额量
const calculateOverage = (assigned: number, count: number) => {
const overage = assigned - count;
return Math.max(0, overage);
}
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
try { try {
setError(null) setError(null)
setLoading(true) setLoading(true)
@@ -86,7 +92,9 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
assigned: validateNumber(item.assigned), assigned: validateNumber(item.assigned),
})) }))
setData(validatedData) const sortedData = validatedData.sort((a, b) => b.count - a.count)
setData(sortedData)
} catch (error) { } catch (error) {
console.error('Failed to fetch allocation status:', error) console.error('Failed to fetch allocation status:', error)
setError(error instanceof Error ? error.message : 'Unknown 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 (loading) return <LoadingCard title="节点分配状态" />
if (error) return <ErrorCard title="节点分配状态" error={error} onRetry={fetchData} /> 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 ( 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> <h2 className="text-lg font-semibold mb-4"></h2>
{/* 时间筛选器 */} {/* 时间筛选器 */}
@@ -140,45 +148,54 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
onClick={fetchData} onClick={fetchData}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
> >
</button> </button>
</div> </div>
<div className='flex gap-6 overflow-hidden'>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div className="flex flex-3 w-full">
<div className="bg-blue-50 p-4 rounded-lg"> <Table>
<div className="text-2xl font-bold text-blue-600">{formatNumber(data.length)}</div> <TableHeader>
<div className="text-sm text-blue-800"></div> <TableRow className="bg-gray-50">
</div> <TableHead className="px-4 py-2 text-left"></TableHead>
<div className="bg-orange-50 p-4 rounded-lg"> <TableHead className="px-4 py-2 text-left">IP量</TableHead>
<div className="text-2xl font-bold text-orange-600">{formatNumber(problematicCities.length)}</div> <TableHead className="px-4 py-2 text-left">IP量</TableHead>
<div className="text-sm text-orange-800"></div> <TableHead className="px-4 py-2 text-left"></TableHead>
</div> </TableRow>
</div> </TableHeader>
<TableBody>
{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) => { {data.map((item, index) => {
const overage = calculateOverage(item.assigned, item.count)
return ( return (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}> <TableRow
<td className="px-4 py-2">{item.city}</td> key={index}
<td className="px-4 py-2">{formatNumber(item.count)}</td> className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
<td className="px-4 py-2">{formatNumber(item.assigned)}</td> >
</tr> <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>
) )
})} })}
</tbody> </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>
) )
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
interface CityNode { interface CityNode {
city: string city: string
@@ -32,7 +33,7 @@ export default function CityNodeStats() {
if (loading) { if (loading) {
return ( 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> <h2 className="text-lg font-semibold mb-4"></h2>
<div className="text-gray-600">...</div> <div className="text-gray-600">...</div>
</div> </div>
@@ -40,7 +41,7 @@ export default function CityNodeStats() {
} }
return ( 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"> <div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold"></h2> <h2 className="text-lg font-semibold"></h2>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
@@ -48,35 +49,37 @@ export default function CityNodeStats() {
</span> </span>
</div> </div>
<div className="overflow-x-auto"> <div className="flex overflow-hidden ">
<table className="w-full"> <div className='flex w-full'>
<thead> <Table>
<tr className="border-b border-gray-200"> <TableHeader>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600"></th> <TableRow className="bg-gray-50">
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600"></th> <TableHead className="px-4 py-2 text-left font-medium text-gray-600"></TableHead>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600">Hash</th> <TableHead className="px-4 py-2 text-left font-medium text-gray-600"></TableHead>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600"></th> <TableHead className="px-4 py-2 text-left font-medium text-gray-600">Hash</TableHead>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-600"></th> <TableHead className="px-4 py-2 text-left font-medium text-gray-600"></TableHead>
</tr> <TableHead className="px-4 py-2 text-left font-medium text-gray-600"></TableHead>
</thead> </TableRow>
<tbody> </TableHeader>
{data.map((item, index) => ( <TableBody>
<tr key={index} className="border-b border-gray-100 hover:bg-gray-50"> {data.map((item, index) => (
<td className="px-4 py-3 text-sm font-medium">{item.city}</td> <TableRow
<td className="px-4 py-3 text-sm"> key={index}
<span className="font-semibold text-gray-700">{item.count}</span> className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
</td> >
<td className="px-4 py-3 text-sm text-gray-500 font-mono">{item.hash}</td> <TableCell className="px-4 py-2">{item.city}</TableCell>
<td className="px-4 py-3 text-sm"> <TableCell className="px-4 py-2">{item.count}</TableCell>
<span className="bg-gray-100 px-2 py-1 rounded text-gray-700"> <TableCell className="px-4 py-2">{item.hash}</TableCell>
{item.label} <TableCell className="px-4 py-2">
</span> <span className="bg-gray-100 px-2 py-1 rounded text-gray-700">
</td> {item.label}</span>
<td className="px-4 py-3 text-sm font-semibold">{item.offset}</td> </TableCell>
</tr> <TableCell className="px-4 py-2">{item.offset}</TableCell>
))} </TableRow>
</tbody> ))}
</table> </TableBody>
</Table>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -2,14 +2,8 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { validateNumber } from '@/lib/formatters' import { validateNumber } from '@/lib/formatters'
import { import { Pagination } from '@/components/ui/pagination'
Pagination, import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
interface Edge { interface Edge {
id: number id: number
@@ -27,8 +21,8 @@ export default function Edge() {
const [data, setData] = useState<Edge[]>([]) const [data, setData] = useState<Edge[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [idThreshold, setIdThreshold] = useState(20) const [idThreshold,] = useState(0)
const [limit, setLimit] = useState(100) const [limit,] = useState(100)
// 分页状态 // 分页状态
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
@@ -43,7 +37,6 @@ export default function Edge() {
try { try {
setError(null) setError(null)
setLoading(true) setLoading(true)
const response = await fetch(`/api/stats?type=edge_nodes&threshold=${threshold}&limit=${resultLimit}`) const response = await fetch(`/api/stats?type=edge_nodes&threshold=${threshold}&limit=${resultLimit}`)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) 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 => { const formatBoolean = (value: boolean | number): string => {
return value ? '是' : '否' return value ? '是' : '否'
} }
@@ -104,38 +92,16 @@ export default function Edge() {
const indexOfLastItem = currentPage * itemsPerPage const indexOfLastItem = currentPage * itemsPerPage
const indexOfFirstItem = indexOfLastItem - itemsPerPage const indexOfFirstItem = indexOfLastItem - itemsPerPage
const currentItems = data.slice(indexOfFirstItem, indexOfLastItem) const currentItems = data.slice(indexOfFirstItem, indexOfLastItem)
const totalPages = Math.ceil(totalItems / itemsPerPage)
// 生成页码按钮 // 处理页码变化
const renderPageNumbers = () => { const handlePageChange = (page: number) => {
const pageNumbers = [] setCurrentPage(page)
const maxVisiblePages = 5 }
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)) // 处理每页显示数量变化
const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1) const handleSizeChange = (size: number) => {
setItemsPerPage(size)
if (endPage - startPage + 1 < maxVisiblePages) { setCurrentPage(1) // 重置到第一页
startPage = Math.max(1, endPage - maxVisiblePages + 1)
}
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
} }
if (loading) return ( if (loading) return (
@@ -160,57 +126,6 @@ export default function Edge() {
return ( return (
<div className="bg-white shadow rounded-lg p-6"> <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 ? ( {data.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="text-gray-400 text-4xl mb-4">📋</div> <div className="text-gray-400 text-4xl mb-4">📋</div>
@@ -218,135 +133,70 @@ export default function Edge() {
</div> </div>
) : ( ) : (
<> <>
<div className="bg-blue-50 p-4 rounded-lg mb-6"> <div className='flex gap-6 overflow-hidden'>
<p className="text-blue-800"> <div className="flex-3 w-full overflow-y-auto">
<span className="font-bold">{totalItems}</span> <Table>
{idThreshold > 0 && ` (ID大于${idThreshold})`} <TableHeader>
</p> <TableRow className="bg-gray-50">
</div> <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>
<div className="flex justify-between items-center mb-4"> <TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">IP</TableHead>
<div className="flex items-center space-x-2"> <TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider"></TableHead>
<span className="text-sm text-gray-700"></span> <TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">IP节点</TableHead>
<select <TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">IP</TableHead>
value={itemsPerPage} <TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider"></TableHead>
onChange={(e) => { <TableHead className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">线</TableHead>
setItemsPerPage(Number(e.target.value)) </TableRow>
setCurrentPage(1) </TableHeader>
}} <TableBody>
className="border border-gray-300 rounded-md px-2 py-1 text-sm" {currentItems.map((item, index) => (
> <TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<option value="10">10</option> <TableCell className="px-4 py-3 text-sm text-gray-900">{item.id}</TableCell>
<option value="20">20</option> <TableCell className="px-4 py-3 text-sm font-mono text-blue-600">{item.macaddr}</TableCell>
<option value="50">50</option> <TableCell className="px-4 py-3 text-sm text-gray-700">{item.city}</TableCell>
<option value="100">100</option> <TableCell className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</TableCell>
</select> <TableCell className="px-4 py-3 text-sm text-gray-700">
<span className="text-sm text-gray-700"></span> <span className={`px-2 py-1 rounded-full text-xs ${
</div> item.isp === '移动' ? 'bg-blue-100 text-blue-800' :
</div> item.isp === '电信' ? 'bg-purple-100 text-purple-800' :
item.isp === '联通' ? 'bg-red-100 text-red-800' :
<div className="overflow-x-auto rounded-lg shadow mb-4"> 'bg-gray-100 text-gray-800'
<table className="min-w-full table-auto border-collapse"> }`}>
<thead> {item.isp}
<tr className="bg-gray-100"> </span></TableCell>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">ID</th> <TableCell className="px-4 py-3 text-sm text-center">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">MAC地址</th> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider"></th> item.single ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
<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> {formatBoolean(item.single)}
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">IP节点</th> </span></TableCell>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">IP</th> <TableCell className="px-4 py-3 text-sm text-center">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider"></th> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
<th className="px-4 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider">线</th> item.sole ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
</tr> }`}>
</thead> {formatBoolean(item.sole)}
<tbody className="bg-white divide-y divide-gray-200"> </span></TableCell>
{currentItems.map((item, index) => ( <TableCell className="px-4 py-3 text-sm text-center">
<tr <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
key={item.id} {item.arch}
className={`hover:bg-gray-50 transition-colors ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`} </span></TableCell>
> <TableCell className="px-4 py-3 text-sm text-gray-700">{formatOnlineTime(item.online)}</TableCell>
<td className="px-4 py-3 text-sm text-gray-900">{item.id}</td> </TableRow>
<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> </TableBody>
<td className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</td> </Table>
<td 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' :
item.isp === '联通' ? 'bg-red-100 text-red-800' :
'bg-gray-100 text-gray-800'
}`}>
{item.isp}
</span>
</td>
<td 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 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 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>
))}
</tbody>
</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" : ""}
/>
</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> </div>
{/* 使用 Pagination 组件 */}
<Pagination
page={currentPage}
size={itemsPerPage}
total={totalItems}
onPageChange={handlePageChange}
onSizeChange={handleSizeChange}
className="mt-4"
/>
</> </>
)} )}
</div> </div>

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
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'
interface GatewayConfig { interface GatewayConfig {
id: number id: number
@@ -28,25 +29,24 @@ function GatewayConfigContent() {
setMacAddress(urlMac) setMacAddress(urlMac)
fetchData(urlMac) fetchData(urlMac)
} else { } else {
// 如果没有mac参数显示空状态或默认查询 // 如果没有mac参数显示所有网关配置
setData([]) setMacAddress('')
setSuccess('请输入MAC地址查询网关配置信息') fetchData('')
} }
}, [searchParams]) }, [searchParams])
const fetchData = async (mac: string) => { const fetchData = async (mac: string) => {
if (!mac.trim()) {
setError('请输入MAC地址')
setSuccess('')
setData([])
return
}
setLoading(true) setLoading(true)
setError('') setError('')
setSuccess('') setSuccess('')
try { 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() const result = await response.json()
if (!response.ok) { if (!response.ok) {
@@ -55,7 +55,11 @@ function GatewayConfigContent() {
// 检查返回的数据是否有效 // 检查返回的数据是否有效
if (!result || result.length === 0) { if (!result || result.length === 0) {
setError(`未找到MAC地址为 ${mac} 的网关配置信息`) if (mac.trim()) {
setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
} else {
setError('未找到任何网关配置信息')
}
setData([]) setData([])
return return
} }
@@ -79,7 +83,6 @@ function GatewayConfigContent() {
})) }))
setData(validatedData) setData(validatedData)
setSuccess(`成功查询到 ${validatedData.length} 条网关配置信息`)
} catch (error) { } catch (error) {
console.error('Failed to fetch gateway config:', error) console.error('Failed to fetch gateway config:', error)
setError(error instanceof Error ? error.message : '获取网关配置失败') setError(error instanceof Error ? error.message : '获取网关配置失败')
@@ -91,19 +94,17 @@ function GatewayConfigContent() {
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (macAddress.trim()) { fetchData(macAddress)
fetchData(macAddress)
}
} }
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => { const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
return ( return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ <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-green-100 text-green-800'
: 'bg-red-100 text-red-800' : 'bg-red-100 text-red-800'
}`}> }`}>
{value === 1 ? trueText : falseText} {value === 0 ? trueText : falseText}
</span> </span>
) )
} }
@@ -120,7 +121,7 @@ function GatewayConfigContent() {
} }
return ( 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 className="flex justify-between items-start mb-6">
<div> <div>
<h2 className="text-xl font-semibold text-gray-800"></h2> <h2 className="text-xl font-semibold text-gray-800"></h2>
@@ -138,7 +139,7 @@ function GatewayConfigContent() {
type="text" type="text"
value={macAddress} value={macAddress}
onChange={(e) => setMacAddress(e.target.value)} 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" 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 <button
@@ -178,104 +179,81 @@ function GatewayConfigContent() {
<p className="text-gray-600">...</p> <p className="text-gray-600">...</p>
</div> </div>
) : data.length > 0 ? ( ) : data.length > 0 ? (
<> <>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 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>
</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>
<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>
</div>
{/* 详细表格 */} {/* 详细表格 */}
<div className="overflow-hidden rounded-lg shadow-sm border border-gray-200"> <div className='flex gap-6 overflow-hidden'>
<table className="min-w-full divide-y divide-gray-200"> <div className="flex-3 w-full flex">
<thead className="bg-gray-50"> <Table>
<tr> <TableHeader>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</th> <TableRow className="bg-gray-50 TableRow ">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</TableHead>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</th> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</TableHead>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">线</th> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
</tr> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">线</TableHead>
</thead> </TableRow>
<tbody className="bg-white divide-y divide-gray-200"> </TableHeader>
<TableBody>
{data.map((item, index) => ( {data.map((item, index) => (
<tr key={index} className="hover:bg-gray-50 transition-colors"> <TableRow
<td className="px-6 py-4 whitespace-nowrap"> key={index}
<div className="font-mono text-sm text-blue-600 font-medium"> className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50 '}
{item.edge} >
</div> <TableCell className="hover:bg-gray-50 transition-colors">
</td> <div className="font-mono text-sm text-blue-600 font-medium">{item.edge}</div>
<td className="px-6 py-4 whitespace-nowrap"> </TableCell>
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs"> <TableCell className="px-6 whitespace-nowrap">
{item.city} <span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
</span> {item.city}
</td> </span></TableCell>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <TableCell className="px-6 whitespace-nowrap text-sm text-gray-900">{item.user}</TableCell>
{item.user} <TableCell className="px-6 whitespace-nowrap">
</td> <div className="font-mono text-sm text-green-600">{item.public}</div>
<td className="px-6 py-4 whitespace-nowrap"> </TableCell>
<div className="font-mono text-sm text-green-600"> <TableCell className="px-6 whitespace-nowrap">
{item.public} <div className="font-mono text-sm text-purple-600">{item.inner_ip}</div>
</div> </TableCell>
</td> <TableCell className="px-6 whitespace-nowrap">
<td className="px-6 py-4 whitespace-nowrap"> <div className="font-mono text-sm text-green-600">{getStatusBadge(item.ischange, '正常', '需更新')}</div>
<div className="font-mono text-sm text-purple-600"> </TableCell>
{item.inner_ip} <TableCell className="px-6 whitespace-nowrap">
</div> <div className="font-mono text-sm text-purple-600">{getOnlineStatus(item.isonline)}</div>
</td> </TableCell>
<td className="px-6 py-4 whitespace-nowrap"> </TableRow>
{getStatusBadge(item.ischange, '已更新', '未更新')}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getOnlineStatus(item.isonline)}
</td>
</tr>
))} ))}
</tbody> </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 border border-blue-100">
<div className="mt-4 flex justify-between items-center text-sm text-gray-600"> <div className="text-2xl font-bold text-blue-600">{data.length}</div>
<span> 1 {data.length} {data.length} </span> <div className="text-sm text-blue-800"></div>
<button </div>
onClick={() => fetchData(macAddress)} <div className="bg-green-50 p-4 rounded-lg border border-green-100">
className="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors" <div className="text-2xl font-bold text-green-600">
> {data.filter(item => item.isonline === 1).length}
</div>
</button> <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>
</div> </div>
</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> </div>
) )

View File

@@ -2,6 +2,12 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' 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 { interface GatewayInfo {
macaddr: string macaddr: string
@@ -10,29 +16,74 @@ interface GatewayInfo {
enable: number 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() { export default function Gatewayinfo() {
const [data, setData] = useState<GatewayInfo[]>([]) const [data, setData] = useState<GatewayInfo[]>([])
const [filteredData, setFilteredData] = useState<GatewayInfo[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
const router = useRouter() const router = useRouter()
const form = useForm<FilterSchema>({
resolver: zodResolver(filterSchema),
defaultValues: {
status: 'all',
},
})
const { watch } = form
const statusFilter = watch('status')
useEffect(() => { useEffect(() => {
fetchData() 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 () => { const fetchData = async () => {
try { try {
setLoading(true) setLoading(true)
setError('') setError('')
const response = await fetch('/api/stats?type=gateway_info') const response = await fetch('/api/stats?type=gateway_info')
if (!response.ok) { if (!response.ok) {
throw new Error('获取网关信息失败') throw new Error('获取网关信息失败')
} }
const result = await response.json() const result = await response.json()
// const sortedData = result.sort(( a, b) => Number(a.inner_ip) - Number(b.inner_ip))
setData(result) const sortedData = result.sort((a: GatewayInfo, b: GatewayInfo) =>
sortByIpAddress(a.inner_ip, b.inner_ip)
)
setData(sortedData)
setFilteredData(sortedData) // 初始化时设置filteredData
} catch (error) { } catch (error) {
console.error('Failed to fetch gateway info:', error) console.error('Failed to fetch gateway info:', error)
setError(error instanceof Error ? error.message : '获取网关信息失败') setError(error instanceof Error ? error.message : '获取网关信息失败')
@@ -53,7 +104,7 @@ export default function Gatewayinfo() {
if (loading) { if (loading) {
return ( 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> <h2 className="text-lg font-semibold mb-4"></h2>
<div className="text-center py-8">...</div> <div className="text-center py-8">...</div>
</div> </div>
@@ -67,71 +118,108 @@ export default function Gatewayinfo() {
<div className="text-center py-8 text-red-600">{error}</div> <div className="text-center py-8 text-red-600">{error}</div>
</div> </div>
) )
} }
return ( 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> <div className='flex gap-6'>
<div className='flex flex-3 justify-between '>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"> <span className="text-lg pt-2 font-semibold mb-4"></span>
<div className="bg-blue-50 p-4 rounded-lg"> <Form {...form}>
<div className="text-2xl font-bold text-blue-600">{data.length}</div> <form className="flex items-center gap-4">
<div className="text-sm text-blue-800"></div> <FormField
</div> control={form.control}
<div className="bg-green-50 p-4 rounded-lg"> name="status"
<div className="text-2xl font-bold text-green-600"> render={({ field }) => (
{data.filter(item => item.enable === 1).length} <div className="flex items-center">
</div> <span className="text-sm mr-2">:</span>
<div className="text-sm text-green-800"></div> <Select
</div> value={field.value}
<div className="bg-red-50 p-4 rounded-lg"> onValueChange={field.onChange}
<div className="text-2xl font-bold text-red-600"> defaultValue="all"
{data.filter(item => item.enable === 0).length} >
</div> <SelectTrigger className="h-9 w-36">
<div className="text-sm text-red-800"></div> <SelectValue placeholder="选择状态" />
</div> </SelectTrigger>
<div className="bg-purple-50 p-4 rounded-lg"> <SelectContent>
<div className="text-2xl font-bold text-purple-600"> <SelectItem value="all"></SelectItem>
{new Set(data.map(item => item.setid)).size} <SelectItem value="1"></SelectItem>
</div> <SelectItem value="0"></SelectItem>
<div className="text-sm text-purple-800"></div> </SelectContent>
</Select>
</div>
)}
/>
</form>
</Form>
</div> </div>
<div className='flex flex-1'></div>
</div> </div>
<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="overflow-x-auto"> <div className="flex flex-1 flex-col gap-4 mb-6">
<table className="min-w-full table-auto"> <div className="bg-blue-50 p-4 rounded-lg">
<thead> <div className="text-2xl font-bold text-blue-600">{data.length}</div>
<tr className="bg-gray-50"> <div className="text-sm text-blue-800"></div>
<th className="px-4 py-2 text-left">MAC地址</th> </div>
<th className="px-4 py-2 text-left">IP</th> <div className="bg-green-50 p-4 rounded-lg">
<th className="px-4 py-2 text-left"></th> <div className="text-2xl font-bold text-green-600">
<th className="px-4 py-2 text-left"></th> {data.filter(item => item.enable === 1).length}
</tr> </div>
</thead> <div className="text-sm text-green-800"></div>
<tbody> </div>
{data.map((item, index) => ( <div className="bg-red-50 p-4 rounded-lg">
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}> <div className="text-2xl font-bold text-red-600">
<td className="px-4 py-2"> {data.filter(item => item.enable === 0).length}
<button </div>
onClick={() => { <div className="text-sm text-red-800"></div>
router.push(`/dashboard?tab=gateway&mac=${item.macaddr}`); </div>
}} <div className="bg-purple-50 p-4 rounded-lg">
className="font-mono text-blue-600 hover:text-blue-800 hover:underline cursor-pointer" <div className="text-2xl font-bold text-purple-600">
> {new Set(data.map(item => item.setid)).size}
{item.macaddr} </div>
</button> <div className="text-sm text-purple-800"></div>
</td> </div>
<td className="px-4 py-2">{item.inner_ip}</td> </div>
<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>
</div> </div>
) )

View File

@@ -9,14 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { User, Lock, Search, Trash2, Plus, X } from 'lucide-react' import { User, Lock, Search, Trash2, Plus, X } from 'lucide-react'
import { toast, Toaster } from 'sonner' import { toast, Toaster } from 'sonner'
import { import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
// 用户类型定义 // 用户类型定义
interface UserData { interface UserData {
@@ -152,7 +145,7 @@ const handleDeleteUser = async (userId: number) => {
) )
return ( 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="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold"></h1> <h1 className="text-3xl font-bold"></h1>
@@ -271,23 +264,24 @@ const handleDeleteUser = async (userId: number) => {
</Card> </Card>
)} )}
<Card> {/* 用户列表直接显示在页面上 */}
<CardHeader> <div className="space-y-4">
<CardTitle></CardTitle> <div>
<CardDescription> <h2 className="text-2xl font-semibold"></h2>
<p className="text-muted-foreground"></p>
</CardDescription> </div>
<div className="relative mt-4 max-w-sm">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <div className="relative max-w-sm mt-4">
<Input <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
placeholder="搜索用户..." <Input
className="pl-8" placeholder="搜索用户..."
value={searchTerm} className="pl-8"
onChange={(e) => setSearchTerm(e.target.value)} value={searchTerm}
/> onChange={(e) => setSearchTerm(e.target.value)}
</div> />
</CardHeader> </div>
<CardContent>
<div className="border rounded-lg">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@@ -299,7 +293,7 @@ const handleDeleteUser = async (userId: number) => {
<TableBody> <TableBody>
{filteredUsers.length === 0 ? ( {filteredUsers.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={4} className="text-center py-4"> <TableCell colSpan={3} className="text-center py-4">
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -309,14 +303,13 @@ const handleDeleteUser = async (userId: number) => {
<TableCell className="font-medium">{user.account}</TableCell> <TableCell className="font-medium">{user.account}</TableCell>
<TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell> <TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-5 border-0 hover:bg-transparent"
onClick={() => handleDeleteUser(Number(user.id))} onClick={() => handleDeleteUser(Number(user.id))}
> ><Trash2 className="h-4 w-4" /></Button>
<Trash2 className="h-4 w-4" />
</Button>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -324,8 +317,8 @@ const handleDeleteUser = async (userId: number) => {
)} )}
</TableBody> </TableBody>
</Table> </Table>
</CardContent> </div>
</Card> </div>
</div> </div>
<Toaster richColors /> <Toaster richColors />
</div> </div>

View File

@@ -64,8 +64,8 @@ function DashboardContent() {
} }
return ( return (
<div className="min-h-screen bg-gray-100"> <div className=" bg-gray-100 w-screen h-screen flex flex-col">
<nav className="bg-white shadow-sm"> <nav className="bg-white flex-none h-16 shadow-sm">
<div className="px-4 sm:px-6 lg:px-8"> <div className="px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center"> <div className="flex justify-between h-16 items-center">
<div className="flex items-center"> <div className="flex items-center">
@@ -85,14 +85,14 @@ function DashboardContent() {
</div> </div>
</nav> </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"> <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) => ( {tabs.map((tab) => (
<button <button
key={tab.id} key={tab.id}
onClick={() => handleTabClick(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 activeTab === tab.id
? 'border-blue-500 text-blue-600' ? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
@@ -104,7 +104,7 @@ function DashboardContent() {
</nav> </nav>
</div> </div>
<div className="grid grid-cols-1 gap-6"> <div className="grid grid-cols-1 gap-6 flex-auto">
{activeTab === 'gatewayInfo' && <Gatewayinfo />} {activeTab === 'gatewayInfo' && <Gatewayinfo />}
{activeTab === 'gateway' && <GatewayConfig />} {activeTab === 'gateway' && <GatewayConfig />}
{activeTab === 'city' && <CityNodeStats />} {activeTab === 'city' && <CityNodeStats />}
@@ -120,7 +120,7 @@ function DashboardContent() {
export default function Dashboard() { export default function Dashboard() {
return ( return (
<Suspense fallback={ <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="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div> <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> <p className="mt-4 text-gray-600">...</p>

View File

@@ -1,20 +1,188 @@
import * as React from "react" 'use client'
import * as React from 'react'
import {useState, useEffect} from 'react'
import { import {
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
MoreHorizontalIcon, MoreHorizontalIcon,
} from "lucide-react" } from 'lucide-react'
import { cn } from "@/lib/utils" import {cn} from '@/lib/utils'
import { Button, buttonVariants } from "@/components/ui/button"
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 ( return (
<nav <nav
role="navigation" role="navigation"
aria-label="pagination" aria-label="pagination"
data-slot="pagination" data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)} className={cn('flex-none', className)}
{...props} {...props}
/> />
) )
@@ -23,42 +191,39 @@ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
function PaginationContent({ function PaginationContent({
className, className,
...props ...props
}: React.ComponentProps<"ul">) { }: React.ComponentProps<'ul'>) {
return ( return (
<ul <ul
data-slot="pagination-content" 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} {...props}
/> />
) )
} }
function PaginationItem({ ...props }: React.ComponentProps<"li">) { function PaginationItem({...props}: React.ComponentProps<'li'>) {
return <li data-slot="pagination-item" {...props} /> return <li data-slot="pagination-item" {...props}/>
} }
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> & } & React.ComponentProps<'a'>
React.ComponentProps<"a">
function PaginationLink({ function PaginationLink({
className, className,
isActive, isActive,
size = "icon",
...props ...props
}: PaginationLinkProps) { }: PaginationLinkProps) {
return ( return (
<a <a
aria-current={isActive ? "page" : undefined} aria-current={isActive ? 'page' : undefined}
data-slot="pagination-link" data-slot="pagination-link"
data-active={isActive} data-active={isActive}
className={cn( className={cn(
buttonVariants({ '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',
variant: isActive ? "outline" : "ghost", `bg-card`,
size, isActive && 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground',
}), className,
className
)} )}
{...props} {...props}
/> />
@@ -72,12 +237,10 @@ function PaginationPrevious({
return ( return (
<PaginationLink <PaginationLink
aria-label="Go to previous page" 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} {...props}
> >
<ChevronLeftIcon /> <ChevronLeftIcon/>
<span className="hidden sm:block">Previous</span>
</PaginationLink> </PaginationLink>
) )
} }
@@ -89,12 +252,10 @@ function PaginationNext({
return ( return (
<PaginationLink <PaginationLink
aria-label="Go to next page" 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} {...props}
> >
<span className="hidden sm:block">Next</span> <ChevronRightIcon/>
<ChevronRightIcon />
</PaginationLink> </PaginationLink>
) )
} }
@@ -102,15 +263,15 @@ function PaginationNext({
function PaginationEllipsis({ function PaginationEllipsis({
className, className,
...props ...props
}: React.ComponentProps<"span">) { }: React.ComponentProps<'span'>) {
return ( return (
<span <span
aria-hidden aria-hidden
data-slot="pagination-ellipsis" 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} {...props}
> >
<MoreHorizontalIcon className="size-4" /> <MoreHorizontalIcon className="size-4"/>
<span className="sr-only">More pages</span> <span className="sr-only">More pages</span>
</span> </span>
) )
@@ -118,6 +279,7 @@ function PaginationEllipsis({
export { export {
Pagination, Pagination,
PaginationLayout,
PaginationContent, PaginationContent,
PaginationLink, PaginationLink,
PaginationItem, PaginationItem,

View File

@@ -12,7 +12,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
> >
<table <table
data-slot="table" data-slot="table"
className={cn("w-full caption-bottom text-sm", className)} className={cn("w-full caption-bottom text-sm ", className)}
{...props} {...props}
/> />
</div> </div>
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return ( return (
<thead <thead
data-slot="table-header" data-slot="table-header"
className={cn("[&_tr]:border-b", className)} className={cn("[&_tr]:border-b sticky top-0", className)}
{...props} {...props}
/> />
) )
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( 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 className
)} )}
{...props} {...props}
@@ -83,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
<td <td
data-slot="table-cell" data-slot="table-cell"
className={cn( 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 className
)} )}
{...props} {...props}