Compare commits
2 Commits
826d8fc4c3
...
0288855002
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0288855002 | ||
|
|
fd8fede301 |
@@ -54,17 +54,76 @@ async function getGatewayInfo() {
|
|||||||
async function getGatewayConfig(request: NextRequest) {
|
async function getGatewayConfig(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
const macAddress = searchParams.get('mac') || '000C29DF1647'
|
const macAddress = searchParams.get('mac') || ''
|
||||||
|
const offset = parseInt(searchParams.get('offset') || '0')
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '100')
|
||||||
|
|
||||||
// 使用参数化查询防止SQL注入
|
// 定义类型接口
|
||||||
const result = await prisma.$queryRaw`
|
interface GatewayRecord {
|
||||||
SELECT edge, city, user, public, inner_ip, ischange, isonline
|
edge: string | null;
|
||||||
|
city: string | null;
|
||||||
|
user: string | null;
|
||||||
|
public: string | null;
|
||||||
|
inner_ip: string | null;
|
||||||
|
ischange: boolean | number | null;
|
||||||
|
isonline: boolean | number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
let totalCountQuery = ''
|
||||||
|
let totalCountParams: (string | number)[] = []
|
||||||
|
|
||||||
|
if (macAddress) {
|
||||||
|
totalCountQuery = `
|
||||||
|
SELECT COUNT(*) as total
|
||||||
FROM gateway
|
FROM gateway
|
||||||
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
|
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
|
||||||
LEFT JOIN edge ON edge.macaddr = gateway.edge
|
LEFT JOIN edge ON edge.macaddr = gateway.edge
|
||||||
WHERE gateway.macaddr = ${macAddress};
|
WHERE gateway.macaddr = ?
|
||||||
`
|
`
|
||||||
return NextResponse.json(safeSerialize(result))
|
totalCountParams = [macAddress]
|
||||||
|
} else {
|
||||||
|
totalCountQuery = `
|
||||||
|
SELECT COUNT(*) as total
|
||||||
|
FROM gateway
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
|
||||||
|
totalCountQuery,
|
||||||
|
...totalCountParams
|
||||||
|
)
|
||||||
|
const totalCount = Number(totalCountResult[0]?.total || 0)
|
||||||
|
|
||||||
|
// 获取分页数据
|
||||||
|
let query = `
|
||||||
|
select edge, city, user, public, inner_ip, ischange, isonline
|
||||||
|
from
|
||||||
|
gateway
|
||||||
|
left join cityhash
|
||||||
|
on cityhash.hash = gateway.cityhash
|
||||||
|
left join edge
|
||||||
|
on edge.macaddr = gateway.edge
|
||||||
|
`
|
||||||
|
let params: (string | number)[] = []
|
||||||
|
|
||||||
|
if (macAddress) {
|
||||||
|
query += ' WHERE gateway.macaddr = ?'
|
||||||
|
params = [macAddress]
|
||||||
|
} else {
|
||||||
|
query += ' LIMIT ? OFFSET ?'
|
||||||
|
params.push(limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指定返回类型
|
||||||
|
const result = await prisma.$queryRawUnsafe<GatewayRecord[]>(query, ...params)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
data: safeSerialize(result),
|
||||||
|
totalCount: totalCount,
|
||||||
|
currentPage: Math.floor(offset / limit) + 1,
|
||||||
|
totalPages: Math.ceil(totalCount / limit)
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gateway config query error:', error)
|
console.error('Gateway config query error:', error)
|
||||||
return NextResponse.json({ error: '查询网关配置失败' }, { status: 500 })
|
return NextResponse.json({ error: '查询网关配置失败' }, { status: 500 })
|
||||||
@@ -91,11 +150,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 +229,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 })
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex gap-6 overflow-hidden'>
|
<div className='flex gap-6 overflow-hidden'>
|
||||||
<div className="flex flex-3 w-full">
|
<div className="flex w-full">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-gray-50">
|
<TableRow className="bg-gray-50">
|
||||||
@@ -159,17 +159,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col gap-4 mb-6">
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg">
|
|
||||||
<div className="text-2xl font-bold text-blue-600">{formatNumber(data.length)}</div>
|
|
||||||
<div className="text-sm text-blue-800">监控城市数量</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-red-50 p-4 rounded-lg">
|
|
||||||
<div className="text-2xl font-bold text-red-600">{formatNumber(problematicCities.length)}</div>
|
|
||||||
<div className="text-sm text-red-800">需关注城市</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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节点格式化
|
||||||
|
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 ? '是' : '否'
|
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}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
import { useEffect, useState, Suspense } from 'react'
|
import { useEffect, useState, Suspense } from 'react'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||||
|
import { Pagination } from '@/components/ui/pagination'
|
||||||
|
|
||||||
interface GatewayConfig {
|
interface GatewayConfig {
|
||||||
id: number
|
|
||||||
city: string
|
city: string
|
||||||
edge: string
|
edge: string
|
||||||
user: string
|
user: string
|
||||||
@@ -14,79 +14,83 @@ interface GatewayConfig {
|
|||||||
isonline: number
|
isonline: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiResponse {
|
||||||
|
data: GatewayConfig[]
|
||||||
|
totalCount: number
|
||||||
|
currentPage: number
|
||||||
|
totalPages: number
|
||||||
|
}
|
||||||
|
|
||||||
function GatewayConfigContent() {
|
function GatewayConfigContent() {
|
||||||
const [data, setData] = useState<GatewayConfig[]>([])
|
const [data, setData] = useState<GatewayConfig[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [macAddress, setMacAddress] = useState('')
|
const [macAddress, setMacAddress] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [success, setSuccess] = useState('')
|
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
// 监听URL的mac参数变化:同步到输入框并触发查询
|
// 分页状态
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(100)
|
||||||
|
const [totalItems, setTotalItems] = useState(0)
|
||||||
|
|
||||||
|
// 判断是否为MAC地址查询(用于控制分页显示)
|
||||||
|
const isMacQuery = !!macAddress
|
||||||
|
|
||||||
|
// 监听URL的mac参数变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlMac = searchParams.get('mac')
|
const urlMac = searchParams.get('mac')
|
||||||
if (urlMac) {
|
if (urlMac) {
|
||||||
setMacAddress(urlMac)
|
setMacAddress(urlMac)
|
||||||
fetchData(urlMac)
|
setCurrentPage(1) // 重置到第一页
|
||||||
|
fetchData(urlMac, 1, itemsPerPage)
|
||||||
} else {
|
} else {
|
||||||
// 如果没有mac参数,显示所有网关配置
|
|
||||||
setMacAddress('')
|
setMacAddress('')
|
||||||
fetchData('')
|
setCurrentPage(1) // 重置到第一页
|
||||||
|
fetchData('', 1, itemsPerPage)
|
||||||
}
|
}
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
const fetchData = async (mac: string) => {
|
const fetchData = async (mac: string, page: number = 1, limit: number = itemsPerPage) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
setSuccess('')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 构建API URL - 如果有MAC地址则添加参数,否则获取全部
|
// 计算偏移量
|
||||||
const apiUrl = mac.trim()
|
const offset = (page - 1) * limit
|
||||||
? `/api/stats?type=gateway_config&mac=${encodeURIComponent(mac)}`
|
|
||||||
: `/api/stats?type=gateway_config`
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl)
|
// 构建API URL
|
||||||
const result = await response.json()
|
let apiUrl = `/api/stats?type=gateway_config&offset=${offset}&limit=${limit}`
|
||||||
|
if (mac.trim()) {
|
||||||
if (!response.ok) {
|
apiUrl += `&mac=${encodeURIComponent(mac)}`
|
||||||
throw new Error(result.error || '查询失败')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP错误! 状态: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: ApiResponse = await response.json()
|
||||||
|
|
||||||
// 检查返回的数据是否有效
|
// 检查返回的数据是否有效
|
||||||
if (!result || result.length === 0) {
|
if (!result.data || result.data.length === 0) {
|
||||||
if (mac.trim()) {
|
if (mac.trim()) {
|
||||||
setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
|
setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
|
||||||
} else {
|
} else {
|
||||||
setError('未找到任何网关配置信息')
|
setError('未找到任何网关配置信息')
|
||||||
}
|
}
|
||||||
setData([])
|
setData([])
|
||||||
|
setTotalItems(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedData = result.map((item: {
|
setData(result.data)
|
||||||
city: string
|
|
||||||
edge: string
|
|
||||||
user: string
|
|
||||||
public: string
|
|
||||||
inner_ip: string
|
|
||||||
ischange: number
|
|
||||||
isonline: number
|
|
||||||
}) => ({
|
|
||||||
city: item.city,
|
|
||||||
edge: item.edge,
|
|
||||||
user: item.user,
|
|
||||||
public: item.public,
|
|
||||||
inner_ip: item.inner_ip,
|
|
||||||
ischange: item.ischange,
|
|
||||||
isonline: item.isonline,
|
|
||||||
}))
|
|
||||||
|
|
||||||
setData(validatedData)
|
setTotalItems(result.totalCount || 0)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch gateway config:', error)
|
console.error('获取网关配置失败:', error)
|
||||||
setError(error instanceof Error ? error.message : '获取网关配置失败')
|
setError(error instanceof Error ? error.message : '获取网关配置失败')
|
||||||
setData([])
|
setData([])
|
||||||
|
setTotalItems(0)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -94,28 +98,39 @@ function GatewayConfigContent() {
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
fetchData(macAddress)
|
setCurrentPage(1) // 重置到第一页
|
||||||
|
fetchData(macAddress, 1, itemsPerPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理页码变化
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page)
|
||||||
|
fetchData(macAddress, page, itemsPerPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每页显示数量变化
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
setItemsPerPage(size)
|
||||||
|
setCurrentPage(1)
|
||||||
|
fetchData(macAddress, 1, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
|
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
|
||||||
|
// 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -131,7 +146,7 @@ function GatewayConfigContent() {
|
|||||||
|
|
||||||
{/* 查询表单 */}
|
{/* 查询表单 */}
|
||||||
<form onSubmit={handleSubmit} className="mb-6">
|
<form onSubmit={handleSubmit} className="mb-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={macAddress}
|
value={macAddress}
|
||||||
@@ -157,17 +172,6 @@ function GatewayConfigContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{success && !error && (
|
|
||||||
<div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-md">
|
|
||||||
<div className="flex items-center text-green-800">
|
|
||||||
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -177,46 +181,43 @@ function GatewayConfigContent() {
|
|||||||
</div>
|
</div>
|
||||||
) : data.length > 0 ? (
|
) : data.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{/* 详细表格 */}
|
|
||||||
<div className='flex gap-6 overflow-hidden'>
|
<div className='flex gap-6 overflow-hidden'>
|
||||||
<div className="flex-3 w-full flex">
|
<div className="flex-3 w-full flex">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-gray-50 TableRow ">
|
<TableRow className="bg-gray-50">
|
||||||
<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">内部账号</TableHead>
|
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</TableHead>
|
||||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</TableHead>
|
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">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>
|
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">在用状态</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((item, index) => (
|
{data.map((item, index) => (
|
||||||
<TableRow
|
<TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||||
key={index}
|
<TableCell>
|
||||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50 '}
|
|
||||||
>
|
|
||||||
<TableCell className="hover:bg-gray-50 transition-colors">
|
|
||||||
<div className="font-mono text-sm text-blue-600 font-medium">{item.edge}</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="px-6 whitespace-nowrap">
|
|
||||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
|
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
|
||||||
{item.city}
|
{item.city}
|
||||||
</span></TableCell>
|
</span>
|
||||||
<TableCell className="px-6 whitespace-nowrap text-sm text-gray-900">{item.user}</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-6 whitespace-nowrap">
|
<TableCell>
|
||||||
|
<div className="font-mono text-sm text-blue-600 font-medium">{item.edge}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
<div className="font-mono text-sm text-green-600">{item.public}</div>
|
<div className="font-mono text-sm text-green-600">{item.public}</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-6 whitespace-nowrap">
|
<TableCell className="text-sm text-gray-900">{item.user}</TableCell>
|
||||||
|
<TableCell>
|
||||||
<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>
|
||||||
<div className="font-mono text-sm text-green-600">{getStatusBadge(item.ischange, '正常', '需更新')}</div>
|
{getStatusBadge(item.ischange, '正常', '更新')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-6 whitespace-nowrap">
|
<TableCell>
|
||||||
<div className="font-mono text-sm text-purple-600">{getOnlineStatus(item.isonline)}</div>
|
{getOnlineStatus(item.isonline)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@@ -225,32 +226,41 @@ function GatewayConfigContent() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col gap-4 mb-6">
|
<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="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-2xl font-bold text-blue-600">{totalItems}</div>
|
||||||
<div className="text-sm text-blue-800">网关数量</div>
|
<div className="text-sm text-blue-800">配置条数</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-50 p-4 rounded-lg border border-green-100">
|
<div className="bg-green-50 p-4 rounded-lg border border-green-100">
|
||||||
<div className="text-2xl font-bold text-green-600">
|
<div className="text-2xl font-bold text-green-600">
|
||||||
{data.filter(item => item.isonline === 1).length}
|
{data.filter(item => item.isonline === 1).length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-green-800">在线网关</div>
|
<div className="text-sm text-green-800">在用节点</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-orange-50 p-4 rounded-lg border border-orange-100">
|
<div className="bg-orange-50 p-4 rounded-lg border border-orange-100">
|
||||||
<div className="text-2xl font-bold text-orange-600">
|
<div className="text-2xl font-bold text-orange-600">
|
||||||
{data.filter(item => item.ischange === 1).length}
|
{data.filter(item => item.ischange === 1).length}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-orange-800">已更新配置</div>
|
<div className="text-sm text-orange-800">需要更新</div>
|
||||||
</div>
|
|
||||||
<div className="bg-purple-50 p-4 rounded-lg border border-purple-100">
|
|
||||||
<div className="text-2xl font-bold text-purple-600">
|
|
||||||
{new Set(data.map(item => item.city)).size}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-purple-800">覆盖城市</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 分页组件 - 仅在非MAC查询时显示 */}
|
||||||
|
{!isMacQuery && (
|
||||||
|
<Pagination
|
||||||
|
page={currentPage}
|
||||||
|
size={itemsPerPage}
|
||||||
|
total={totalItems}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onSizeChange={handleSizeChange}
|
||||||
|
className="mt-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-400 text-4xl mb-4">📋</div>
|
||||||
|
<p className="text-gray-600">暂无网关配置数据</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
setFilteredData(data)
|
||||||
} else {
|
} else {
|
||||||
const filterValue = statusFilter || 'all'
|
const enableValue = parseInt(statusFilter)
|
||||||
if (filterValue === 'all') {
|
// 添加 NaN 检查
|
||||||
|
if (isNaN(enableValue)) {
|
||||||
setFilteredData(data)
|
setFilteredData(data)
|
||||||
} else {
|
} else {
|
||||||
const enableValue = parseInt(filterValue)
|
|
||||||
setFilteredData(data.filter(item => item.enable === enableValue))
|
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="选择状态" />
|
||||||
@@ -213,12 +213,6 @@ export default function Gatewayinfo() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-red-800">禁用网关</div>
|
<div className="text-sm text-red-800">禁用网关</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-purple-50 p-4 rounded-lg">
|
|
||||||
<div className="text-2xl font-bold text-purple-600">
|
|
||||||
{new Set(data.map(item => item.setid)).size}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-purple-800">配置版本数</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user