'use client'; import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Activity, HardDrive, Users } from 'lucide-react'; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig, } from '@/components/ui/chart'; import { Skeleton } from '@/components/ui/skeleton'; import { ZoneService, zoneQueryKey, type ZoneOverview, type ZoneStatsRange, } from '@/lib/services/openflare'; import { formatDateTime } from '@/lib/utils'; import { formatBytes, formatCompactNumber } from '@/lib/utils/metrics'; import { cn } from '@/lib/utils'; const rangeOptions: Array<{ value: ZoneStatsRange; label: string }> = [ { value: '24h', label: '24 小时' }, { value: '7d', label: '7 天' }, { value: '30d', label: '30 天' }, ]; const visitorsChartConfig = { value: { label: '唯一访问者', color: 'hsl(217 91% 60%)' }, } satisfies ChartConfig; const requestsChartConfig = { value: { label: '请求总数', color: 'hsl(217 91% 60%)' }, } satisfies ChartConfig; const bytesChartConfig = { value: { label: '已提供数据', color: 'hsl(217 91% 60%)' }, } satisfies ChartConfig; function formatAxisLabel(iso: string, range: ZoneStatsRange) { const date = new Date(iso); if (Number.isNaN(date.getTime())) { return ''; } if (range === '24h') { return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: undefined, hour12: true, }); } return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', }); } function formatWindowLabel(startedAt?: string, endedAt?: string) { if (!startedAt || !endedAt) { return '—'; } const start = new Date(startedAt); const end = new Date(endedAt); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { return '—'; } const fmt = (value: Date) => value .toLocaleDateString('en-GB', { day: 'numeric', month: 'long' }) .toUpperCase(); return `${fmt(start)} — ${fmt(end)}`; } export function ZoneOverviewPanel({ overview, zoneId, }: { overview: ZoneOverview; zoneId: number; }) { const [range, setRange] = useState('24h'); const statsQuery = useQuery({ queryKey: [...zoneQueryKey, zoneId, 'stats', range], queryFn: () => ZoneService.getStats(zoneId, range), enabled: zoneId > 0, }); const stats = statsQuery.data; const chartData = useMemo( () => (stats?.series ?? []).map((point) => ({ label: formatAxisLabel(point.bucket_started_at, range), at: point.bucket_started_at, visitors: point.unique_visitors, requests: point.request_count, bytes: point.bytes_sent, })), [range, stats?.series], ); return (
{rangeOptions.map((option) => ( ))}

{formatWindowLabel( stats?.window_started_at, stats?.window_ended_at, )}

{statsQuery.isLoading ? (
{Array.from({ length: 3 }).map((_, index) => ( ))}
) : statsQuery.isError ? (
加载流量统计失败
) : (
{!stats?.available ? (

分析存储暂不可用,图表可能为空。请确认 ClickHouse 已启用。

) : null} formatBytes(value, { zeroText: '0 B' }) } />
)}
Zone 信息

根域

{overview.zone.domain}

创建时间

{formatDateTime(overview.zone.created_at)}

); } function MetricTrendCard({ icon: Icon, label, value, data, dataKey, config, gradientId, valueFormatter, }: { icon: typeof Users; label: string; value: string; data: Array>; dataKey: string; config: ChartConfig; gradientId: string; valueFormatter?: (value: number) => string; }) { return (
{label}

{value}

valueFormatter ? valueFormatter(Number(tick)) : formatCompactNumber(Number(tick)) } /> { const numeric = Number(raw); return valueFormatter ? valueFormatter(numeric) : formatCompactNumber(numeric); }} /> } />
); }