修复转化漏斗:前后端数据格式对齐,正确解析 events/rates 结构
This commit is contained in:
@@ -21,13 +21,15 @@ type FunnelStep = { label: string; count: number; pct_of_prev?: number };
|
|||||||
export function AnalyticsPageClient() {
|
export function AnalyticsPageClient() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [funnel, setFunnel] = useState<FunnelStep[]>([]);
|
const [funnel, setFunnel] = useState<FunnelStep[]>([]);
|
||||||
|
const [rates, setRates] = useState<Record<string, number> | null>(null);
|
||||||
const [days, setDays] = useState(30);
|
const [days, setDays] = useState(30);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await opsApi.funnel(days);
|
const data = await opsApi.funnel(days);
|
||||||
setFunnel((data as unknown as { steps?: FunnelStep[] }).steps ?? []);
|
setFunnel(data.steps);
|
||||||
|
setRates(data.rates ?? null);
|
||||||
} catch { /* */ }
|
} catch { /* */ }
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { opsApi } from "@/lib/ops-api";
|
import { opsApi } from "@/lib/ops-api";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { SystemStatusPayload, MembershipsPayload, FunnelPayload, MembershipEntry } from "@/types/ops";
|
import type { SystemStatusPayload, MembershipsPayload, MembershipEntry } from "@/types/ops";
|
||||||
import {
|
import {
|
||||||
PieChart, Pie, Cell, ResponsiveContainer, Tooltip,
|
PieChart, Pie, Cell, ResponsiveContainer, Tooltip,
|
||||||
BarChart, Bar, XAxis, YAxis, CartesianGrid,
|
BarChart, Bar, XAxis, YAxis, CartesianGrid,
|
||||||
@@ -36,7 +36,7 @@ export function OverviewPageClient() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [status, setStatus] = useState<SystemStatusPayload | null>(null);
|
const [status, setStatus] = useState<SystemStatusPayload | null>(null);
|
||||||
const [memberships, setMemberships] = useState<MembershipEntry[]>([]);
|
const [memberships, setMemberships] = useState<MembershipEntry[]>([]);
|
||||||
const [funnel, setFunnel] = useState<FunnelPayload | null>(null);
|
const [funnel, setFunnel] = useState<{ steps: { label: string; count: number; pct_of_prev?: number }[] } | null>(null);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -44,7 +44,7 @@ export function OverviewPageClient() {
|
|||||||
const [s, m, f] = await Promise.all([
|
const [s, m, f] = await Promise.all([
|
||||||
opsApi.systemStatus() as Promise<SystemStatusPayload>,
|
opsApi.systemStatus() as Promise<SystemStatusPayload>,
|
||||||
opsApi.memberships() as Promise<MembershipsPayload>,
|
opsApi.memberships() as Promise<MembershipsPayload>,
|
||||||
opsApi.funnel(30) as Promise<FunnelPayload>,
|
opsApi.funnel(30),
|
||||||
]);
|
]);
|
||||||
setStatus(s);
|
setStatus(s);
|
||||||
setMemberships((m as MembershipsPayload).memberships ?? []);
|
setMemberships((m as MembershipsPayload).memberships ?? []);
|
||||||
|
|||||||
+28
-2
@@ -19,8 +19,34 @@ export const opsApi = {
|
|||||||
paymentRuntime() {
|
paymentRuntime() {
|
||||||
return opsFetch<Record<string, unknown>>("/api/payments/runtime");
|
return opsFetch<Record<string, unknown>>("/api/payments/runtime");
|
||||||
},
|
},
|
||||||
funnel(days = 30) {
|
async funnel(days = 30) {
|
||||||
return opsFetch<Record<string, unknown>>(`/api/ops/analytics/funnel?days=${days}`);
|
const raw = await opsFetch<{
|
||||||
|
events?: Record<string, { total?: number; unique_users?: number }>;
|
||||||
|
rates?: Record<string, number>;
|
||||||
|
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<string, string> = {
|
||||||
|
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) {
|
users(q: string, limit = 20) {
|
||||||
return opsFetch<Record<string, unknown>>(`/api/ops/users?q=${encodeURIComponent(q)}&limit=${limit}`);
|
return opsFetch<Record<string, unknown>>(`/api/ops/users?q=${encodeURIComponent(q)}&limit=${limit}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user