/* Copyright 2026 Arctel.net Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ "use client" import {useCallback, useEffect, useState} from "react" import {BarChart3, Globe, RefreshCw, TrendingUp, Users, XCircle} from "lucide-react" import {Area, AreaChart, CartesianGrid, XAxis, YAxis} from "recharts" import services from "@/lib/services" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" import {Badge} from "@/components/ui/badge" import {Button} from "@/components/ui/button" import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from "@/components/ui/chart" import {Spinner} from "@/components/ui/spinner" interface TrendData { date: string count: number } interface BrowserData { browser: string count: number } interface TopUserData { user_id: string username: string nickname: string count: number } const chartConfig = { count: { label: "请求量", color: "hsl(var(--primary))", }, } satisfies ChartConfig // Format YYYY-MM-DD to MM/DD function formatDateLabel(dateStr: string) { if (!dateStr || dateStr.length < 10) return dateStr const parts = dateStr.split("-") if (parts.length === 3) { return `${parts[1]}/${parts[2]}` } return dateStr } export function AccessAnalytics() { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [clickhouseDisabled, setClickhouseDisabled] = useState(false) const [trend, setTrend] = useState([]) const [browsers, setBrowsers] = useState([]) const [topUsers, setTopUsers] = useState([]) const fetchAnalytics = useCallback(async () => { try { setLoading(true) setError(null) const data = await services.adminLog.getLogsAnalytics() // Formats the date labels for the X-axis representation const formattedTrend = (data.trend || []).map(item => ({ ...item, formattedDate: formatDateLabel(item.date) })) setTrend(formattedTrend) setBrowsers(data.browsers || []) setTopUsers(data.top_users || []) setClickhouseDisabled(false) } catch (err) { const errorInstance = err instanceof Error ? err : new Error("获取数据统计失败") const errMsg = errorInstance.message || "" if (errMsg.includes("ClickHouse") || errMsg.includes("未启用")) { setClickhouseDisabled(true) } else { setError(errorInstance) } } finally { setLoading(false) } }, []) useEffect(() => { fetchAnalytics() }, [fetchAnalytics]) const totalBrowserRequests = browsers.reduce((sum, item) => sum + item.count, 0) const totalTrendRequests = trend.reduce((sum, item) => sum + item.count, 0) if (clickhouseDisabled) { return (

ClickHouse 未启用

当前系统配置未启用 ClickHouse 存储,系统不会收集用户访问日志。如需使用此功能,请在后端 `config.yaml` 配置文件中配置并启用 ClickHouse。

) } if (error) { return } if (loading) { return } return (
{/* Overview Cards */}
近 7 天总请求数
{totalTrendRequests.toLocaleString()}

系统记录的所有成功认证的访问总频次

活跃终端分类数
{browsers.length}

在一周内发起请求的浏览器代理大类统计

活跃独立用户数
{topUsers.length}

一周内累计发起高频请求的注册账户总量

{/* Access Trend Chart */}
一周访问量趋势 展现系统最近 7 天内的每日 API 请求曲线
{trend.length === 0 ? ( ) : (
} />
)}
{/* Two Columns for Ranking Statistics */}
{/* Browser Rankings */} 使用的浏览器排行 基于请求头 User-Agent 智能分类的一周占比统计 {browsers.length === 0 ? ( ) : (
{browsers.map((item, index) => { const percent = totalBrowserRequests > 0 ? (item.count / totalBrowserRequests) * 100 : 0 return (
{index + 1} {item.browser} {item.count.toLocaleString()} 次 ({percent.toFixed(1)}%)
) })}
)} {/* Top Users */} 最活跃的用户排行 (Top 10) 统计最近一周发起接口访问请求数量最多的账户 {topUsers.length === 0 ? ( ) : (
{topUsers.map((user) => (
{(user.username || "U").slice(0, 1).toUpperCase()}
{user.username || "未知"}
{user.nickname ? `(${user.nickname})` : "(无昵称)"} | ID: {user.user_id}
{user.count.toLocaleString()}
次请求
))}
)}
) }