更新字段枚举值展示&修复节点分页滑轮滚动和数据总数展示

This commit is contained in:
wmp
2025-09-22 15:11:09 +08:00
parent 826d8fc4c3
commit fd8fede301
4 changed files with 150 additions and 76 deletions

View File

@@ -91,11 +91,23 @@ async function getCityConfigCount() {
async function getCityNodeCount() { async function getCityNodeCount() {
try { try {
const result = await prisma.$queryRaw` const result = await prisma.$queryRaw`
SELECT c.city, c.hash, c.label, COUNT(e.id) as count, c.offset select c.city, c.hash, c.label, e.count, c.\`offset\`
FROM cityhash c from
LEFT JOIN edge e ON c.id = e.city_id cityhash c
GROUP BY c.hash, c.city, c.label, c.offset left join (
ORDER BY count DESC select city_id, count(*) as count
from
edge
where
edge.active is true
group by
city_id
) e
on c.id = e.city_id
group by
c.hash
order by
count desc
` `
return NextResponse.json(safeSerialize(result)) return NextResponse.json(safeSerialize(result))
} catch (error) { } catch (error) {
@@ -158,21 +170,34 @@ async function getAllocationStatus(request: NextRequest) {
// 获取节点信息 // 获取节点信息
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') || '0' const offset = parseInt(searchParams.get('offset') || '0')
const limit = searchParams.get('limit') || '100' const limit = parseInt(searchParams.get('limit') || '100')
// 获取总数 - 使用类型断言
const totalCountResult = await prisma.$queryRaw<[{ total: bigint }]>`
SELECT COUNT(*) as total
FROM edge
WHERE active = true
`
const totalCount = Number(totalCountResult[0]?.total || 0)
// 使用参数化查询防止SQL注入 // 获取分页数据
const result = await prisma.$queryRaw` const result = await prisma.$queryRaw`
SELECT edge.id, edge.macaddr, city, public, isp, single, sole, arch, online SELECT edge.id, edge.macaddr, city, public, isp, single, sole, arch, online
FROM edge FROM edge
LEFT JOIN cityhash ON cityhash.id = edge.city_id LEFT JOIN cityhash ON cityhash.id = edge.city_id
WHERE edge.id > ${threshold} AND active = true WHERE edge.active = true
LIMIT ${limit} ORDER BY edge.id
` LIMIT ${limit} OFFSET ${offset}
return NextResponse.json(safeSerialize(result)) `
return NextResponse.json({
data: safeSerialize(result),
totalCount: totalCount,
currentPage: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(totalCount / limit)
})
} catch (error) { } catch (error) {
console.error('Edge nodes query error:', error) console.error('Edge nodes query error:', error)
return NextResponse.json({ error: '查询边缘节点失败' }, { status: 500 }) return NextResponse.json({ error: '查询边缘节点失败' }, { status: 500 })

View File

@@ -21,27 +21,28 @@ 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,] = useState(0)
const [limit,] = useState(100)
// 分页状态 // 分页状态
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [itemsPerPage, setItemsPerPage] = useState(10) const [itemsPerPage, setItemsPerPage] = useState(100) // 默认100条
const [totalItems, setTotalItems] = useState(0) const [totalItems, setTotalItems] = useState(0)
useEffect(() => { useEffect(() => {
fetchData() fetchData()
}, []) }, [currentPage, itemsPerPage]) // 监听页码和每页数量的变化
const fetchData = async (threshold: number = idThreshold, resultLimit: number = limit) => { const fetchData = async () => {
try { try {
setError(null) setError(null)
setLoading(true) setLoading(true)
const response = await fetch(`/api/stats?type=edge_nodes&threshold=${threshold}&limit=${resultLimit}`)
// 计算偏移量
const offset = (currentPage - 1) * itemsPerPage
const response = await fetch(`/api/stats?type=edge_nodes&offset=${offset}&limit=${itemsPerPage}`)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
const result = await response.json() const result = await response.json()
type ResultEdge = { type ResultEdge = {
id: number id: number
macaddr: string macaddr: string
@@ -54,21 +55,20 @@ export default function Edge() {
online: number online: number
} }
const validatedData = (result as ResultEdge[]).map((item) => ({ const validatedData = (result.data as ResultEdge[]).map((item) => ({
id: validateNumber(item.id), id: validateNumber(item.id),
macaddr: item.macaddr || '', macaddr: item.macaddr || '',
city: item.city || '', city: item.city || '',
public: item.public || '', public: item.public || '',
isp: item.isp || '', isp: item.isp || '',
single: item.single === 1 || item.single === true, single: item.single,
sole: item.sole === 1 || item.sole === true, sole: item.sole,
arch: validateNumber(item.arch), arch: validateNumber(item.arch),
online: validateNumber(item.online) online: validateNumber(item.online)
})) }))
setData(validatedData) setData(validatedData)
setTotalItems(validatedData.length) setTotalItems(result.totalCount || 0)
setCurrentPage(1) // 重置到第一页
} catch (error) { } catch (error) {
console.error('Failed to fetch edge nodes:', error) console.error('Failed to fetch edge nodes:', error)
setError(error instanceof Error ? error.message : '获取边缘节点数据失败') setError(error instanceof Error ? error.message : '获取边缘节点数据失败')
@@ -77,8 +77,66 @@ export default function Edge() {
} }
} }
const formatBoolean = (value: boolean | number): string => { // 多IP节点格式化
return value ? '是' : '否' const formatMultiIP = (value: number | boolean): string => {
if (typeof value === 'number') {
switch (value) {
case 1: return '是'
case 0: return '否'
case -1: return '未知'
default: return `未知 (${value})`
}
}
return value ? '是' : '否'
}
// 独享IP节点格式化
const formatExclusiveIP = (value: number | boolean): string => {
if (typeof value === 'number') {
return value === 1 ? '是' : '否'
}
return value ? '是' : '否'
}
// 多IP节点颜色
const getMultiIPColor = (value: number | boolean): string => {
if (typeof value === 'number') {
switch (value) {
case 1: return 'bg-red-100 text-red-800'
case 0: return 'bg-green-100 text-green-800'
case -1: return 'bg-gray-100 text-gray-800'
default: return 'bg-gray-100 text-gray-800'
}
}
return value ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
}
// 独享IP节点颜色
const getExclusiveIPColor = (value: number | boolean): string => {
if (typeof value === 'number') {
return value === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}
return value ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}
const formatArchType = (arch: number): string => {
switch (arch) {
case 0: return '一代'
case 1: return '二代'
case 2: return 'AMD64'
case 3: return 'x86'
default: return `未知 (${arch})`
}
}
const getArchColor = (arch: number): string => {
switch (arch) {
case 0: return 'bg-blue-100 text-blue-800'
case 1: return 'bg-green-100 text-green-800'
case 2: return 'bg-purple-100 text-purple-800'
case 3: return 'bg-orange-100 text-orange-800'
default: return 'bg-gray-100 text-gray-800'
}
} }
const formatOnlineTime = (seconds: number): string => { const formatOnlineTime = (seconds: number): string => {
@@ -88,11 +146,6 @@ export default function Edge() {
return `${Math.floor(seconds / 86400)}` return `${Math.floor(seconds / 86400)}`
} }
// 计算分页数据
const indexOfLastItem = currentPage * itemsPerPage
const indexOfFirstItem = indexOfLastItem - itemsPerPage
const currentItems = data.slice(indexOfFirstItem, indexOfLastItem)
// 处理页码变化 // 处理页码变化
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
setCurrentPage(page) setCurrentPage(page)
@@ -125,7 +178,7 @@ export default function Edge() {
) )
return ( return (
<div className="bg-white shadow rounded-lg p-6"> <div className="flex bg-white flex-col shadow overflow-hidden rounded-lg p-6">
{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>
@@ -138,7 +191,6 @@ export default function Edge() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-gray-50"> <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">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"></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>
@@ -150,9 +202,8 @@ export default function Edge() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{currentItems.map((item, index) => ( {data.map((item, index) => (
<TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}> <TableRow key={item.id} 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 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 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 font-mono text-green-600">{item.public}</TableCell>
@@ -164,23 +215,23 @@ export default function Edge() {
'bg-gray-100 text-gray-800' 'bg-gray-100 text-gray-800'
}`}> }`}>
{item.isp} {item.isp}
</span></TableCell> </span>
</TableCell>
<TableCell className="px-4 py-3 text-sm text-center"> <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 ${ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getMultiIPColor(item.single)}`}>
item.single ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' {formatMultiIP(item.single)}
}`}> </span>
{formatBoolean(item.single)} </TableCell>
</span></TableCell>
<TableCell className="px-4 py-3 text-sm text-center"> <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 ${ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getExclusiveIPColor(item.sole)}`}>
item.sole ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' {formatExclusiveIP(item.sole)}
}`}> </span>
{formatBoolean(item.sole)} </TableCell>
</span></TableCell>
<TableCell className="px-4 py-3 text-sm text-center"> <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"> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getArchColor(item.arch)}`}>
{item.arch} {formatArchType(item.arch)}
</span></TableCell> </span>
</TableCell>
<TableCell className="px-4 py-3 text-sm text-gray-700">{formatOnlineTime(item.online)}</TableCell> <TableCell className="px-4 py-3 text-sm text-gray-700">{formatOnlineTime(item.online)}</TableCell>
</TableRow> </TableRow>
))} ))}
@@ -188,7 +239,8 @@ export default function Edge() {
</Table> </Table>
</div> </div>
</div> </div>
{/* 使用 Pagination 组件 */}
{/* 分页 */}
<Pagination <Pagination
page={currentPage} page={currentPage}
size={itemsPerPage} size={itemsPerPage}

View File

@@ -98,24 +98,21 @@ function GatewayConfigContent() {
} }
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => { const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
// 0是正常1是更新正常绿+ 更新(红)
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 === 0 value === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{value === 0 ? trueText : falseText} {value === 0 ? trueText : falseText}
</span> </span>
) )
} }
const getOnlineStatus = (isonline: number) => { const getOnlineStatus = (isonline: number) => {
// 0是空闲1是在用在用+ 空闲(绿)
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<div className={`w-2 h-2 rounded-full mr-2 ${ <div className={`${isonline === 0 ? 'bg-green-500' : 'bg-red-500'}`} />
isonline === 1 ? 'bg-green-500' : 'bg-red-500' {getStatusBadge(isonline, '空闲', '在用')}
}`} />
{getStatusBadge(isonline, '在线', '离线')}
</div> </div>
) )
} }
@@ -189,7 +186,7 @@ function GatewayConfigContent() {
<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">IP地址</TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">线</TableHead> <TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -213,7 +210,7 @@ function GatewayConfigContent() {
<div className="font-mono text-sm text-purple-600">{item.inner_ip}</div> <div className="font-mono text-sm text-purple-600">{item.inner_ip}</div>
</TableCell> </TableCell>
<TableCell className="px-6 whitespace-nowrap"> <TableCell className="px-6 whitespace-nowrap">
<div className="font-mono text-sm text-green-600">{getStatusBadge(item.ischange, '正常', '更新')}</div> <div className="font-mono text-sm text-green-600">{getStatusBadge(item.ischange, '正常', '更新')}</div>
</TableCell> </TableCell>
<TableCell className="px-6 whitespace-nowrap"> <TableCell className="px-6 whitespace-nowrap">
<div className="font-mono text-sm text-purple-600">{getOnlineStatus(item.isonline)}</div> <div className="font-mono text-sm text-purple-600">{getOnlineStatus(item.isonline)}</div>

View File

@@ -42,7 +42,7 @@ export default function Gatewayinfo() {
const form = useForm<FilterSchema>({ const form = useForm<FilterSchema>({
resolver: zodResolver(filterSchema), resolver: zodResolver(filterSchema),
defaultValues: { defaultValues: {
status: 'all', status: '1',
}, },
}) })
@@ -53,21 +53,21 @@ export default function Gatewayinfo() {
fetchData() fetchData()
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!data.length) return if (!data.length) return
if (statusFilter === 'all') { if (statusFilter === 'all') {
setFilteredData(data)
} else {
const enableValue = parseInt(statusFilter)
// 添加 NaN 检查
if (isNaN(enableValue)) {
setFilteredData(data) setFilteredData(data)
} else { } else {
const filterValue = statusFilter || 'all' setFilteredData(data.filter(item => item.enable === enableValue))
if (filterValue === 'all') {
setFilteredData(data)
} else {
const enableValue = parseInt(filterValue)
setFilteredData(data.filter(item => item.enable === enableValue))
}
} }
}, [data, statusFilter]) }
}, [data, statusFilter])
const fetchData = async () => { const fetchData = async () => {
try { try {
@@ -136,7 +136,7 @@ export default function Gatewayinfo() {
<Select <Select
value={field.value} value={field.value}
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue="all" defaultValue="1"
> >
<SelectTrigger className="h-9 w-36"> <SelectTrigger className="h-9 w-36">
<SelectValue placeholder="选择状态" /> <SelectValue placeholder="选择状态" />