From 2fa27f54ac13e04467841602e30a1327db232bce Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 18 May 2026 21:29:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BD=AC=E5=8C=96=E6=BC=8F?= =?UTF-8?q?=E6=96=97=EF=BC=9A=E5=89=8D=E5=90=8E=E7=AB=AF=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=AF=B9=E9=BD=90=EF=BC=8C=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=20events/rates=20=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ops/analytics/AnalyticsPageClient.tsx | 4 ++- .../ops/overview/OverviewPageClient.tsx | 6 ++-- frontend/lib/ops-api.ts | 30 +++++++++++++++++-- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/frontend/components/ops/analytics/AnalyticsPageClient.tsx b/frontend/components/ops/analytics/AnalyticsPageClient.tsx index d066d8cd..eb17ccde 100644 --- a/frontend/components/ops/analytics/AnalyticsPageClient.tsx +++ b/frontend/components/ops/analytics/AnalyticsPageClient.tsx @@ -21,13 +21,15 @@ type FunnelStep = { label: string; count: number; pct_of_prev?: number }; export function AnalyticsPageClient() { const [loading, setLoading] = useState(true); const [funnel, setFunnel] = useState([]); + const [rates, setRates] = useState | null>(null); const [days, setDays] = useState(30); const load = async () => { setLoading(true); try { const data = await opsApi.funnel(days); - setFunnel((data as unknown as { steps?: FunnelStep[] }).steps ?? []); + setFunnel(data.steps); + setRates(data.rates ?? null); } catch { /* */ } setLoading(false); }; diff --git a/frontend/components/ops/overview/OverviewPageClient.tsx b/frontend/components/ops/overview/OverviewPageClient.tsx index a8034061..2fc13b09 100644 --- a/frontend/components/ops/overview/OverviewPageClient.tsx +++ b/frontend/components/ops/overview/OverviewPageClient.tsx @@ -7,7 +7,7 @@ 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, FunnelPayload, MembershipEntry } from "@/types/ops"; +import type { SystemStatusPayload, MembershipsPayload, MembershipEntry } from "@/types/ops"; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, BarChart, Bar, XAxis, YAxis, CartesianGrid, @@ -36,7 +36,7 @@ export function OverviewPageClient() { const [loading, setLoading] = useState(true); const [status, setStatus] = useState(null); const [memberships, setMemberships] = useState([]); - const [funnel, setFunnel] = useState(null); + const [funnel, setFunnel] = useState<{ steps: { label: string; count: number; pct_of_prev?: number }[] } | null>(null); const load = async () => { setLoading(true); @@ -44,7 +44,7 @@ export function OverviewPageClient() { const [s, m, f] = await Promise.all([ opsApi.systemStatus() as Promise, opsApi.memberships() as Promise, - opsApi.funnel(30) as Promise, + opsApi.funnel(30), ]); setStatus(s); setMemberships((m as MembershipsPayload).memberships ?? []); diff --git a/frontend/lib/ops-api.ts b/frontend/lib/ops-api.ts index 11c8cb15..30b58613 100644 --- a/frontend/lib/ops-api.ts +++ b/frontend/lib/ops-api.ts @@ -19,8 +19,34 @@ export const opsApi = { paymentRuntime() { return opsFetch>("/api/payments/runtime"); }, - funnel(days = 30) { - return opsFetch>(`/api/ops/analytics/funnel?days=${days}`); + async funnel(days = 30) { + const raw = await opsFetch<{ + events?: Record; + rates?: Record; + window_days?: number; + }>(`/api/ops/analytics/funnel?days=${days}`); + const stepOrder = ["signup_completed", "dashboard_active", "paywall_feature_clicked", "paywall_viewed", "checkout_started", "checkout_succeeded"]; + const stepLabels: Record = { + signup_completed: "注册", + dashboard_active: "活跃", + paywall_feature_clicked: "点击付费", + paywall_viewed: "看到入口", + checkout_started: "发起支付", + checkout_succeeded: "支付成功", + }; + const steps = stepOrder.map((key, i) => { + const evt = raw?.events?.[key]; + const count = evt?.total ?? 0; + let pct_of_prev: number | undefined; + if (i > 0) { + const prevCount = raw?.events?.[stepOrder[i - 1]]?.total ?? 0; + pct_of_prev = prevCount > 0 ? Math.round((count / prevCount) * 100) : 0; + } else { + pct_of_prev = 100; + } + return { label: stepLabels[key] ?? key, count, pct_of_prev }; + }); + return { steps, rates: raw?.rates, window_days: raw?.window_days }; }, users(q: string, limit = 20) { return opsFetch>(`/api/ops/users?q=${encodeURIComponent(q)}&limit=${limit}`);