调整整体页面布局
This commit is contained in:
@@ -12,6 +12,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'
|
||||
import { Page } from '@/components/page'
|
||||
|
||||
const filterSchema = z.object({
|
||||
timeFilter: z.string(),
|
||||
@@ -139,9 +140,8 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
if (error) return <ErrorCard title="节点分配状态" error={error} onRetry={fetchData} />
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full bg-white shadow p-6 overflow-hidden">
|
||||
<h2 className="text-lg font-semibold mb-4">节点分配状态</h2>
|
||||
|
||||
<Page>
|
||||
<h2 className="flex-none text-lg font-semibold mb-4">节点分配状态</h2>
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex items-center gap-4">
|
||||
@@ -173,69 +173,48 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 overflow-hidden">
|
||||
<div className="flex w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead
|
||||
className="px-4 py-2 text-left cursor-pointer hover:bg-blue-50 transition-colors"
|
||||
onClick={() => handleSort('city')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>城市</span>
|
||||
{renderSortIcon('city')}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="px-4 py-2 text-left cursor-pointer hover:bg-blue-50 transition-colors"
|
||||
onClick={() => handleSort('count')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>可用IP量</span>
|
||||
{renderSortIcon('count')}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="px-4 py-2 text-left cursor-pointer hover:bg-blue-50 transition-colors"
|
||||
onClick={() => handleSort('assigned')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>分配IP量</span>
|
||||
{renderSortIcon('assigned')}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="px-4 py-2 text-left cursor-pointer hover:bg-blue-50 transition-colors"
|
||||
onClick={() => handleSort('overage')}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>超额量</span>
|
||||
{renderSortIcon('overage')}
|
||||
</div>
|
||||
</TableHead>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>城市</TableHead>
|
||||
<TableHead onClick={() => handleSort('count')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>可用IP量</span>
|
||||
{renderSortIcon('count')}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead onClick={() => handleSort('assigned')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>分配IP量</span>
|
||||
{renderSortIcon('assigned')}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead onClick={() => handleSort('overage')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>超额量</span>
|
||||
{renderSortIcon('overage')}
|
||||
</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedData.map((item, index) => {
|
||||
const overage = calculateOverage(Number(item.assigned), Number(item.count))
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{item.city}</TableCell>
|
||||
<TableCell>{item.count}</TableCell>
|
||||
<TableCell>{item.assigned}</TableCell>
|
||||
<TableCell>
|
||||
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
|
||||
{overage}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedData.map((item, index) => {
|
||||
const overage = calculateOverage(Number(item.assigned), Number(item.count))
|
||||
return (
|
||||
<TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<TableCell className="px-4 py-2">{item.city}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.count}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.assigned}</TableCell>
|
||||
<TableCell className="px-4 py-2">
|
||||
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
|
||||
{overage}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
import { getCityNodeCount, type CityNode } from '@/actions/stats'
|
||||
import { Page } from '@/components/page'
|
||||
|
||||
export default function CityNodeStats() {
|
||||
const [data, setData] = useState<CityNode[]>([])
|
||||
@@ -38,7 +39,7 @@ export default function CityNodeStats() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full bg-white p-6 overflow-hidden">
|
||||
<Page>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold">城市节点数量分布</h2>
|
||||
<span className="text-sm text-gray-500">
|
||||
@@ -46,39 +47,28 @@ export default function CityNodeStats() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex overflow-hidden ">
|
||||
<div className="flex w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">城市</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">节点数量</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">Hash</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">标签</TableHead>
|
||||
<TableHead className="px-4 py-2 text-left font-medium text-gray-600">轮换顺位</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
||||
>
|
||||
<TableCell className="px-4 py-2">{item.city}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.count}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.hash}</TableCell>
|
||||
<TableCell className="px-4 py-2">
|
||||
<span className="bg-gray-100 px-2 py-1 rounded text-gray-700">
|
||||
{item.label || '无标签'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.offset}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>城市</TableHead>
|
||||
<TableHead>节点数量</TableHead>
|
||||
<TableHead>Hash</TableHead>
|
||||
<TableHead>标签</TableHead>
|
||||
<TableHead>轮换顺位</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{item.city}</TableCell>
|
||||
<TableCell>{item.count}</TableCell>
|
||||
<TableCell>{item.hash}</TableCell>
|
||||
<TableCell>{item.label}</TableCell>
|
||||
<TableCell>{item.offset}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import { logout } from '@/actions/auth'
|
||||
import { LogOut } from 'lucide-react'
|
||||
import { LogOutIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function DashboardLayout({
|
||||
@@ -37,47 +37,45 @@ export default function DashboardLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 w-screen h-screen flex flex-col overflow-hidden">
|
||||
<div className="w-screen h-screen flex flex-col">
|
||||
|
||||
{/* 顶部导航栏 */}
|
||||
<nav className="bg-white flex-none border-b h-16 shadow-sm">
|
||||
<header className="flex-none basis-16 border-b">
|
||||
<div className="px-4 sm:px-6">
|
||||
<div className="flex justify-between h-16 items-center">
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-xl font-bold text-gray-900">网络节点管理系统</h1>
|
||||
</div>
|
||||
|
||||
{/* 简化的退出按钮 */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
disabled={isLoading}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<LogOutIcon className="h-4 w-4" />
|
||||
<span>{isLoading ? '退出中...' : '退出登录'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<div className="flex-auto overflow-hidden flex">
|
||||
{/* 侧边栏 */}
|
||||
<div className="w-64 bg-gray-100 border-r border-gray-200 flex flex-col">
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
<NavbarItem href="/gatewayinfo" active={isActive('/gatewayinfo')}>网关信息</NavbarItem>
|
||||
<NavbarItem href="/gatewayConfig" active={isActive('/gatewayConfig')}>网关配置</NavbarItem>
|
||||
<NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}>网关查询</NavbarItem>
|
||||
<NavbarItem href="/cityNodeStats" active={isActive('/cityNodeStats')}>城市信息</NavbarItem>
|
||||
<NavbarItem href="/allocationStatus" active={isActive('/allocationStatus')}>分配状态</NavbarItem>
|
||||
<NavbarItem href="/edge" active={isActive('/edge')}>节点信息</NavbarItem>
|
||||
<NavbarItem href="/settings" active={isActive('/settings')}>设置</NavbarItem>
|
||||
</nav>
|
||||
</div>
|
||||
<nav className="flex-none basis-64 p-4 space-y-2 border-r flex flex-col">
|
||||
<NavbarItem href="/gatewayinfo" active={isActive('/gatewayinfo')}>网关信息</NavbarItem>
|
||||
<NavbarItem href="/gatewayConfig" active={isActive('/gatewayConfig')}>网关配置</NavbarItem>
|
||||
<NavbarItem href="/gatewayMonitor" active={isActive('/gatewayMonitor')}>网关查询</NavbarItem>
|
||||
<NavbarItem href="/cityNodeStats" active={isActive('/cityNodeStats')}>城市信息</NavbarItem>
|
||||
<NavbarItem href="/allocationStatus" active={isActive('/allocationStatus')}>分配状态</NavbarItem>
|
||||
<NavbarItem href="/edge" active={isActive('/edge')}>节点信息</NavbarItem>
|
||||
<NavbarItem href="/settings" active={isActive('/settings')}>设置</NavbarItem>
|
||||
</nav>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="flex flex-3 w-full overflow-hidden">
|
||||
<main className="flex-auto overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
11
src/components/page.tsx
Normal file
11
src/components/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ComponentProps, ReactNode } from 'react'
|
||||
|
||||
export function Page(props: {
|
||||
children: ReactNode
|
||||
} & ComponentProps<'div'>) {
|
||||
return (
|
||||
<div className="w-full h-full p-6 flex flex-col">
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,11 +8,11 @@ function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
className="rounded-md border overflow-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn('w-full caption-bottom text-sm ', className)}
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn('[&_tr]:border-b sticky top-0', className)}
|
||||
className={cn('sticky top-0 bg-gray-50', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors h-10',
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b border-border/50 transition-colors',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -71,6 +71,7 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
'text-sm text-gray-500',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user