mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 09:46:37 +08:00
prettier
This commit is contained in:
@@ -14,209 +14,282 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use client"
|
||||
'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 { 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"
|
||||
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
|
||||
date: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface BrowserData {
|
||||
browser: string
|
||||
count: number
|
||||
browser: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface TopUserData {
|
||||
user_id: string
|
||||
username: string
|
||||
nickname: string
|
||||
count: number
|
||||
user_id: string;
|
||||
username: string;
|
||||
nickname: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
count: {
|
||||
label: "请求量",
|
||||
color: "hsl(var(--primary))",
|
||||
label: '请求量',
|
||||
color: 'hsl(var(--primary))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
} 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 (!dateStr || dateStr.length < 10) return dateStr;
|
||||
const parts = dateStr.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[1]}/${parts[2]}`
|
||||
return `${parts[1]}/${parts[2]}`;
|
||||
}
|
||||
return dateStr
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
export function AccessAnalytics() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [clickhouseDisabled, setClickhouseDisabled] = useState(false)
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [clickhouseDisabled, setClickhouseDisabled] = useState(false);
|
||||
|
||||
const [trend, setTrend] = useState<TrendData[]>([])
|
||||
const [browsers, setBrowsers] = useState<BrowserData[]>([])
|
||||
const [topUsers, setTopUsers] = useState<TopUserData[]>([])
|
||||
const [trend, setTrend] = useState<TrendData[]>([]);
|
||||
const [browsers, setBrowsers] = useState<BrowserData[]>([]);
|
||||
const [topUsers, setTopUsers] = useState<TopUserData[]>([]);
|
||||
|
||||
const fetchAnalytics = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const data = await services.adminLog.getLogsAnalytics()
|
||||
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 => ({
|
||||
const formattedTrend = (data.trend || []).map((item) => ({
|
||||
...item,
|
||||
formattedDate: formatDateLabel(item.date)
|
||||
}))
|
||||
formattedDate: formatDateLabel(item.date),
|
||||
}));
|
||||
|
||||
setTrend(formattedTrend)
|
||||
setBrowsers(data.browsers || [])
|
||||
setTopUsers(data.top_users || [])
|
||||
setClickhouseDisabled(false)
|
||||
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)
|
||||
const errorInstance =
|
||||
err instanceof Error ? err : new Error('获取数据统计失败');
|
||||
const errMsg = errorInstance.message || '';
|
||||
if (errMsg.includes('ClickHouse') || errMsg.includes('未启用')) {
|
||||
setClickhouseDisabled(true);
|
||||
} else {
|
||||
setError(errorInstance)
|
||||
setError(errorInstance);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAnalytics()
|
||||
}, [fetchAnalytics])
|
||||
fetchAnalytics();
|
||||
}, [fetchAnalytics]);
|
||||
|
||||
const totalBrowserRequests = browsers.reduce((sum, item) => sum + item.count, 0)
|
||||
const totalTrendRequests = trend.reduce((sum, item) => sum + item.count, 0)
|
||||
const totalBrowserRequests = browsers.reduce(
|
||||
(sum, item) => sum + item.count,
|
||||
0,
|
||||
);
|
||||
const totalTrendRequests = trend.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
if (clickhouseDisabled) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-8 border border-dashed rounded-lg bg-card text-center my-6 min-h-[300px]">
|
||||
<XCircle className="size-10 text-muted-foreground mb-3" />
|
||||
<h3 className="text-base font-semibold">ClickHouse 未启用</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 max-w-[400px] mb-4">
|
||||
当前系统配置未启用 ClickHouse 存储,系统不会收集用户访问日志。如需使用此功能,请在后端 `config.yaml` 配置文件中配置并启用 ClickHouse。
|
||||
<div className='flex flex-col items-center justify-center p-8 border border-dashed rounded-lg bg-card text-center my-6 min-h-[300px]'>
|
||||
<XCircle className='size-10 text-muted-foreground mb-3' />
|
||||
<h3 className='text-base font-semibold'>ClickHouse 未启用</h3>
|
||||
<p className='text-sm text-muted-foreground mt-1 max-w-[400px] mb-4'>
|
||||
当前系统配置未启用 ClickHouse
|
||||
存储,系统不会收集用户访问日志。如需使用此功能,请在后端 `config.yaml`
|
||||
配置文件中配置并启用 ClickHouse。
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorInline error={error} onRetry={fetchAnalytics} />
|
||||
return <ErrorInline error={error} onRetry={fetchAnalytics} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <LoadingStateWithBorder icon={BarChart3} description="加载访问统计指标中..." />
|
||||
return (
|
||||
<LoadingStateWithBorder
|
||||
icon={BarChart3}
|
||||
description='加载访问统计指标中...'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className='space-y-6'>
|
||||
{/* Overview Cards */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Card className="bg-card/25 border-border/40">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardTitle className="text-sm font-semibold tracking-tight">近 7 天总请求数</CardTitle>
|
||||
<TrendingUp className="size-4 text-muted-foreground" />
|
||||
<div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
<Card className='bg-card/25 border-border/40'>
|
||||
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
|
||||
<CardTitle className='text-sm font-semibold tracking-tight'>
|
||||
近 7 天总请求数
|
||||
</CardTitle>
|
||||
<TrendingUp className='size-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold font-mono">{totalTrendRequests.toLocaleString()}</div>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">系统记录的所有成功认证的访问总频次</p>
|
||||
<div className='text-2xl font-bold font-mono'>
|
||||
{totalTrendRequests.toLocaleString()}
|
||||
</div>
|
||||
<p className='text-[10px] text-muted-foreground mt-1'>
|
||||
系统记录的所有成功认证的访问总频次
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-card/25 border-border/40">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardTitle className="text-sm font-semibold tracking-tight">活跃终端分类数</CardTitle>
|
||||
<Globe className="size-4 text-muted-foreground" />
|
||||
<Card className='bg-card/25 border-border/40'>
|
||||
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
|
||||
<CardTitle className='text-sm font-semibold tracking-tight'>
|
||||
活跃终端分类数
|
||||
</CardTitle>
|
||||
<Globe className='size-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold font-mono">{browsers.length}</div>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">在一周内发起请求的浏览器代理大类统计</p>
|
||||
<div className='text-2xl font-bold font-mono'>
|
||||
{browsers.length}
|
||||
</div>
|
||||
<p className='text-[10px] text-muted-foreground mt-1'>
|
||||
在一周内发起请求的浏览器代理大类统计
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-card/25 border-border/40 sm:col-span-2 lg:col-span-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||
<CardTitle className="text-sm font-semibold tracking-tight">活跃独立用户数</CardTitle>
|
||||
<Users className="size-4 text-muted-foreground" />
|
||||
<Card className='bg-card/25 border-border/40 sm:col-span-2 lg:col-span-1'>
|
||||
<CardHeader className='flex flex-row items-center justify-between pb-2 space-y-0'>
|
||||
<CardTitle className='text-sm font-semibold tracking-tight'>
|
||||
活跃独立用户数
|
||||
</CardTitle>
|
||||
<Users className='size-4 text-muted-foreground' />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold font-mono">{topUsers.length}</div>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">一周内累计发起高频请求的注册账户总量</p>
|
||||
<div className='text-2xl font-bold font-mono'>
|
||||
{topUsers.length}
|
||||
</div>
|
||||
<p className='text-[10px] text-muted-foreground mt-1'>
|
||||
一周内累计发起高频请求的注册账户总量
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Access Trend Chart */}
|
||||
<Card className="bg-card/20 border-border/40">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<Card className='bg-card/20 border-border/40'>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<div>
|
||||
<CardTitle className="text-base font-bold">一周访问量趋势</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
<CardTitle className='text-base font-bold'>
|
||||
一周访问量趋势
|
||||
</CardTitle>
|
||||
<CardDescription className='text-xs'>
|
||||
展现系统最近 7 天内的每日 API 请求曲线
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="size-8" onClick={fetchAnalytics} disabled={loading}>
|
||||
{loading ? <Spinner className="size-4" /> : <RefreshCw className="size-4 text-muted-foreground" />}
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='size-8'
|
||||
onClick={fetchAnalytics}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner className='size-4' />
|
||||
) : (
|
||||
<RefreshCw className='size-4 text-muted-foreground' />
|
||||
)}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="pl-2 pr-4 pt-2">
|
||||
<CardContent className='pl-2 pr-4 pt-2'>
|
||||
{trend.length === 0 ? (
|
||||
<EmptyStateWithBorder icon={TrendingUp} description="暂无趋势数据" />
|
||||
<EmptyStateWithBorder
|
||||
icon={TrendingUp}
|
||||
description='暂无趋势数据'
|
||||
/>
|
||||
) : (
|
||||
<div className="h-[280px] w-full">
|
||||
<ChartContainer config={chartConfig} className="w-full h-full aspect-auto">
|
||||
<div className='h-[280px] w-full'>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className='w-full h-full aspect-auto'
|
||||
>
|
||||
<AreaChart
|
||||
data={trend}
|
||||
margin={{ top: 10, right: 10, left: -20, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-count)" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="var(--color-count)" stopOpacity={0.01} />
|
||||
<linearGradient id='colorCount' x1='0' y1='0' x2='0' y2='1'>
|
||||
<stop
|
||||
offset='5%'
|
||||
stopColor='var(--color-count)'
|
||||
stopOpacity={0.3}
|
||||
/>
|
||||
<stop
|
||||
offset='95%'
|
||||
stopColor='var(--color-count)'
|
||||
stopOpacity={0.01}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||
<CartesianGrid strokeDasharray='3 3' vertical={false} />
|
||||
<XAxis
|
||||
dataKey="formattedDate"
|
||||
dataKey='formattedDate'
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
dy={10}
|
||||
className="font-mono"
|
||||
className='font-mono'
|
||||
/>
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
dx={-10}
|
||||
className="font-mono"
|
||||
className='font-mono'
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke="var(--color-count)"
|
||||
type='monotone'
|
||||
dataKey='count'
|
||||
stroke='var(--color-count)'
|
||||
strokeWidth={2}
|
||||
fillOpacity={1}
|
||||
fill="url(#colorCount)"
|
||||
name="请求次数"
|
||||
fill='url(#colorCount)'
|
||||
name='请求次数'
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
@@ -226,46 +299,56 @@ export function AccessAnalytics() {
|
||||
</Card>
|
||||
|
||||
{/* Two Columns for Ranking Statistics */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className='grid gap-6 md:grid-cols-2'>
|
||||
{/* Browser Rankings */}
|
||||
<Card className="bg-card/20 border-border/40 flex flex-col h-full">
|
||||
<Card className='bg-card/20 border-border/40 flex flex-col h-full'>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-bold flex items-center gap-1.5">
|
||||
<Globe className="size-4.5 text-muted-foreground" />
|
||||
<CardTitle className='text-base font-bold flex items-center gap-1.5'>
|
||||
<Globe className='size-4.5 text-muted-foreground' />
|
||||
使用的浏览器排行
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
<CardDescription className='text-xs'>
|
||||
基于请求头 User-Agent 智能分类的一周占比统计
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1">
|
||||
<CardContent className='flex-1'>
|
||||
{browsers.length === 0 ? (
|
||||
<EmptyStateWithBorder icon={Globe} description="暂无浏览器排行数据" />
|
||||
<EmptyStateWithBorder
|
||||
icon={Globe}
|
||||
description='暂无浏览器排行数据'
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className='space-y-4'>
|
||||
{browsers.map((item, index) => {
|
||||
const percent = totalBrowserRequests > 0 ? (item.count / totalBrowserRequests) * 100 : 0
|
||||
const percent =
|
||||
totalBrowserRequests > 0
|
||||
? (item.count / totalBrowserRequests) * 100
|
||||
: 0;
|
||||
return (
|
||||
<div key={item.browser} className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-xs font-medium">
|
||||
<span className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="px-1.5 py-0 h-5 font-mono text-[10px] select-none">
|
||||
<div key={item.browser} className='space-y-1.5'>
|
||||
<div className='flex items-center justify-between text-xs font-medium'>
|
||||
<span className='flex items-center gap-2'>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='px-1.5 py-0 h-5 font-mono text-[10px] select-none'
|
||||
>
|
||||
{index + 1}
|
||||
</Badge>
|
||||
<span className="font-semibold">{item.browser}</span>
|
||||
<span className='font-semibold'>{item.browser}</span>
|
||||
</span>
|
||||
<span className="text-muted-foreground font-mono text-xs">
|
||||
{item.count.toLocaleString()} 次 ({percent.toFixed(1)}%)
|
||||
<span className='text-muted-foreground font-mono text-xs'>
|
||||
{item.count.toLocaleString()} 次 ({percent.toFixed(1)}
|
||||
%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full rounded-full bg-muted overflow-hidden">
|
||||
<div className='h-2 w-full rounded-full bg-muted overflow-hidden'>
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all duration-500"
|
||||
className='h-full bg-primary rounded-full transition-all duration-500'
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
@@ -273,39 +356,50 @@ export function AccessAnalytics() {
|
||||
</Card>
|
||||
|
||||
{/* Top Users */}
|
||||
<Card className="bg-card/20 border-border/40 flex flex-col h-full">
|
||||
<Card className='bg-card/20 border-border/40 flex flex-col h-full'>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-bold flex items-center gap-1.5">
|
||||
<Users className="size-4.5 text-muted-foreground" />
|
||||
<CardTitle className='text-base font-bold flex items-center gap-1.5'>
|
||||
<Users className='size-4.5 text-muted-foreground' />
|
||||
最活跃的用户排行 (Top 10)
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
<CardDescription className='text-xs'>
|
||||
统计最近一周发起接口访问请求数量最多的账户
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1">
|
||||
<CardContent className='flex-1'>
|
||||
{topUsers.length === 0 ? (
|
||||
<EmptyStateWithBorder icon={Users} description="暂无活跃用户数据" />
|
||||
<EmptyStateWithBorder
|
||||
icon={Users}
|
||||
description='暂无活跃用户数据'
|
||||
/>
|
||||
) : (
|
||||
<div className="divide-y divide-border/40">
|
||||
<div className='divide-y divide-border/40'>
|
||||
{topUsers.map((user) => (
|
||||
<div key={user.user_id} className="flex items-center justify-between py-2.5 first:pt-0 last:pb-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary font-bold text-xs select-none">
|
||||
{(user.username || "U").slice(0, 1).toUpperCase()}
|
||||
<div
|
||||
key={user.user_id}
|
||||
className='flex items-center justify-between py-2.5 first:pt-0 last:pb-0'
|
||||
>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary font-bold text-xs select-none'>
|
||||
{(user.username || 'U').slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
<div className="truncate max-w-[180px] sm:max-w-xs">
|
||||
<div className="text-xs font-bold leading-tight truncate">
|
||||
{user.username || "未知"}
|
||||
<div className='truncate max-w-[180px] sm:max-w-xs'>
|
||||
<div className='text-xs font-bold leading-tight truncate'>
|
||||
{user.username || '未知'}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground leading-normal truncate">
|
||||
{user.nickname ? `(${user.nickname})` : "(无昵称)"} | ID: {user.user_id}
|
||||
<div className='text-[10px] text-muted-foreground leading-normal truncate'>
|
||||
{user.nickname ? `(${user.nickname})` : '(无昵称)'} |
|
||||
ID: {user.user_id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div className="text-xs font-bold font-mono">{user.count.toLocaleString()}</div>
|
||||
<div className="text-[9px] text-muted-foreground uppercase font-bold tracking-wider">次请求</div>
|
||||
<div className='text-right shrink-0'>
|
||||
<div className='text-xs font-bold font-mono'>
|
||||
{user.count.toLocaleString()}
|
||||
</div>
|
||||
<div className='text-[9px] text-muted-foreground uppercase font-bold tracking-wider'>
|
||||
次请求
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -315,5 +409,5 @@ export function AccessAnalytics() {
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user