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>
    );
}