Spaces:
Sleeping
Sleeping
File size: 8,475 Bytes
b88ce1b 6fdc899 b88ce1b 6fdc899 b88ce1b 6fdc899 b88ce1b 6fdc899 b88ce1b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import { Activity, Users, Key, BarChart3, ArrowRight, CheckCircle2, XCircle } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
export default function Dashboard() {
const { token } = useAuth();
const [stats, setStats] = useState({
keys: 0,
tokens: 0,
keyRequests: 0,
tokenEnabled: 0,
tokenDisabled: 0,
todayRequests: 0
});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const headers = { 'X-Admin-Token': token };
const [keysRes, tokensRes, keyStatsRes, tokenStatsRes, todayReqRes] = await Promise.all([
fetch('/admin/keys', { headers }),
fetch('/admin/tokens', { headers }),
fetch('/admin/keys/stats', { headers }),
fetch('/admin/tokens/stats', { headers }),
fetch('/admin/today-requests', { headers })
]);
const keys = await keysRes.json();
const tokens = await tokensRes.json();
const keyStats = await keyStatsRes.json();
const tokenStats = await tokenStatsRes.json();
const todayReq = await todayReqRes.json();
setStats({
keys: keys.length,
tokens: tokens.length,
keyRequests: keyStats.totalRequests || 0,
tokenEnabled: tokenStats.enabled || 0,
tokenDisabled: tokenStats.disabled || 0,
todayRequests: todayReq.todayRequests || 0
});
} catch (error) {
console.error('Failed to fetch dashboard data', error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [token]);
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};
return (
<motion.div
variants={container}
initial="hidden"
animate="show"
className="space-y-8"
>
{/* Welcome Section */}
<motion.div variants={item} className="relative overflow-hidden rounded-2xl bg-zinc-900 p-8 text-white shadow-lg">
<div className="absolute top-0 right-0 -mt-4 -mr-4 h-32 w-32 rounded-full bg-zinc-800 blur-2xl opacity-50" />
<div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-32 w-32 rounded-full bg-zinc-800 blur-2xl opacity-50" />
<div className="relative z-10">
<h2 className="text-2xl font-semibold mb-2 tracking-tight">欢迎回来, 管理员</h2>
<p className="text-zinc-400 text-base max-w-2xl">
Antigravity API 网关正在平稳运行。这里是您的控制中心,您可以管理 Token、密钥并监控系统状态。
</p>
</div>
</motion.div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatsCard
title="服务状态"
value="运行中"
icon={Activity}
color="text-emerald-600"
bg="bg-emerald-50"
subtext="系统正常"
/>
<StatsCard
title="Token 账号"
value={stats.tokens}
icon={Users}
color="text-blue-600"
bg="bg-blue-50"
subtext={
<div className="flex gap-2 text-xs">
<span className="text-emerald-600 flex items-center gap-1"><CheckCircle2 className="w-3 h-3" /> {stats.tokenEnabled}</span>
<span className="text-zinc-400 flex items-center gap-1"><XCircle className="w-3 h-3" /> {stats.tokenDisabled}</span>
</div>
}
/>
<StatsCard
title="API 密钥"
value={stats.keys}
icon={Key}
color="text-violet-600"
bg="bg-violet-50"
subtext={`总请求: ${stats.keyRequests}`}
/>
<StatsCard
title="今日请求"
value={stats.todayRequests}
icon={BarChart3}
color="text-orange-600"
bg="bg-orange-50"
subtext="实时统计"
/>
</div>
{/* Quick Start */}
<motion.div variants={item} className="bg-white rounded-xl border border-zinc-200 p-8 shadow-sm">
<h3 className="text-lg font-semibold text-zinc-900 mb-6">快速开始指南</h3>
<div className="grid gap-6 md:grid-cols-2">
<QuickStartStep
number="01"
title="获取 Google Token"
desc="在 Token 管理页面登录 Google 账号获取 Access Token,这是服务的核心凭证。"
link="/tokens"
/>
<QuickStartStep
number="02"
title="生成 API 密钥"
desc="在密钥管理页面生成对外服务的 API Key,支持设置频率限制。"
link="/keys"
/>
<QuickStartStep
number="03"
title="测试接口"
desc="使用 API 测试工具验证接口连通性,确保模型回复正常。"
link="/test"
/>
<QuickStartStep
number="04"
title="监控日志"
desc="在日志页面查看实时请求记录,排查潜在问题。"
link="/logs"
/>
</div>
</motion.div>
</motion.div>
);
}
function StatsCard({ title, value, icon: Icon, color, bg, subtext }) {
return (
<motion.div
variants={{
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}}
className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm hover:shadow-md transition-all duration-300 group"
>
<div className="flex justify-between items-start mb-4">
<div className={`p-3 rounded-lg ${bg} ${color} group-hover:scale-105 transition-transform duration-300`}>
<Icon className="w-5 h-5" />
</div>
</div>
<h3 className="text-zinc-500 text-xs font-medium mb-1 uppercase tracking-wider">{title}</h3>
<div className="text-2xl font-bold text-zinc-900 mb-2">{value}</div>
<div className="text-xs text-zinc-400">{subtext}</div>
</motion.div>
);
}
function QuickStartStep({ number, title, desc, link }) {
return (
<Link to={link} className="flex gap-4 p-4 rounded-xl hover:bg-zinc-50 transition-colors group cursor-pointer border border-transparent hover:border-zinc-100">
<div className="text-3xl font-bold text-zinc-100 group-hover:text-zinc-200 transition-colors">
{number}
</div>
<div>
<h4 className="text-base font-semibold text-zinc-900 mb-1 group-hover:text-zinc-900 transition-colors flex items-center gap-2">
{title}
<ArrowRight className="w-4 h-4 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all text-zinc-400" />
</h4>
<p className="text-zinc-500 text-sm leading-relaxed">
{desc}
</p>
</div>
</Link>
);
}
|