"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}
真值行数
)}
);
}