"use client"; import { useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { Activity, Cpu, CreditCard, Database, RefreshCcw, TrendingUp, Users } from "lucide-react"; import Link from "next/link"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { opsApi } from "@/lib/ops-api"; import type { MembershipEntry, MembershipsPayload, SystemStatusPayload } from "@/types/ops"; const OverviewCharts = dynamic( () => import("./OverviewCharts").then((mod) => mod.OverviewCharts), { ssr: false, loading: () =>
, }, ); 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: { key?: string; label: string; count: number; pct_of_prev?: number; uniqueActors?: 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] = await Promise.all([ opsApi.systemStatus() as Promise, opsApi.membershipsOverview(200, 30) as Promise, opsApi.funnel(30), ]); setStatus(s); setMemberships((m as MembershipsPayload).memberships ?? []); setFunnel(f); setGrowth(m?.daily ?? []); } catch { /* */ } setLoading(false); }; useEffect(() => { void load(); }, []); if (loading) return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); 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 stepByKey = Object.fromEntries(steps.map((step) => [step.key || step.label, step])); const payingUsers = stepByKey.payment_success?.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; const coverage = td?.city_coverage; const truthRows = td?.truth_records?.row_count ?? 0; 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) : []; 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" }, ] : []; const memberPie = [ { name: "付费", value: paid, color: "#22c55e" }, ...(trials > 0 ? [{ name: "体验", value: trials, color: "#f59e0b" }] : []), ]; 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); return (

总览

系统实时数据快照

0 ? `+${trials} 体验` : undefined} />
{features && ( 功能开关
{Object.entries(features).map(([k, v]) => ( {k.replace(/_/g, " ")}: {String(v)} ))}
)} {coverage && ( 城市模型覆盖
{coverage.total_cities ?? 0}
总城市
{coverage.with_truth_rows ?? 0}
有真值
{coverage.with_feature_rows ?? 0}
有特征
{td?.truth_records?.row_count ?? 0}
真值行数
)}
); }