初始化管理后台仓库
This commit is contained in:
434
src/app/(root)/(dashboard)/page.tsx
Normal file
434
src/app/(root)/(dashboard)/page.tsx
Normal file
@@ -0,0 +1,434 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export type DashboardPageProps = {}
|
||||
|
||||
export default function DashboardPage(props: DashboardPageProps) {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* 欢迎区域 - 全宽 */}
|
||||
<div className="bg-white border border-gray-200 rounded-md">
|
||||
<div className="flex items-center justify-between p-5">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-800">IP代理管理控制台</h1>
|
||||
<p className="text-gray-500 mt-1">上次更新: {new Date().toLocaleString('zh-CN')}</p>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 border border-gray-200 rounded-md hover:bg-gray-200 transition-colors text-sm font-medium">
|
||||
查看使用报告
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-600 text-white border border-blue-700 rounded-md hover:bg-blue-700 transition-colors text-sm font-medium">
|
||||
添加新代理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主体内容 - 双栏布局 */}
|
||||
<div className="flex flex-col lg:flex-row space-y-5 lg:space-y-0 lg:space-x-5">
|
||||
{/* 左侧栏 - 占比较大 */}
|
||||
<div className="w-full lg:w-8/12 space-y-5">
|
||||
{/* 代理资源统计卡片组 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-5">
|
||||
<DataCard
|
||||
title="在线代理"
|
||||
value="14,283"
|
||||
change="+12.5%"
|
||||
isIncrease={true}
|
||||
icon={
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
<DataCard
|
||||
title="总请求数"
|
||||
value="851,492"
|
||||
change="+8.2%"
|
||||
isIncrease={true}
|
||||
icon={
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
<DataCard
|
||||
title="成功率"
|
||||
value="98.5%"
|
||||
change="+2.4%"
|
||||
isIncrease={true}
|
||||
icon={
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
<DataCard
|
||||
title="平均响应时间"
|
||||
value="0.82s"
|
||||
change="-12.3%"
|
||||
isIncrease={true}
|
||||
icon={
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 代理使用图表 */}
|
||||
<div className="bg-white border border-gray-200 rounded-md">
|
||||
<div className="flex justify-between items-center p-5 border-b border-gray-200">
|
||||
<h2 className="font-bold text-gray-800">代理使用趋势</h2>
|
||||
<div className="flex items-center space-x-2">
|
||||
<select className="text-sm border border-gray-200 rounded-md px-3 py-1 bg-white">
|
||||
<option>今日</option>
|
||||
<option>本周</option>
|
||||
<option>本月</option>
|
||||
<option>全年</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-80 bg-white p-5 flex items-center justify-center text-gray-400 border-b border-gray-200">
|
||||
请求量与响应时间统计图表
|
||||
</div>
|
||||
<div className="flex justify-between p-5 text-sm">
|
||||
<div className="text-gray-500">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-blue-500 mr-1"></span>
|
||||
请求数量
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-green-500 mr-1"></span>
|
||||
成功率
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-orange-500 mr-1"></span>
|
||||
响应时间
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IP代理列表 */}
|
||||
<div className="bg-white border border-gray-200 rounded-md">
|
||||
<div className="p-5 border-b border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="font-bold text-gray-800">活跃代理IP</h2>
|
||||
<Link href="/proxies" className="text-blue-600 text-sm hover:underline">查看全部</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr
|
||||
className="bg-gray-50 text-left text-xs text-gray-500 uppercase tracking-wider border-b border-gray-200">
|
||||
<th className="px-5 py-3">IP地址</th>
|
||||
<th className="px-5 py-3">位置</th>
|
||||
<th className="px-5 py-3">状态</th>
|
||||
<th className="px-5 py-3">请求次数</th>
|
||||
<th className="px-5 py-3">成功率</th>
|
||||
<th className="px-5 py-3">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{[1, 2, 3, 4, 5].map((item) => (
|
||||
<ContentRow
|
||||
key={item}
|
||||
ip={`192.168.${item}.${item * 10}`}
|
||||
location={item % 2 === 0 ? '中国' : '美国'}
|
||||
status={item % 3 === 0 ? 'error' : item % 2 === 0 ? 'warning' : 'active'}
|
||||
requests={Math.floor(Math.random() * 10000)}
|
||||
successRate={`${95 + Math.floor(Math.random() * 5)}%`}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 border-t border-gray-200 flex justify-end">
|
||||
<button className="px-3 py-1 bg-white border border-gray-200 rounded-md text-sm mr-2">上一页</button>
|
||||
<button className="px-3 py-1 bg-blue-600 text-white rounded-md text-sm">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧栏 - 占比较小 */}
|
||||
<div className="w-full lg:w-4/12 space-y-5">
|
||||
{/* 代理资源分布 */}
|
||||
<div className="bg-white border border-gray-200 rounded-md">
|
||||
<div className="p-5 border-b border-gray-200">
|
||||
<h2 className="font-bold text-gray-800">资源分布</h2>
|
||||
</div>
|
||||
<div className="h-64 bg-white p-5 flex items-center justify-center text-gray-400">
|
||||
IP地区分布饼图
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 p-5 border-t border-gray-200">
|
||||
<div className="flex justify-between p-2 bg-gray-50 rounded-md border border-gray-200">
|
||||
<span className="text-xs text-gray-600">中国</span>
|
||||
<span className="text-xs font-medium">42%</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 bg-gray-50 rounded-md border border-gray-200">
|
||||
<span className="text-xs text-gray-600">美国</span>
|
||||
<span className="text-xs font-medium">28%</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 bg-gray-50 rounded-md border border-gray-200">
|
||||
<span className="text-xs text-gray-600">欧洲</span>
|
||||
<span className="text-xs font-medium">16%</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-2 bg-gray-50 rounded-md border border-gray-200">
|
||||
<span className="text-xs text-gray-600">其他</span>
|
||||
<span className="text-xs font-medium">14%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统告警 */}
|
||||
<div className="bg-white border border-gray-200 rounded-md">
|
||||
<div className="p-5 border-b border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="font-bold text-gray-800">告警通知</h2>
|
||||
<span className="px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">3 个新告警</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 space-y-3">
|
||||
<AlertItem
|
||||
title="IP不足告警"
|
||||
severity="high"
|
||||
time="10分钟前"
|
||||
message="特定地区(美国加州)代理IP资源不足,影响用户请求。"
|
||||
/>
|
||||
<AlertItem
|
||||
title="响应延迟"
|
||||
severity="medium"
|
||||
time="30分钟前"
|
||||
message="欧洲区域代理响应时间超过阈值(1.5s),请检查网络状况。"
|
||||
/>
|
||||
<AlertItem
|
||||
title="异常请求"
|
||||
severity="low"
|
||||
time="2小时前"
|
||||
message="检测到异常请求模式,可能存在爬虫攻击行为。"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
<button
|
||||
className="w-full py-2 bg-gray-100 text-gray-600 border border-gray-200 rounded-md text-sm hover:bg-gray-200 transition-colors">
|
||||
查看全部告警
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 系统状态 */}
|
||||
<div className="bg-white border border-gray-200 rounded-md">
|
||||
<div className="p-5 border-b border-gray-200">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="font-bold text-gray-800">系统状态</h2>
|
||||
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full font-medium">运行正常</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 space-y-3">
|
||||
<StatusBar
|
||||
title="代理服务器负载"
|
||||
value={28}
|
||||
status="normal"
|
||||
/>
|
||||
<StatusBar
|
||||
title="带宽使用率"
|
||||
value={65}
|
||||
status="normal"
|
||||
/>
|
||||
<StatusBar
|
||||
title="存储空间"
|
||||
value={82}
|
||||
status="warning"
|
||||
/>
|
||||
<StatusBar
|
||||
title="API请求队列"
|
||||
value={45}
|
||||
status="normal"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
<Link
|
||||
href="/system/status"
|
||||
className="block w-full py-2 text-center bg-gray-100 text-gray-600 border border-gray-200 rounded-md text-sm hover:bg-gray-200 transition-colors">
|
||||
查看详细状态
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 数据卡片组件 - 显示关键指标
|
||||
function DataCard({
|
||||
title,
|
||||
value,
|
||||
change,
|
||||
isIncrease,
|
||||
icon,
|
||||
}: {
|
||||
title: string;
|
||||
value: string;
|
||||
change: string;
|
||||
isIncrease: boolean;
|
||||
icon: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-md p-5">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-sm text-gray-500">{title}</h3>
|
||||
<p className="text-xl font-bold mt-1 text-gray-800">{value}</p>
|
||||
<div className="flex items-center mt-2">
|
||||
<span className={`text-xs font-medium ${isIncrease ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{change}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 ml-1">相比上周期</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`p-3 rounded-md ${isIncrease ? 'bg-blue-50 text-blue-600' : 'bg-orange-50 text-orange-500'} border ${isIncrease ? 'border-blue-100' : 'border-orange-100'}`}>
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 代理IP内容行组件
|
||||
function ContentRow({
|
||||
ip,
|
||||
location,
|
||||
status,
|
||||
requests,
|
||||
successRate,
|
||||
}: {
|
||||
ip: string;
|
||||
location: string;
|
||||
status: 'active' | 'warning' | 'error';
|
||||
requests: number;
|
||||
successRate: string;
|
||||
}) {
|
||||
const statusConfig = {
|
||||
active: {color: 'bg-green-100 text-green-800 border-green-200', label: '在线'},
|
||||
warning: {color: 'bg-yellow-100 text-yellow-800 border-yellow-200', label: '不稳定'},
|
||||
error: {color: 'bg-red-100 text-red-800 border-red-200', label: '离线'},
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="hover:bg-gray-50">
|
||||
<td className="px-5 py-4">
|
||||
<div className="font-mono text-sm">{ip}</div>
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="text-sm text-gray-700">{location}</div>
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<span className={`px-2 py-1 text-xs rounded-md ${statusConfig[status].color} border`}>
|
||||
{statusConfig[status].label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm text-gray-700">
|
||||
{requests.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm text-gray-700">
|
||||
{successRate}
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex space-x-2">
|
||||
<button className="p-1 border border-gray-200 rounded-md hover:bg-gray-50">
|
||||
<svg className="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button className="p-1 border border-gray-200 rounded-md hover:bg-gray-50">
|
||||
<svg className="h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button className="p-1 border border-red-200 rounded-md hover:bg-red-50">
|
||||
<svg className="h-4 w-4 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
// 告警通知组件
|
||||
function AlertItem({
|
||||
title,
|
||||
severity,
|
||||
time,
|
||||
message,
|
||||
}: {
|
||||
title: string;
|
||||
severity: 'high' | 'medium' | 'low';
|
||||
time: string;
|
||||
message: string;
|
||||
}) {
|
||||
const severityConfig = {
|
||||
high: {color: 'bg-red-50 border-red-200', dot: 'bg-red-500'},
|
||||
medium: {color: 'bg-yellow-50 border-yellow-200', dot: 'bg-yellow-500'},
|
||||
low: {color: 'bg-blue-50 border-blue-200', dot: 'bg-blue-500'},
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`p-3 rounded-md ${severityConfig[severity].color} border`}>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center">
|
||||
<span className={`h-2 w-2 rounded-full ${severityConfig[severity].dot} mr-2`}></span>
|
||||
<span className="font-medium text-gray-800 text-sm">{title}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{time}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-600">{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 系统状态条组件
|
||||
function StatusBar({
|
||||
title,
|
||||
value,
|
||||
status,
|
||||
}: {
|
||||
title: string;
|
||||
value: number;
|
||||
status: 'normal' | 'warning' | 'error';
|
||||
}) {
|
||||
const statusConfig = {
|
||||
normal: {color: 'bg-green-500', bgColor: 'bg-green-100'},
|
||||
warning: {color: 'bg-yellow-500', bgColor: 'bg-yellow-100'},
|
||||
error: {color: 'bg-red-500', bgColor: 'bg-red-100'},
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-3 border border-gray-200 rounded-md">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-gray-700">{title}</span>
|
||||
<span className="text-sm font-medium">{value}%</span>
|
||||
</div>
|
||||
<div className={`w-full h-2 ${statusConfig[status].bgColor} rounded-full`}>
|
||||
<div
|
||||
className={`h-2 ${statusConfig[status].color} rounded-full`}
|
||||
style={{width: `${value}%`}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
276
src/app/(root)/appbar.tsx
Normal file
276
src/app/(root)/appbar.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
'use client'
|
||||
import {useState, useRef, useEffect} from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import {usePathname} from 'next/navigation'
|
||||
|
||||
export type AppbarProps = {}
|
||||
|
||||
export default function Appbar(props: AppbarProps) {
|
||||
const [currentUser] = useState({
|
||||
name: '张三',
|
||||
avatar: '/avatar.png',
|
||||
role: '管理员',
|
||||
})
|
||||
|
||||
const [showDropdown, setShowDropdown] = useState(false)
|
||||
const [showNotifications, setShowNotifications] = useState(false)
|
||||
const [notifications] = useState([
|
||||
{id: 1, title: '系统通知', content: '您有新的待审核内容', time: '10分钟前', read: false},
|
||||
{id: 2, title: '安全提醒', content: '您的账号于昨天登录了新设备', time: '1小时前', read: true},
|
||||
{id: 3, title: '系统更新', content: '系统将在今晚进行例行维护', time: '2小时前', read: true},
|
||||
])
|
||||
|
||||
const pathname = usePathname()
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const notificationRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 处理点击外部关闭下拉菜单
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setShowDropdown(false)
|
||||
}
|
||||
if (notificationRef.current && !notificationRef.current.contains(event.target as Node)) {
|
||||
setShowNotifications(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
// 根据路径生成面包屑
|
||||
const generateBreadcrumbs = () => {
|
||||
const paths = pathname.split('/').filter(Boolean)
|
||||
|
||||
const breadcrumbs = [
|
||||
{path: '/', label: '首页'},
|
||||
...paths.map((path, index) => {
|
||||
const url = `/${paths.slice(0, index + 1).join('/')}`
|
||||
const label = getBreadcrumbLabel(path)
|
||||
return {path: url, label}
|
||||
}),
|
||||
]
|
||||
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
const getBreadcrumbLabel = (path: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
'dashboard': '控制台',
|
||||
'content': '内容管理',
|
||||
'articles': '文章管理',
|
||||
'media': '媒体库',
|
||||
'users': '用户管理',
|
||||
'roles': '角色权限',
|
||||
'settings': '系统设置',
|
||||
'logs': '系统日志',
|
||||
}
|
||||
|
||||
return labels[path] || path
|
||||
}
|
||||
|
||||
const breadcrumbs = generateBreadcrumbs()
|
||||
const unreadCount = notifications.filter(n => !n.read).length
|
||||
|
||||
return (
|
||||
<header className="bg-white h-16 border-b border-gray-200 flex items-center justify-between px-6">
|
||||
{/* 面包屑导航 */}
|
||||
<div className="flex items-center text-sm">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<div key={crumb.path} className="flex items-center">
|
||||
{index > 0 && (
|
||||
<svg className="mx-2 h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
)}
|
||||
<Link
|
||||
href={crumb.path}
|
||||
className={index === breadcrumbs.length - 1
|
||||
? 'text-gray-800 font-medium'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}
|
||||
>
|
||||
{crumb.label}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 右侧用户信息和工具栏 */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* 搜索框 */}
|
||||
<div className="hidden md:block relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索..."
|
||||
className="pl-10 pr-4 py-2 bg-gray-100 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-56"
|
||||
/>
|
||||
<svg
|
||||
className="h-4 w-4 text-gray-400 absolute left-3 top-2.5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* 通知图标 */}
|
||||
<div className="relative" ref={notificationRef}>
|
||||
<button
|
||||
onClick={() => setShowNotifications(!showNotifications)}
|
||||
className="relative p-2 rounded-full text-gray-600 hover:bg-gray-100 hover:text-gray-800 transition-colors"
|
||||
aria-label="通知"
|
||||
>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
{unreadCount > 0 && (
|
||||
<span
|
||||
className="absolute top-1 right-1 h-4 w-4 text-xs flex items-center justify-center rounded-full bg-red-500 text-white">{unreadCount}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 通知下拉面板 */}
|
||||
{showNotifications && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border border-gray-200">
|
||||
<div className="px-4 py-2 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 className="font-medium text-gray-800">通知</h3>
|
||||
<button className="text-xs text-blue-600 hover:text-blue-800">
|
||||
全部标为已读
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-72 overflow-y-auto">
|
||||
{notifications.length > 0 ? (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`px-4 py-3 border-b border-gray-100 hover:bg-gray-50 ${
|
||||
notification.read ? 'bg-white' : 'bg-blue-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<h4 className="text-sm font-medium text-gray-800">{notification.title}</h4>
|
||||
<span className="text-xs text-gray-500">{notification.time}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">{notification.content}</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="py-8 px-4 text-center">
|
||||
<svg className="w-12 h-12 text-gray-300 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1}
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mt-2 text-sm text-gray-500">暂无通知</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100 p-2 text-center">
|
||||
<Link href="/notifications" className="text-xs text-blue-600 hover:text-blue-800">
|
||||
查看全部通知
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<div className="hidden md:block h-8 w-px bg-gray-200"></div>
|
||||
|
||||
{/* 用户下拉菜单 */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setShowDropdown(!showDropdown)}
|
||||
className="flex items-center space-x-2 rounded-lg hover:bg-gray-100 p-2 transition-colors"
|
||||
aria-label="用户菜单"
|
||||
>
|
||||
<div
|
||||
className="h-8 w-8 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center overflow-hidden border-2 border-white shadow-sm">
|
||||
<Image
|
||||
src={currentUser.avatar}
|
||||
alt="用户头像"
|
||||
width={32}
|
||||
height={32}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
target.parentElement!.innerHTML = currentUser.name.charAt(0)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden md:block text-left">
|
||||
<p className="text-sm font-medium text-gray-800">{currentUser.name}</p>
|
||||
<p className="text-xs text-gray-500">{currentUser.role}</p>
|
||||
</div>
|
||||
<svg
|
||||
className="h-4 w-4 text-gray-400 hidden md:block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* 用户下拉内容 */}
|
||||
{showDropdown && (
|
||||
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg py-2 z-20 border border-gray-200">
|
||||
<div className="px-4 py-2 border-b border-gray-100 md:hidden">
|
||||
<p className="font-medium text-gray-800">{currentUser.name}</p>
|
||||
<p className="text-xs text-gray-500">{currentUser.role}</p>
|
||||
</div>
|
||||
|
||||
<div className="py-1">
|
||||
<Link href="/profile" className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<svg className="mr-3 h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
个人资料
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings/account"
|
||||
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<svg className="mr-3 h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
账号设置
|
||||
</Link>
|
||||
<Link
|
||||
href="/system/help" className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<svg className="mr-3 h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
帮助中心
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100 mt-1">
|
||||
<Link href="/login" className="flex items-center px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
|
||||
<svg className="mr-3 h-5 w-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
||||
</svg>
|
||||
退出登录
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
27
src/app/(root)/layout.tsx
Normal file
27
src/app/(root)/layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import {ReactNode} from 'react'
|
||||
import Appbar from '@/app/(root)/appbar'
|
||||
import Navigation from '@/app/(root)/navigation'
|
||||
|
||||
export type RootLayoutProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function RootLayout({children}: RootLayoutProps) {
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
{/* 侧边栏 */}
|
||||
<Navigation/>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* 顶部导航栏 */}
|
||||
<Appbar/>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
191
src/app/(root)/navigation.tsx
Normal file
191
src/app/(root)/navigation.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
import {useState} from 'react'
|
||||
import Link from 'next/link'
|
||||
import {usePathname} from 'next/navigation'
|
||||
|
||||
export type NavigationProps = {}
|
||||
|
||||
// 菜单组接口
|
||||
interface MenuGroup {
|
||||
title: string;
|
||||
items: MenuItem[];
|
||||
}
|
||||
|
||||
// 菜单项接口
|
||||
interface MenuItem {
|
||||
path: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// 定义菜单组
|
||||
const menuGroups: MenuGroup[] = [
|
||||
{
|
||||
title: '概览',
|
||||
items: [
|
||||
{
|
||||
path: '/',
|
||||
icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
|
||||
label: '首页',
|
||||
},
|
||||
{
|
||||
path: '/statistics',
|
||||
icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
|
||||
label: '数据统计',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'IP 资源',
|
||||
items: [
|
||||
{
|
||||
path: '/proxy/nodes',
|
||||
icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9',
|
||||
label: '节点列表',
|
||||
},
|
||||
{
|
||||
path: '/proxy/pools',
|
||||
icon: 'M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01',
|
||||
label: 'IP池管理',
|
||||
},
|
||||
{
|
||||
path: '/proxy/sources',
|
||||
icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
|
||||
label: '代理源管理',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '客户',
|
||||
items: [
|
||||
{
|
||||
path: '/clients',
|
||||
icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z',
|
||||
label: '客户管理',
|
||||
},
|
||||
{path: '/packages', icon: 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10', label: '套餐管理'},
|
||||
{
|
||||
path: '/orders',
|
||||
icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2',
|
||||
label: '订单管理',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '运营',
|
||||
items: [
|
||||
{path: '/api/management', icon: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4', label: 'API管理'},
|
||||
{
|
||||
path: '/traffic',
|
||||
icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
label: '流量监控',
|
||||
},
|
||||
{
|
||||
path: '/billing',
|
||||
icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
label: '计费系统',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '系统',
|
||||
items: [
|
||||
{
|
||||
path: '/settings',
|
||||
icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z',
|
||||
label: '系统设置',
|
||||
},
|
||||
{
|
||||
path: '/security',
|
||||
icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||
label: '安全管理',
|
||||
},
|
||||
{
|
||||
path: '/logs',
|
||||
icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
label: '系统日志',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default function Navigation(props: NavigationProps) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const pathname = usePathname()
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return path === '/'
|
||||
? pathname === path
|
||||
: pathname.startsWith(path)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`bg-white border-r border-gray-200 transition-all duration-300 ease-in-out flex flex-col ${collapsed ? 'w-20' : 'w-64'}`}>
|
||||
{/* Logo */}
|
||||
<div className="h-16 flex items-center px-5 border-b border-gray-200">
|
||||
{!collapsed ? (
|
||||
<span className="text-xl font-bold tracking-wide text-gray-800">管理系统</span>
|
||||
) : (
|
||||
<span className="text-xl font-bold mx-auto text-gray-800">系</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 导航菜单 */}
|
||||
<nav className="flex-1 py-4 overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
{menuGroups.map((group, groupIndex) => (
|
||||
<div key={groupIndex} className="px-3">
|
||||
{!collapsed && (
|
||||
<h3 className="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
{group.title}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
<ul className={`mt-${collapsed ? '0' : '2'} space-y-1`}>
|
||||
{group.items.map((item) => (
|
||||
<li key={item.path}>
|
||||
<Link
|
||||
href={item.path}
|
||||
className={`flex items-center px-3 py-2 rounded-md transition-colors ${
|
||||
isActive(item.path)
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
className={`h-5 w-5 ${isActive(item.path) ? 'text-blue-600' : 'text-gray-500'}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon}/>
|
||||
</svg>
|
||||
{!collapsed && <span className="ml-3 font-medium text-sm">{item.label}</span>}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{!collapsed && groupIndex < menuGroups.length - 1 && (
|
||||
<div className="my-4 border-t border-gray-200"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* 侧边栏底部按钮 */}
|
||||
<div className="p-4 border-t border-gray-200 mt-auto">
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="flex items-center justify-center w-full p-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d={collapsed ? 'M13 5l7 7-7 7M5 5l7 7-7 7' : 'M11 19l-7-7 7-7m8 14l-7-7 7-7'}/>
|
||||
</svg>
|
||||
{!collapsed && <span className="ml-2 text-sm">收起菜单</span>}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
495
src/app/(root)/proxy/nodes/page.tsx
Normal file
495
src/app/(root)/proxy/nodes/page.tsx
Normal file
@@ -0,0 +1,495 @@
|
||||
'use client'
|
||||
import {useState, useEffect} from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
// 定义节点数据接口
|
||||
interface ProxyNode {
|
||||
id: string;
|
||||
ipAddress: string;
|
||||
location: {
|
||||
country: string;
|
||||
region: string;
|
||||
};
|
||||
type: string;
|
||||
status: 'online' | 'offline' | 'warning';
|
||||
responseTime: number;
|
||||
lastCheckTime: string;
|
||||
pool: string;
|
||||
isStatic: boolean;
|
||||
}
|
||||
|
||||
export type ProxyNodesPageProps = {}
|
||||
|
||||
export default function ProxyNodesPage(props: ProxyNodesPageProps) {
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
|
||||
const [nodes, setNodes] = useState<ProxyNode[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const [filterStatus, setFilterStatus] = useState<string>('all')
|
||||
const [filterType, setFilterType] = useState<string>('all')
|
||||
const [filterPool, setFilterPool] = useState<string>('all')
|
||||
|
||||
// 模拟数据加载
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setNodes([
|
||||
{
|
||||
id: 'ip-1',
|
||||
ipAddress: '203.45.167.82',
|
||||
location: {country: '美国', region: '纽约'},
|
||||
type: '数据中心',
|
||||
status: 'online',
|
||||
responseTime: 126,
|
||||
lastCheckTime: '2024-05-10 15:30:22',
|
||||
pool: '北美专用池',
|
||||
isStatic: true,
|
||||
},
|
||||
{
|
||||
id: 'ip-2',
|
||||
ipAddress: '185.72.193.54',
|
||||
location: {country: '德国', region: '法兰克福'},
|
||||
type: '住宅',
|
||||
status: 'online',
|
||||
responseTime: 158,
|
||||
lastCheckTime: '2024-05-10 15:28:45',
|
||||
pool: '欧洲高速池',
|
||||
isStatic: false,
|
||||
},
|
||||
{
|
||||
id: 'ip-3',
|
||||
ipAddress: '118.96.244.105',
|
||||
location: {country: '新加坡', region: '中心区'},
|
||||
type: '移动',
|
||||
status: 'warning',
|
||||
responseTime: 312,
|
||||
lastCheckTime: '2024-05-10 15:25:12',
|
||||
pool: '亚太地区池',
|
||||
isStatic: false,
|
||||
},
|
||||
{
|
||||
id: 'ip-4',
|
||||
ipAddress: '45.178.29.6',
|
||||
location: {country: '加拿大', region: '多伦多'},
|
||||
type: '数据中心',
|
||||
status: 'online',
|
||||
responseTime: 143,
|
||||
lastCheckTime: '2024-05-10 15:23:08',
|
||||
pool: '北美专用池',
|
||||
isStatic: false,
|
||||
},
|
||||
{
|
||||
id: 'ip-5',
|
||||
ipAddress: '79.114.83.201',
|
||||
location: {country: '英国', region: '伦敦'},
|
||||
type: '住宅',
|
||||
status: 'offline',
|
||||
responseTime: 0,
|
||||
lastCheckTime: '2024-05-10 15:18:33',
|
||||
pool: '欧洲高速池',
|
||||
isStatic: false,
|
||||
},
|
||||
{
|
||||
id: 'ip-6',
|
||||
ipAddress: '164.83.219.47',
|
||||
location: {country: '日本', region: '东京'},
|
||||
type: '住宅',
|
||||
status: 'online',
|
||||
responseTime: 87,
|
||||
lastCheckTime: '2024-05-10 15:15:21',
|
||||
pool: '亚太地区池',
|
||||
isStatic: true,
|
||||
},
|
||||
{
|
||||
id: 'ip-7',
|
||||
ipAddress: '221.67.93.143',
|
||||
location: {country: '中国', region: '上海'},
|
||||
type: '移动',
|
||||
status: 'online',
|
||||
responseTime: 104,
|
||||
lastCheckTime: '2024-05-10 15:10:46',
|
||||
pool: '亚太地区池',
|
||||
isStatic: false,
|
||||
},
|
||||
{
|
||||
id: 'ip-8',
|
||||
ipAddress: '37.209.148.72',
|
||||
location: {country: '法国', region: '巴黎'},
|
||||
type: '数据中心',
|
||||
status: 'warning',
|
||||
responseTime: 276,
|
||||
lastCheckTime: '2024-05-10 15:05:19',
|
||||
pool: '欧洲高速池',
|
||||
isStatic: false,
|
||||
},
|
||||
])
|
||||
setLoading(false)
|
||||
}, 800)
|
||||
}, [])
|
||||
|
||||
// 过滤节点数据
|
||||
const filteredNodes = nodes.filter(node => {
|
||||
return (
|
||||
(searchTerm === '' ||
|
||||
node.ipAddress.includes(searchTerm) ||
|
||||
node.location.country.includes(searchTerm) ||
|
||||
node.pool.includes(searchTerm)) &&
|
||||
(filterStatus === 'all' ||
|
||||
(filterStatus === 'online' && node.status === 'online') ||
|
||||
(filterStatus === 'offline' && node.status === 'offline') ||
|
||||
(filterStatus === 'warning' && node.status === 'warning')) &&
|
||||
(filterType === 'all' || node.type === filterType) &&
|
||||
(filterPool === 'all' || node.pool === filterPool)
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* 概览区域 - 使用色块和留白风格 */}
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
{/* 标题区域 - 简洁风格 */}
|
||||
<div className="bg-white px-5 py-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-lg font-bold text-gray-900">节点列表</h1>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="bg-gray-50 border border-gray-200 text-gray-700 px-3 py-1.5 rounded-md text-sm font-medium flex items-center hover:bg-gray-100">
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
||||
</svg>
|
||||
导出
|
||||
</button>
|
||||
<button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-md text-sm font-medium flex items-center">
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
添加节点
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-500">查看和管理所有代理IP资源,支持多维度筛选</p>
|
||||
</div>
|
||||
|
||||
{/* 统计信息区域 - 色块风格 */}
|
||||
<div className="grid grid-cols-4 gap-px bg-gray-100">
|
||||
{/* 总IP数量 */}
|
||||
<div className="bg-white p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-blue-50 rounded-md">
|
||||
<svg className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-xs font-medium text-gray-500">总IP数量</p>
|
||||
<div className="text-lg font-semibold text-gray-900">152,487</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 在线IP */}
|
||||
<div className="bg-white p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-green-50 rounded-md">
|
||||
<svg className="h-5 w-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M5 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<div className="flex items-center">
|
||||
<p className="text-xs font-medium text-gray-500">在线IP</p>
|
||||
<span className="ml-2 text-xs font-medium text-green-600">91%</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-gray-900">138,954</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IP池分布 */}
|
||||
<div className="bg-white p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-indigo-50 rounded-md">
|
||||
<svg className="h-5 w-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<div className="flex items-center">
|
||||
<p className="text-xs font-medium text-gray-500">IP池分布</p>
|
||||
<span className="ml-2 text-xs font-medium text-gray-500">5个地区</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-gray-900">12</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 异常IP */}
|
||||
<div className="bg-white p-4">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 flex items-center justify-center bg-red-50 rounded-md">
|
||||
<svg className="h-5 w-5 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<div className="flex items-center">
|
||||
<p className="text-xs font-medium text-gray-500">异常IP</p>
|
||||
<span className="ml-2 text-xs font-medium text-red-600">需检查</span>
|
||||
</div>
|
||||
<div className="text-lg font-semibold text-gray-900">1,205</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据展示 */}
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
{/* 筛选搜索区域 */}
|
||||
<div className="bg-white p-4 border-b border-gray-200">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-12">
|
||||
{/* 搜索框 */}
|
||||
<div className="relative md:col-span-5">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索IP地址、地区或标签..."
|
||||
className="w-full px-3 py-2 bg-gray-50 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
/>
|
||||
<svg
|
||||
className="absolute right-3 top-2.5 h-4 w-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* 筛选区域 */}
|
||||
<div className="flex space-x-3 md:col-span-7">
|
||||
<select
|
||||
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white text-gray-700 flex-1"
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="online">在线</option>
|
||||
<option value="offline">离线</option>
|
||||
<option value="warning">异常</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white text-gray-700 flex-1"
|
||||
value={filterType}
|
||||
onChange={(e) => setFilterType(e.target.value)}
|
||||
>
|
||||
<option value="all">全部类型</option>
|
||||
<option value="数据中心">数据中心</option>
|
||||
<option value="住宅">住宅</option>
|
||||
<option value="移动">移动</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm bg-white text-gray-700 flex-1"
|
||||
value={filterPool}
|
||||
onChange={(e) => setFilterPool(e.target.value)}
|
||||
>
|
||||
<option value="all">全部池</option>
|
||||
<option value="北美专用池">北美专用池</option>
|
||||
<option value="欧洲高速池">欧洲高速池</option>
|
||||
<option value="亚太地区池">亚太地区池</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IP表格区域 */}
|
||||
{loading ? (
|
||||
<div className="p-12 flex justify-center items-center bg-white">
|
||||
<svg
|
||||
className="animate-spin h-6 w-6 text-blue-600"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto bg-white">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
|
||||
IP地址
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
|
||||
位置
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
|
||||
类型
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
|
||||
状态
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
|
||||
响应时间
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
|
||||
所属池
|
||||
</th>
|
||||
<th scope="col" className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{filteredNodes.map((node, index) => (
|
||||
<tr key={node.id} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<div className="flex items-center">
|
||||
<span className={`w-2 h-2 rounded-full mr-2 ${
|
||||
node.status === 'online' ? 'bg-green-500' :
|
||||
node.status === 'offline' ? 'bg-red-500' : 'bg-yellow-500'
|
||||
}`}></span>
|
||||
<span className="font-medium text-gray-900">{node.ipAddress}</span>
|
||||
{node.isStatic && (
|
||||
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
静态
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<div className="text-gray-900">{node.location.country}</div>
|
||||
<div className="text-xs text-gray-500">{node.location.region}</div>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
node.type === '数据中心' ? 'bg-purple-100 text-purple-800' :
|
||||
node.type === '住宅' ? 'bg-blue-100 text-blue-800' : 'bg-indigo-100 text-indigo-800'
|
||||
}`}>
|
||||
{node.type}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
node.status === 'online' ? 'bg-green-100 text-green-800' :
|
||||
node.status === 'offline' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{node.status === 'online' ? '在线' : node.status === 'offline' ? '离线' : '异常'}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{node.responseTime > 0 ? (
|
||||
<div className="flex items-center">
|
||||
<span className={`font-medium ${
|
||||
node.responseTime < 150 ? 'text-green-600' :
|
||||
node.responseTime < 250 ? 'text-yellow-600' : 'text-red-600'
|
||||
}`}>
|
||||
{node.responseTime} ms
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{node.pool}
|
||||
</td>
|
||||
|
||||
<td className="px-4 py-3 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
||||
<Link href={`/proxy/nodes/${node.id}`} className="text-blue-600 hover:text-blue-900">
|
||||
详情
|
||||
</Link>
|
||||
<button className="text-gray-600 hover:text-gray-900">
|
||||
编辑
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分页控制 */}
|
||||
<div className="bg-gray-50 px-5 py-3 border-t border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-gray-700">
|
||||
显示 <span className="font-medium">1</span> 至 <span className="font-medium">8</span> 条,共 <span
|
||||
className="font-medium">152,487</span> 条结果
|
||||
</div>
|
||||
<div>
|
||||
<nav className="relative z-0 inline-flex -space-x-px">
|
||||
<button
|
||||
className="relative inline-flex items-center px-2 py-1.5 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
|
||||
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium hover:bg-gray-50 text-blue-600 bg-blue-50 border-blue-300">
|
||||
1
|
||||
</button>
|
||||
<button
|
||||
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
2
|
||||
</button>
|
||||
<button
|
||||
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
3
|
||||
</button>
|
||||
<span
|
||||
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
||||
...
|
||||
</span>
|
||||
<button
|
||||
className="relative inline-flex items-center px-3 py-1.5 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||
152
|
||||
</button>
|
||||
<button
|
||||
className="relative inline-flex items-center px-2 py-1.5 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
|
||||
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||
clipRule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
421
src/app/(root)/proxy/pools/page.tsx
Normal file
421
src/app/(root)/proxy/pools/page.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
'use client'
|
||||
import {useState, useEffect} from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export type ProxyPoolsPageProps = {}
|
||||
|
||||
// 定义IP池接口
|
||||
interface ProxyPool {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ips: number;
|
||||
activeIps: number;
|
||||
region: string;
|
||||
type: string;
|
||||
createdAt: string;
|
||||
status: 'active' | 'inactive' | 'maintenance';
|
||||
}
|
||||
|
||||
export default function ProxyPoolsPage(props: ProxyPoolsPageProps) {
|
||||
const [pools, setPools] = useState<ProxyPool[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const [filterStatus, setFilterStatus] = useState<string>('all')
|
||||
const [filterRegion, setFilterRegion] = useState<string>('all')
|
||||
|
||||
// 模拟数据加载
|
||||
useEffect(() => {
|
||||
// 实际项目中替换为API调用
|
||||
setTimeout(() => {
|
||||
setPools([
|
||||
{
|
||||
id: 'pool-1',
|
||||
name: '全球通用池',
|
||||
description: '包含全球多个地区的高质量IP',
|
||||
ips: 5000,
|
||||
activeIps: 4328,
|
||||
region: '全球',
|
||||
type: '住宅IP',
|
||||
createdAt: '2023-10-15',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'pool-2',
|
||||
name: '北美专用池',
|
||||
description: '美国和加拿大地区专用IP池',
|
||||
ips: 3200,
|
||||
activeIps: 2950,
|
||||
region: '北美',
|
||||
type: '数据中心IP',
|
||||
createdAt: '2023-11-02',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'pool-3',
|
||||
name: '欧洲高速池',
|
||||
description: '欧洲地区高速稳定IP',
|
||||
ips: 2800,
|
||||
activeIps: 2180,
|
||||
region: '欧洲',
|
||||
type: '住宅IP',
|
||||
createdAt: '2023-09-28',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'pool-4',
|
||||
name: '亚太地区池',
|
||||
description: '亚洲和太平洋地区IP',
|
||||
ips: 4200,
|
||||
activeIps: 3890,
|
||||
region: '亚太',
|
||||
type: '移动IP',
|
||||
createdAt: '2023-12-05',
|
||||
status: 'maintenance',
|
||||
},
|
||||
{
|
||||
id: 'pool-5',
|
||||
name: '电商专用池',
|
||||
description: '适用于电商平台的IP池',
|
||||
ips: 1500,
|
||||
activeIps: 1200,
|
||||
region: '全球',
|
||||
type: '住宅IP',
|
||||
createdAt: '2024-01-10',
|
||||
status: 'inactive',
|
||||
},
|
||||
])
|
||||
setLoading(false)
|
||||
}, 800)
|
||||
}, [])
|
||||
|
||||
// 过滤IP池
|
||||
const filteredPools = pools.filter(pool => {
|
||||
return (
|
||||
(searchTerm === '' ||
|
||||
pool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
pool.description.toLowerCase().includes(searchTerm.toLowerCase())) &&
|
||||
(filterStatus === 'all' || pool.status === filterStatus) &&
|
||||
(filterRegion === 'all' || pool.region === filterRegion)
|
||||
)
|
||||
})
|
||||
|
||||
// 获取状态颜色和文本
|
||||
const getStatusInfo = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return {color: 'bg-green-100 text-green-800', text: '运行中'}
|
||||
case 'inactive':
|
||||
return {color: 'bg-gray-100 text-gray-800', text: '未启用'}
|
||||
case 'maintenance':
|
||||
return {color: 'bg-yellow-100 text-yellow-800', text: '维护中'}
|
||||
default:
|
||||
return {color: 'bg-gray-100 text-gray-800', text: '未知'}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">IP池管理</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">管理和配置代理IP池</p>
|
||||
</div>
|
||||
<button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium flex items-center">
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
创建新IP池
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 筛选和搜索工具栏 */}
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索IP池..."
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<svg
|
||||
className="absolute right-3 top-2.5 h-5 w-5 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<div className="w-full">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">状态</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="active">运行中</option>
|
||||
<option value="inactive">未启用</option>
|
||||
<option value="maintenance">维护中</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">地区</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
||||
value={filterRegion}
|
||||
onChange={(e) => setFilterRegion(e.target.value)}
|
||||
>
|
||||
<option value="all">全部地区</option>
|
||||
<option value="全球">全球</option>
|
||||
<option value="北美">北美</option>
|
||||
<option value="欧洲">欧洲</option>
|
||||
<option value="亚太">亚太</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IP池列表 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-8 flex justify-center">
|
||||
<svg
|
||||
className="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path
|
||||
className="opacity-75" fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
) : filteredPools.length === 0 ? (
|
||||
<div className="p-8 text-center">
|
||||
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
|
||||
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<p className="mt-4 text-gray-500 text-lg">没有找到匹配的IP池</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
IP池名称
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
IP概况
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
地区
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
IP类型
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
状态
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
创建日期
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredPools.map((pool) => {
|
||||
const statusInfo = getStatusInfo(pool.status)
|
||||
return (
|
||||
<tr key={pool.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{pool.name}</div>
|
||||
<div className="text-sm text-gray-500 max-w-xs truncate">{pool.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">总IP: {pool.ips.toLocaleString()}</div>
|
||||
<div className="text-sm text-gray-500">活跃IP: {pool.activeIps.toLocaleString()}</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-1.5">
|
||||
<div
|
||||
className="bg-blue-600 h-1.5 rounded-full"
|
||||
style={{width: `${(pool.activeIps / pool.ips) * 100}%`}}
|
||||
></div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">{pool.region}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-900">{pool.type}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusInfo.color}`}>
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{pool.createdAt}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Link href={`/proxy/pools/${pool.id}`} className="text-blue-600 hover:text-blue-900 mr-4">
|
||||
详情
|
||||
</Link>
|
||||
<Link
|
||||
href={`/proxy/pools/${pool.id}/edit`} className="text-indigo-600 hover:text-indigo-900 mr-4">
|
||||
编辑
|
||||
</Link>
|
||||
<button className="text-red-600 hover:text-red-900">
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 数据卡片概览 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-blue-100 text-blue-600">
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">总IP池</h3>
|
||||
<div className="mt-1 text-3xl font-semibold">{pools.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">活跃IP池</span>
|
||||
<span className="font-medium text-gray-900">{pools.filter(p => p.status === 'active').length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-green-100 text-green-600">
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">总IP数量</h3>
|
||||
<div className="mt-1 text-3xl font-semibold">
|
||||
{pools.reduce((sum, pool) => sum + pool.ips, 0).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-500">活跃IP</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{pools.reduce((sum, pool) => sum + pool.activeIps, 0).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-purple-100 text-purple-600">
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">IP利用率</h3>
|
||||
<div className="mt-1 text-3xl font-semibold">
|
||||
{Math.round((pools.reduce((sum, pool) => sum + pool.activeIps, 0) /
|
||||
pools.reduce((sum, pool) => sum + pool.ips, 0)) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-purple-600 h-2 rounded-full"
|
||||
style={{
|
||||
width: `${(pools.reduce((sum, pool) => sum + pool.activeIps, 0) /
|
||||
pools.reduce((sum, pool) => sum + pool.ips, 0)) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作指南 */}
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm text-blue-700">
|
||||
IP池是管理IP资源的基础单位。您可以创建不同用途的IP池,并为客户分配相应的访问权限。
|
||||
</p>
|
||||
<p className="mt-3 text-sm md:mt-0 md:ml-6">
|
||||
<a href="/help/proxy-pools" className="whitespace-nowrap font-medium text-blue-700 hover:text-blue-600">
|
||||
查看文档 <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
src/app/(root)/proxy/sources/page.tsx
Normal file
11
src/app/(root)/proxy/sources/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import {ReactNode} from 'react'
|
||||
|
||||
export type ProxySourcesPageProps = {
|
||||
|
||||
}
|
||||
|
||||
export default function ProxySourcesPage(props: ProxySourcesPageProps) {
|
||||
return (
|
||||
|
||||
)
|
||||
}
|
||||
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
src/app/globals.css
Normal file
26
src/app/globals.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
16
src/app/layout.tsx
Normal file
16
src/app/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout(props: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>{props.children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
3
src/app/lib/base/index.ts
Normal file
3
src/app/lib/base/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const BASE_URL = process.env.API_BASE_URL
|
||||
export const CLIENT_ID = process.env.CLIENT_ID
|
||||
export const CLIENT_SECRET = process.env.CLIENT_SECRET
|
||||
85
src/app/login/page.tsx
Normal file
85
src/app/login/page.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
export type LoginPageProps = {};
|
||||
|
||||
export default function LoginPage(props: LoginPageProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center p-4">
|
||||
{/* 登录卡片 */}
|
||||
<div className="bg-white rounded-lg shadow-lg w-full max-w-md p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-800">内容管理系统</h1>
|
||||
<p className="text-sm text-gray-500 mt-2">请登录以访问管理后台</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-6">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
账号
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
className="mt-1 block w-full px-4 py-3 border border-gray-200 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className="mt-1 block w-full px-4 py-3 border border-gray-200 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label
|
||||
htmlFor="remember-me"
|
||||
className="ml-2 block text-sm text-gray-700"
|
||||
>
|
||||
记住账号
|
||||
</label>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<a
|
||||
href="#"
|
||||
className="font-medium text-blue-600 hover:text-blue-500"
|
||||
>
|
||||
忘记密码
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
|
||||
>
|
||||
登录系统
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-xs text-gray-500">
|
||||
© {new Date().getFullYear()} 内部系统 - 仅限授权人员访问
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user