@@ -5,3 +5,9 @@ import { callByUser } from "./base"
|
|||||||
export async function getPageUsers(params: { page: number; size: number }) {
|
export async function getPageUsers(params: { page: number; size: number }) {
|
||||||
return callByUser<PageRecord<User>>("/api/admin/user/page", params)
|
return callByUser<PageRecord<User>>("/api/admin/user/page", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function bindAdmin(params: { id: number }) {
|
||||||
|
return callByUser("/api/admin/user/bind", {
|
||||||
|
user_id: params.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Link from 'next/link'
|
import Link from "next/link"
|
||||||
|
|
||||||
export type DashboardPageProps = {}
|
export type DashboardPageProps = {}
|
||||||
|
|
||||||
@@ -9,16 +9,18 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
<div className="bg-white border border-gray-200 rounded-md">
|
<div className="bg-white border border-gray-200 rounded-md">
|
||||||
<div className="flex items-center justify-between p-5">
|
<div className="flex items-center justify-between p-5">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-gray-800">IP代理管理控制台</h1>
|
<h1 className="text-xl font-bold text-gray-800">
|
||||||
<p className="text-gray-500 mt-1">上次更新: {new Date().toLocaleString('zh-CN')}</p>
|
IP代理管理控制台
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
上次更新: {new Date().toLocaleString("zh-CN")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<button
|
<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">
|
||||||
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>
|
||||||
<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">
|
||||||
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,8 +39,18 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
change="+12.5%"
|
change="+12.5%"
|
||||||
isIncrease={true}
|
isIncrease={true}
|
||||||
icon={
|
icon={
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M12 5l7 7-7 7"/>
|
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>
|
</svg>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -48,9 +60,18 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
change="+8.2%"
|
change="+8.2%"
|
||||||
isIncrease={true}
|
isIncrease={true}
|
||||||
icon={
|
icon={
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
|
className="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -60,10 +81,18 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
change="+2.4%"
|
change="+2.4%"
|
||||||
isIncrease={true}
|
isIncrease={true}
|
||||||
icon={
|
icon={
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
|
className="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
strokeLinecap="round"
|
||||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -73,10 +102,18 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
change="-12.3%"
|
change="-12.3%"
|
||||||
isIncrease={true}
|
isIncrease={true}
|
||||||
icon={
|
icon={
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
|
className="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
strokeLinecap="round"
|
||||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -119,14 +156,18 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
<div className="p-5 border-b border-gray-200">
|
<div className="p-5 border-b border-gray-200">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="font-bold text-gray-800">活跃代理IP</h2>
|
<h2 className="font-bold text-gray-800">活跃代理IP</h2>
|
||||||
<Link href="/proxies" className="text-blue-600 text-sm hover:underline">查看全部</Link>
|
<Link
|
||||||
|
href="/proxies"
|
||||||
|
className="text-blue-600 text-sm hover:underline"
|
||||||
|
>
|
||||||
|
查看全部
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr
|
<tr className="bg-gray-50 text-left text-xs text-gray-500 uppercase tracking-wider border-b border-gray-200">
|
||||||
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">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>
|
||||||
@@ -136,12 +177,18 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{[1, 2, 3, 4, 5].map((item) => (
|
{[1, 2, 3, 4, 5].map(item => (
|
||||||
<ContentRow
|
<ContentRow
|
||||||
key={item}
|
key={item}
|
||||||
ip={`192.168.${item}.${item * 10}`}
|
ip={`192.168.${item}.${item * 10}`}
|
||||||
location={item % 2 === 0 ? '中国' : '美国'}
|
location={item % 2 === 0 ? "中国" : "美国"}
|
||||||
status={item % 3 === 0 ? 'error' : item % 2 === 0 ? 'warning' : 'active'}
|
status={
|
||||||
|
item % 3 === 0
|
||||||
|
? "error"
|
||||||
|
: item % 2 === 0
|
||||||
|
? "warning"
|
||||||
|
: "active"
|
||||||
|
}
|
||||||
requests={Math.floor(Math.random() * 10000)}
|
requests={Math.floor(Math.random() * 10000)}
|
||||||
successRate={`${95 + Math.floor(Math.random() * 5)}%`}
|
successRate={`${95 + Math.floor(Math.random() * 5)}%`}
|
||||||
/>
|
/>
|
||||||
@@ -150,8 +197,12 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gray-50 border-t border-gray-200 flex justify-end">
|
<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-white border border-gray-200 rounded-md text-sm mr-2">
|
||||||
<button className="px-3 py-1 bg-blue-600 text-white rounded-md text-sm">下一页</button>
|
上一页
|
||||||
|
</button>
|
||||||
|
<button className="px-3 py-1 bg-blue-600 text-white rounded-md text-sm">
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,7 +242,9 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
<div className="p-5 border-b border-gray-200">
|
<div className="p-5 border-b border-gray-200">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="font-bold text-gray-800">告警通知</h2>
|
<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>
|
<span className="px-2 py-1 bg-red-100 text-red-800 text-xs rounded-full">
|
||||||
|
3 个新告警
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 space-y-3">
|
<div className="p-3 space-y-3">
|
||||||
@@ -215,8 +268,7 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 border-t border-gray-200">
|
<div className="p-4 border-t border-gray-200">
|
||||||
<button
|
<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">
|
||||||
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,35 +279,22 @@ export default function DashboardPage(props: DashboardPageProps) {
|
|||||||
<div className="p-5 border-b border-gray-200">
|
<div className="p-5 border-b border-gray-200">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h2 className="font-bold text-gray-800">系统状态</h2>
|
<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>
|
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full font-medium">
|
||||||
|
运行正常
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 space-y-3">
|
<div className="p-3 space-y-3">
|
||||||
<StatusBar
|
<StatusBar title="代理服务器负载" value={28} status="normal" />
|
||||||
title="代理服务器负载"
|
<StatusBar title="带宽使用率" value={65} status="normal" />
|
||||||
value={28}
|
<StatusBar title="存储空间" value={82} status="warning" />
|
||||||
status="normal"
|
<StatusBar title="API请求队列" value={45} status="normal" />
|
||||||
/>
|
|
||||||
<StatusBar
|
|
||||||
title="带宽使用率"
|
|
||||||
value={65}
|
|
||||||
status="normal"
|
|
||||||
/>
|
|
||||||
<StatusBar
|
|
||||||
title="存储空间"
|
|
||||||
value={82}
|
|
||||||
status="warning"
|
|
||||||
/>
|
|
||||||
<StatusBar
|
|
||||||
title="API请求队列"
|
|
||||||
value={45}
|
|
||||||
status="normal"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 border-t border-gray-200">
|
<div className="p-4 border-t border-gray-200">
|
||||||
<Link
|
<Link
|
||||||
href="/system/status"
|
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">
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,11 +313,11 @@ function DataCard({
|
|||||||
isIncrease,
|
isIncrease,
|
||||||
icon,
|
icon,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string
|
||||||
value: string;
|
value: string
|
||||||
change: string;
|
change: string
|
||||||
isIncrease: boolean;
|
isIncrease: boolean
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border border-gray-200 rounded-md p-5">
|
<div className="bg-white border border-gray-200 rounded-md p-5">
|
||||||
@@ -287,14 +326,17 @@ function DataCard({
|
|||||||
<h3 className="text-sm text-gray-500">{title}</h3>
|
<h3 className="text-sm text-gray-500">{title}</h3>
|
||||||
<p className="text-xl font-bold mt-1 text-gray-800">{value}</p>
|
<p className="text-xl font-bold mt-1 text-gray-800">{value}</p>
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center mt-2">
|
||||||
<span className={`text-xs font-medium ${isIncrease ? 'text-green-600' : 'text-red-600'}`}>
|
<span
|
||||||
|
className={`text-xs font-medium ${isIncrease ? "text-green-600" : "text-red-600"}`}
|
||||||
|
>
|
||||||
{change}
|
{change}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-gray-400 ml-1">相比上周期</span>
|
<span className="text-xs text-gray-400 ml-1">相比上周期</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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'}`}>
|
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}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -310,16 +352,22 @@ function ContentRow({
|
|||||||
requests,
|
requests,
|
||||||
successRate,
|
successRate,
|
||||||
}: {
|
}: {
|
||||||
ip: string;
|
ip: string
|
||||||
location: string;
|
location: string
|
||||||
status: 'active' | 'warning' | 'error';
|
status: "active" | "warning" | "error"
|
||||||
requests: number;
|
requests: number
|
||||||
successRate: string;
|
successRate: string
|
||||||
}) {
|
}) {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
active: {color: 'bg-green-100 text-green-800 border-green-200', label: '在线'},
|
active: {
|
||||||
warning: {color: 'bg-yellow-100 text-yellow-800 border-yellow-200', label: '不稳定'},
|
color: "bg-green-100 text-green-800 border-green-200",
|
||||||
error: {color: 'bg-red-100 text-red-800 border-red-200', label: '离线'},
|
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 (
|
return (
|
||||||
@@ -331,36 +379,67 @@ function ContentRow({
|
|||||||
<div className="text-sm text-gray-700">{location}</div>
|
<div className="text-sm text-gray-700">{location}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4">
|
<td className="px-5 py-4">
|
||||||
<span className={`px-2 py-1 text-xs rounded-md ${statusConfig[status].color} border`}>
|
<span
|
||||||
|
className={`px-2 py-1 text-xs rounded-md ${statusConfig[status].color} border`}
|
||||||
|
>
|
||||||
{statusConfig[status].label}
|
{statusConfig[status].label}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 text-sm text-gray-700">
|
<td className="px-5 py-4 text-sm text-gray-700">
|
||||||
{requests.toLocaleString()}
|
{requests.toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 text-sm text-gray-700">
|
<td className="px-5 py-4 text-sm text-gray-700">{successRate}</td>
|
||||||
{successRate}
|
|
||||||
</td>
|
|
||||||
<td className="px-5 py-4">
|
<td className="px-5 py-4">
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button className="p-1 border border-gray-200 rounded-md hover:bg-gray-50">
|
<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">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
className="h-4 w-4 text-gray-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
strokeLinecap="round"
|
||||||
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"/>
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button className="p-1 border border-gray-200 rounded-md hover:bg-gray-50">
|
<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">
|
<svg
|
||||||
|
className="h-4 w-4 text-gray-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
strokeLinecap="round"
|
||||||
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"/>
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button className="p-1 border border-red-200 rounded-md hover:bg-red-50">
|
<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">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/>
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,22 +455,24 @@ function AlertItem({
|
|||||||
time,
|
time,
|
||||||
message,
|
message,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string
|
||||||
severity: 'high' | 'medium' | 'low';
|
severity: "high" | "medium" | "low"
|
||||||
time: string;
|
time: string
|
||||||
message: string;
|
message: string
|
||||||
}) {
|
}) {
|
||||||
const severityConfig = {
|
const severityConfig = {
|
||||||
high: {color: 'bg-red-50 border-red-200', dot: 'bg-red-500'},
|
high: { color: "bg-red-50 border-red-200", dot: "bg-red-500" },
|
||||||
medium: {color: 'bg-yellow-50 border-yellow-200', dot: 'bg-yellow-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'},
|
low: { color: "bg-blue-50 border-blue-200", dot: "bg-blue-500" },
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`p-3 rounded-md ${severityConfig[severity].color} border`}>
|
<div className={`p-3 rounded-md ${severityConfig[severity].color} border`}>
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className={`h-2 w-2 rounded-full ${severityConfig[severity].dot} mr-2`}></span>
|
<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>
|
<span className="font-medium text-gray-800 text-sm">{title}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500">{time}</span>
|
<span className="text-xs text-gray-500">{time}</span>
|
||||||
@@ -407,14 +488,14 @@ function StatusBar({
|
|||||||
value,
|
value,
|
||||||
status,
|
status,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string
|
||||||
value: number;
|
value: number
|
||||||
status: 'normal' | 'warning' | 'error';
|
status: "normal" | "warning" | "error"
|
||||||
}) {
|
}) {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
normal: {color: 'bg-green-500', bgColor: 'bg-green-100'},
|
normal: { color: "bg-green-500", bgColor: "bg-green-100" },
|
||||||
warning: {color: 'bg-yellow-500', bgColor: 'bg-yellow-100'},
|
warning: { color: "bg-yellow-500", bgColor: "bg-yellow-100" },
|
||||||
error: {color: 'bg-red-500', bgColor: 'bg-red-100'},
|
error: { color: "bg-red-500", bgColor: "bg-red-100" },
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -423,7 +504,9 @@ function StatusBar({
|
|||||||
<span className="text-sm text-gray-700">{title}</span>
|
<span className="text-sm text-gray-700">{title}</span>
|
||||||
<span className="text-sm font-medium">{value}%</span>
|
<span className="text-sm font-medium">{value}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`w-full h-2 ${statusConfig[status].bgColor} rounded-full`}>
|
<div
|
||||||
|
className={`w-full h-2 ${statusConfig[status].bgColor} rounded-full`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`h-2 ${statusConfig[status].color} rounded-full`}
|
className={`h-2 ${statusConfig[status].color} rounded-full`}
|
||||||
style={{ width: `${value}%` }}
|
style={{ width: `${value}%` }}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function NavItem({ href, icon: Icon, label }: NavItemProps) {
|
|||||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className={`h-5 w-5 ${collapsed ? "" : "flex-shrink-0"}`} />
|
<Icon className={`h-5 w-5 ${collapsed ? "" : "shrink-0"}`} />
|
||||||
{!collapsed && <span className="ml-3 font-medium text-sm">{label}</span>}
|
{!collapsed && <span className="ml-3 font-medium text-sm">{label}</span>}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
@@ -175,13 +175,13 @@ export default function Navigation() {
|
|||||||
<NavItem href="/statistics" icon={BarChart3} label="数据统计" />
|
<NavItem href="/statistics" icon={BarChart3} label="数据统计" />
|
||||||
</NavGroup>
|
</NavGroup>
|
||||||
|
|
||||||
<NavSeparator />
|
{/*<NavSeparator />*/}
|
||||||
|
|
||||||
{/* IP 资源 */}
|
{/* IP 资源 */}
|
||||||
<NavGroup title="IP 资源">
|
{/*<NavGroup title="IP 资源">
|
||||||
<NavItem href="/proxy/nodes" icon={Globe} label="节点列表" />
|
<NavItem href="/proxy/nodes" icon={Globe} label="节点列表" />
|
||||||
<NavItem href="/proxy/pools" icon={Server} label="IP池管理" />
|
<NavItem href="/proxy/pools" icon={Server} label="IP池管理" />
|
||||||
</NavGroup>
|
</NavGroup>*/}
|
||||||
|
|
||||||
<NavSeparator />
|
<NavSeparator />
|
||||||
|
|
||||||
@@ -205,9 +205,9 @@ export default function Navigation() {
|
|||||||
|
|
||||||
{/* 系统 */}
|
{/* 系统 */}
|
||||||
<NavGroup title="系统">
|
<NavGroup title="系统">
|
||||||
<NavItem href="/settings" icon={Settings} label="系统设置" />
|
{/*<NavItem href="/settings" icon={Settings} label="系统设置" />*/}
|
||||||
<NavItem href="/security" icon={Shield} label="安全管理" />
|
<NavItem href="/security" icon={Shield} label="管理员" />
|
||||||
<NavItem href="/logs" icon={FileText} label="系统日志" />
|
{/*<NavItem href="/logs" icon={FileText} label="系统日志" />*/}
|
||||||
</NavGroup>
|
</NavGroup>
|
||||||
</nav>
|
</nav>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
import { getPageUsers } from "@/actions/user"
|
import { bindAdmin, getPageUsers } from "@/actions/user"
|
||||||
import { DataTable, useDataTable } from "@/components/data-table"
|
import { DataTable, useDataTable } from "@/components/data-table"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { useFetch } from "@/hooks/data"
|
||||||
import type { User } from "@/models/user"
|
import type { User } from "@/models/user"
|
||||||
|
|
||||||
export default function UserPage() {
|
export default function UserPage() {
|
||||||
const table = useDataTable<User>((page, size) => getPageUsers({ page, size }))
|
const table = useDataTable<User>((page, size) => getPageUsers({ page, size }))
|
||||||
|
const bind = useFetch((id: number) => bindAdmin({ id }), {
|
||||||
|
done: "用户已认领",
|
||||||
|
fail: "用户认领失败",
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
@@ -27,10 +31,14 @@ export default function UserPage() {
|
|||||||
{ header: "创建时间", accessorKey: "created_at" },
|
{ header: "创建时间", accessorKey: "created_at" },
|
||||||
{
|
{
|
||||||
header: "操作",
|
header: "操作",
|
||||||
cell: () => (
|
cell: ctx => (
|
||||||
<div>
|
<Button
|
||||||
<Button>认领</Button>
|
size={"sm"}
|
||||||
</div>
|
onClick={() => bind(ctx.row.original.id)}
|
||||||
|
disabled={ctx.row.original.admin_id !== null}
|
||||||
|
>
|
||||||
|
{ctx.row.original.admin_id !== null ? "已认领" : "认领"}
|
||||||
|
</Button>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -1,5 +1,42 @@
|
|||||||
import { useState } from "react"
|
import {
|
||||||
|
type Dispatch,
|
||||||
|
type SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import type { ApiResponse } from "@/lib/api"
|
||||||
|
|
||||||
export function useStatus() {
|
export function useStatus() {
|
||||||
return useState<"load" | "fail" | "done">("load")
|
return useState<"load" | "fail" | "done">("load")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useFetch<TArgs extends unknown[], TResult>(
|
||||||
|
fetchData: (...args: TArgs) => Promise<ApiResponse<TResult>>,
|
||||||
|
messages: {
|
||||||
|
done?: string
|
||||||
|
fail?: string
|
||||||
|
},
|
||||||
|
setStatus?: Dispatch<SetStateAction<"load" | "fail" | "done">>,
|
||||||
|
) {
|
||||||
|
return useCallback(
|
||||||
|
async (...args: TArgs) => {
|
||||||
|
try {
|
||||||
|
setStatus?.("load")
|
||||||
|
const resp = await fetchData(...args)
|
||||||
|
if (!resp.success) {
|
||||||
|
throw new Error(resp.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus?.("done")
|
||||||
|
toast.success(messages.done || "获取数据成功")
|
||||||
|
} catch (e) {
|
||||||
|
setStatus?.("fail")
|
||||||
|
toast.error(messages.fail || "获取数据失败", {
|
||||||
|
description: (e as Error).message || "未知错误",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchData, setStatus, messages],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user