import { useState, useEffect } from 'react'; import { Mic2, Disc3, Users, Activity, BarChart3, PieChart, TrendingUp, ArrowRight, Headphones, ShoppingCart, CreditCard, RefreshCw, UserCheck, UserX, Filter, Crown, Coins } from 'lucide-react'; import { motion } from 'framer-motion'; import { AnimatedCounter } from '@/components/AnimatedCounter.tsx'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { getListeningTrendApi, getSubscriptionTrendApi, getRenewalTrendApi, getSubscriberStatsApi, getContentQualityApi, getBusinessConversionApi, getPreferenceApi, getUserStickinessApi, getChannelListApi, getProgramListApi, getVipStatsApi } from '@/api/radio.ts'; import type { RadioChannel } from '@/types/radio.ts'; // ─── Color Palette ─── const COLORS = { listen: '#0f172a', // Slate-900 (primary) sub: '#3b82f6', // Blue-500 renew: '#10b981', // Emerald-500 muted: '#9ca3af', // Gray-400 chart: ['#0f172a', '#3b82f6', '#10b981', '#f59e0b', '#6366f1'], }; export default function Dashboard() { const [loading, setLoading] = useState(true); // Filter states const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [channelId, setChannelId] = useState(''); const [channelOptions, setChannelOptions] = useState([]); // Core stats const [statsData, setStatsData] = useState<{ activeSubscribers: number; totalSubscribers: number; expiredSubscribers: number; channelCount: number; programCount: number; }>({ activeSubscribers: 0, totalSubscribers: 0, expiredSubscribers: 0, channelCount: 0, programCount: 0, }); const [vipStats, setVipStats] = useState<{ activeVipUsers: number; vipRevenue: number; newVipOrders: number; }>({ activeVipUsers: 0, vipRevenue: 0, newVipOrders: 0, }); // Unified Trend Data const [trendData, setTrendData] = useState<{ date: string; listen: number; sub: number; renew: number }[]>([]); // Quality / Preference / Funnel / Retention const [qualityData, setQualityData] = useState<{ programId: string; title: string; playCount: number; avgCompletion: number }[]>([]); const [preferenceData, setPreferenceData] = useState<{ categoryId: string; categoryName: string; revenue: number; share: number }[]>([]); const [funnelData, setFunnelData] = useState<{ listenUsers: number; orderUsers: number; payUsers: number; ltv: number } | null>(null); const [retentionData, setRetentionData] = useState<{ date: string; newUsers: number; retention: number[] }[]>([]); const [subscriberTrend, setSubscriberTrend] = useState<{ date: string; count: number }[]>([]); // Initial load: Fetch channels for filter useEffect(() => { getChannelListApi({ pageSize: 100 }) .then(res => { if (res && res.list) setChannelOptions(res.list); }) .catch(console.error); }, []); useEffect(() => { fetchDashboardData(); fetchTrendData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [startDate, endDate, channelId]); const fetchDashboardData = async () => { setLoading(true); try { const commonParams = { ...(startDate ? { startDate } : {}), ...(endDate ? { endDate } : {}), ...(channelId ? { channelId } : {}), }; const dateOnlyParams = { ...(startDate ? { startDate } : {}), ...(endDate ? { endDate } : {}), }; const channelOnlyParams = { ...(channelId ? { channelId } : {}), }; const [subStats, conversion, preference, channels, programs, quality, stickiness, vipData] = await Promise.all([ getSubscriberStatsApi(commonParams), getBusinessConversionApi(commonParams), getPreferenceApi(), // NO PARAMS getChannelListApi({ pageSize: 1 }), getProgramListApi({ pageSize: 1 }), getContentQualityApi(channelOnlyParams), getUserStickinessApi(dateOnlyParams), getVipStatsApi(dateOnlyParams) ]); setStatsData({ activeSubscribers: subStats.activeSubscribers || 0, totalSubscribers: subStats.totalSubscribers || 0, expiredSubscribers: subStats.expiredSubscribers || 0, channelCount: channels.total || 0, programCount: programs.total || 0, }); setSubscriberTrend(subStats.activeTrend || []); setFunnelData(conversion); setPreferenceData(preference.list || []); setQualityData(quality || []); setRetentionData(stickiness || []); setVipStats({ activeVipUsers: vipData.activeVipUsers || 0, vipRevenue: vipData.vipRevenue || 0, newVipOrders: vipData.newVipOrders || 0, }); } catch (error) { console.error('Fetch dashboard error:', error); } finally { setLoading(false); } }; const fetchTrendData = async () => { try { const commonParams = { ...(startDate ? { startDate } : {}), ...(endDate ? { endDate } : {}), ...(channelId ? { channelId } : {}), }; const [listenRes, subRes, renewRes] = await Promise.all([ getListeningTrendApi(commonParams), getSubscriptionTrendApi(commonParams), getRenewalTrendApi(commonParams) ]); // Merge trend arrays grouping by date const mergedMap = new Map(); const addData = (arr: { date: string; count: number }[], key: 'listen' | 'sub' | 'renew') => { if (!arr) return; arr.forEach(item => { const existing = mergedMap.get(item.date) || { date: item.date, listen: 0, sub: 0, renew: 0 }; existing[key] = item.count; mergedMap.set(item.date, existing); }); }; addData(listenRes.trend, 'listen'); addData(subRes.trend, 'sub'); addData(renewRes.trend, 'renew'); const finalTrend = Array.from(mergedMap.values()).sort((a, b) => a.date.localeCompare(b.date)); setTrendData(finalTrend); } catch (error) { console.error('Fetch trend error:', error); } }; // ─── Funnel steps ─── const funnelSteps = funnelData ? [ { label: '活跃收听', value: funnelData.listenUsers || 0, icon: Headphones, color: COLORS.chart[0] }, { label: '尝试下单', value: funnelData.orderUsers || 0, icon: ShoppingCart, color: COLORS.chart[1] }, { label: '支付成功', value: funnelData.payUsers || 0, icon: CreditCard, color: COLORS.chart[2] }, ] : []; const funnelMax = funnelSteps.length > 0 ? Math.max(...funnelSteps.map((s) => s.value), 1) : 1; // ─── Stats cards ─── const stats = [ { name: '有效订阅', value: statsData.activeSubscribers, icon: UserCheck, subLabel: '当前活跃付费用户', color: COLORS.sub, trend: statsData.totalSubscribers > 0 ? `共 ${statsData.totalSubscribers} 人` : undefined, }, { name: '已过期订阅', value: statsData.expiredSubscribers, icon: UserX, subLabel: '可触达召回用户', color: COLORS.muted, }, { name: '活跃频道', value: statsData.channelCount, icon: Mic2, subLabel: '在线广播节点', color: COLORS.listen, }, { name: '节目总数', value: statsData.programCount, icon: Disc3, subLabel: '内容资产储备', color: COLORS.renew, }, { name: 'VIP 用户', value: vipStats.activeVipUsers, icon: Crown, subLabel: '当前生效的尊享会员', color: '#f59e0b', trend: vipStats.newVipOrders > 0 ? `+${vipStats.newVipOrders} 订单` : undefined, }, { name: 'VIP 营收', value: `¥${(vipStats.vipRevenue / 100).toFixed(2)}`, icon: Coins, subLabel: '区间内会员特权变现', color: '#0f172a', }, ]; // ─── Formatted date helper ─── // Handles format "2026-03-07T00:00:00+08:00" -> "03-07" const fmtDate = (val: string) => { if (!val) return ''; const justDate = val.split('T')[0]; return justDate.split('-').slice(1).join('-'); }; return (
{/* ═══ Header ═══ */}

电台数据概览

实时监控频道运营、内容表现与商业转化

{/* ═══ Filters ═══ */}
日期: setStartDate(e.target.value)} className="bg-transparent text-[13px] font-medium text-foreground outline-none border-none custom-date-input" /> - setEndDate(e.target.value)} className="bg-transparent text-[13px] font-medium text-foreground outline-none border-none custom-date-input" />
频道:
{ setStartDate(''); setEndDate(''); setChannelId(''); }} className="p-1.5 rounded-lg bg-muted border border-border shadow-sm hover:bg-muted/80 transition-colors flex items-center justify-center h-8 w-8" title="重置过滤" > { fetchDashboardData(); fetchTrendData(); }} className="p-1.5 rounded-lg bg-card border border-border shadow-sm hover:bg-muted transition-colors flex items-center justify-center h-8 w-8" title="刷新数据" >
{/* ═══ Stats Overview ═══ */}
{stats.map((stat, idx) => (

{stat.name}

{loading ? '—' : ( )}

{stat.subLabel} {stat.trend && ( {stat.trend} )}
))}
{/* ═══ Main Trend Chart + Sidebar ═══ */}
{/* Multiplex Trend Chart */}
{/* Chart header */}

核心运营趋势

收听与订阅多维度对比分析

{/* Chart area */}
active && payload?.length ? (

{fmtDate(label as string)} 统计

{payload.map(p => (
{p.name}
{p.value}
))}
) : null } />
{/* ─── Conversion Funnel Side Card ─── */}

商业转化漏斗

{/* LTV highlight */} {funnelData && (

人均生命周期价值 (LTV)

)} {/* Funnel bars */}
{funnelSteps.map((step, i) => { const pct = funnelMax > 0 ? (step.value / funnelMax) * 100 : 0; const convRate = i > 0 && funnelSteps[i - 1].value > 0 ? ((step.value / funnelSteps[i - 1].value) * 100).toFixed(1) : null; return (
{step.label}
{convRate && ( )}
{i < funnelSteps.length - 1 && (
)}
); })}
{/* ═══ Subscriber Active Trend (mini chart) + Content Quality ═══ */}
{/* Mini subscriber trend */}

有效订阅用户趋势

当前 {statsData.activeSubscribers}
active && payload?.length ? (

{fmtDate(label as string)}

{payload[0].value} 人

) : null } />
{/* Content Quality */}

内容完播率 TOP5

{qualityData.slice(0, 5).map((item, i) => { const pct = (item.avgCompletion * 100); return (
{item.title}
{item.playCount} 播放 = 60 ? COLORS.sub : COLORS.muted }}> {pct.toFixed(1)}%
= 60 ? COLORS.sub : COLORS.listen }} transition={{ duration: 1, delay: i * 0.08 }} />
); })}
{/* ═══ Bottom: Preference + Retention ═══ */}
{/* Revenue by Category */}

品类营收结构

全量统计
{preferenceData.slice(0, 5).map((item, idx) => (
{idx + 1}
{item.categoryName} ¥{(item.revenue / 100).toLocaleString()}
{(item.share * 100).toFixed(1)}%
))}
{/* User Retention (real data from getUserStickinessApi) */}

用户留存分析

{retentionData.slice(0, 7).map((row) => ( {(row.retention || []).map((rate: number, idx: number) => { const pct = (rate * 100); const bgOpacity = Math.max(0.06, rate * 0.5); return ( ); })} {/* Fill empty if less than 4 retention values */} {(row.retention || []).length < 4 && Array.from({ length: 4 - (row.retention || []).length }).map((_, idx) => ( ))} ))} {retentionData.length === 0 && ( )}
日期 新增 次日 3日 7日 30日
{fmtDate(row.date)} {row.newUsers}
20 ? COLORS.sub : COLORS.muted, }} > {pct.toFixed(0)}%
暂无留存数据
); }