"use client"; import { useEffect, useState } from "react"; import { RefreshCcw, TrendingUp, Users, CreditCard, Database, Activity, Cpu } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { opsApi } from "@/lib/ops-api"; import Link from "next/link"; import type { SystemStatusPayload, MembershipsPayload, MembershipEntry } from "@/types/ops"; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, BarChart, Bar, XAxis, YAxis, CartesianGrid, AreaChart, Area, Legend, } from "recharts"; function KpiCard({ href, icon: Icon, label, value, color, sub }: { href: string; icon: React.ElementType; label: string; value: string | number; color: string; sub?: string; }) { return ( {label} {value} {sub ? {sub} : null} ); } export function OverviewPageClient() { const [loading, setLoading] = useState(true); const [status, setStatus] = useState(null); const [memberships, setMemberships] = useState([]); const [funnel, setFunnel] = useState<{ steps: { label: string; count: number; pct_of_prev?: number }[] } | null>(null); const [growth, setGrowth] = useState<{ date: string; trial: number; paid: number; total: number; cumulative: number }[]>([]); const load = async () => { setLoading(true); try { const [s, m, f, g] = await Promise.all([ opsApi.systemStatus() as Promise, opsApi.memberships() as Promise, opsApi.funnel(30), opsApi.membershipsGrowth(30), ]); setStatus(s); setMemberships((m as MembershipsPayload).memberships ?? []); setFunnel(f); setGrowth(g?.daily ?? []); } catch { /* */ } setLoading(false); }; useEffect(() => { void load(); }, []); if (loading) return ( {Array.from({ length: 5 }).map((_, i) => ( ))} ); // ── Derived data ── const paid = memberships.filter((m) => !m.is_trial).length; const trials = memberships.filter((m) => m.is_trial).length; const steps = funnel?.steps ?? []; const totalUsers = steps[0]?.count ?? 0; const payingUsers = steps[5]?.count ?? 0; const convRate = totalUsers > 0 ? ((payingUsers / totalUsers) * 100).toFixed(1) : "—"; const cache = status?.cache; const cacheAnalysis = cache?.analysis; const td = status?.training_data; const features = status?.features; // Cache bucket data for bar chart const cacheBuckets = cache ? [ { name: "API", value: cache.api_cache_entries ?? 0 }, { name: "预报", value: cache.open_meteo_forecast_entries ?? 0 }, { name: "METAR", value: cache.metar_entries ?? 0 }, { name: "TAF", value: cache.taf_entries ?? 0 }, { name: "结算", value: cache.settlement_entries ?? 0 }, ].filter((d) => d.value > 0) : []; // Cache pie const cachePie = cacheAnalysis ? [ { name: "命中", value: cacheAnalysis.cache_hits ?? 0, color: "#22c55e" }, { name: "未命中", value: cacheAnalysis.cache_misses ?? 0, color: "#f59e0b" }, { name: "强制刷新", value: cacheAnalysis.force_refresh_requests ?? 0, color: "#3b82f6" }, ] : []; // Membership pie const memberPie = [ { name: "付费", value: paid, color: "#22c55e" }, ...(trials > 0 ? [{ name: "体验", value: trials, color: "#f59e0b" }] : []), ]; // Plan breakdown const planCounts: Record = {}; memberships.forEach((m) => { const code = m.plan_code ?? "unknown"; planCounts[code] = (planCounts[code] ?? 0) + 1; }); const planBreakdown = Object.entries(planCounts) .map(([k, v]) => { const label = k.startsWith("signup_trial") ? "3天体验" : k === "pro_monthly" ? "月付" : k === "pro_quarterly" ? "季付" : k === "pro_yearly" ? "年付" : k; return { name: label, value: v }; }) .sort((a, b) => b.value - a.value); const truthRows = td?.truth_records?.row_count ?? 0; const coverage = td?.city_coverage; return ( 总览 系统实时数据快照 刷新 {/* KPI row */} 0 ? `+${trials} 体验` : undefined} /> {/* Membership Growth Trend */} {growth.length > 0 && ( 会员增长趋势 — 近 30 天 {/* Cumulative area chart */} 累计会员 {/* Daily stack chart */} 每日新增 )} {/* Main grid: 2 columns */} {/* Column 1 */} {/* Funnel mini chart */} {steps.length > 0 && ( 30天转化漏斗 ({ name: s.label, count: s.count }))} layout="vertical" margin={{ left: 80, right: 20 }}> )} {/* Cache buckets */} {cacheBuckets.length > 0 && ( 缓存桶分布 )} {/* Features */} {features && ( 功能开关 {Object.entries(features).map(([k, v]) => ( {k.replace(/_/g, " ")}: {String(v)} ))} )} {/* Column 2 */} {/* Membership donut + plan breakdown side by side */} 会员分布 {memberPie.map((d, i) => ())} {memberPie.map((d) => ( {d.name} {d.value} ))} {planBreakdown.length > 0 && ( {planBreakdown.map((p) => ( {p.name} {p.value} ))} )} {/* Cache hit rate donut */} {cachePie.length > 0 && ( 缓存分析{" "} {cacheAnalysis?.hit_rate != null && ( {(cacheAnalysis.hit_rate * 100).toFixed(1)}% 命中 )} {cachePie.map((d, i) => ())} 总请求 {cacheAnalysis?.total_requests ?? 0} 强制刷新 {cacheAnalysis?.force_refresh_requests ?? 0} 命中 {cacheAnalysis?.cache_hits ?? 0} 未命中 {cacheAnalysis?.cache_misses ?? 0} )} {/* Training data summary */} {coverage && ( 城市模型覆盖 {coverage.total_cities ?? 0} 总城市 {coverage.with_truth_rows ?? 0} 有真值 {coverage.with_feature_rows ?? 0} 有特征 {td?.truth_records?.row_count ?? 0} 真值行数 )} ); }
系统实时数据快照