import { useQuery } from '@tanstack/react-query' import api from '@/lib/api' import { LoadingSpinner } from '@/components/LoadingSpinner' import { CollapsibleSection } from '@/components/CollapsibleSection' import { MetricCard } from '@/components/MetricCard' import { formatCurrency, formatNumber } from '@/lib/utils' import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Area, AreaChart } from 'recharts' const CHANNEL_COLORS: Record = { cash: '#22c55e', alipay: '#3b82f6', wechat: '#06b6d4', meituan: '#f59e0b', unionpay: '#8b5cf6', douyin: '#ec4899', credit: '#ef4444', jd_delivery: '#f97316', meituan_delivery: '#f59e0b', taobao_delivery: '#eab308', } const CHANNEL_LABELS: Record = { cash: '现金', alipay: '支付宝', wechat: '微信', meituan: '美团到店', unionpay: '银联', douyin: '抖音', credit: '挂账', jd_delivery: '京东到家', meituan_delivery: '美团外卖', taobao_delivery: '淘宝外卖', meituan_commission: '美团佣金', taobao_commission: '淘宝佣金', jd_commission: '京东佣金', } const channelLabel = (k: string) => CHANNEL_LABELS[k] || k export function TimePage() { const { data: weekdayData, isLoading: weekdayLoading } = useQuery({ queryKey: ['time/weekday'], queryFn: () => api.get('/time/weekday'), }) const { data: hourlyData, isLoading: hourlyLoading } = useQuery({ queryKey: ['time/hourly'], queryFn: () => api.get('/time/hourly'), }) const { data: channelData, isLoading: channelLoading } = useQuery({ queryKey: ['channel'], queryFn: () => api.get('/channel'), }) const weekdayRows = (weekdayData as any)?.data || [] const hourlyRows = (hourlyData as any)?.data || [] const channelRows = (channelData as any)?.data || [] const pageLoading = weekdayLoading || hourlyLoading || channelLoading const peakWeekday = weekdayRows.length > 0 ? weekdayRows.reduce((best: any, r: any) => Number(r.received || 0) > Number(best?.received || 0) ? r : best, null) : null const lowWeekday = weekdayRows.length > 0 ? weekdayRows.reduce((low: any, r: any) => Number(r.received || 0) < Number(low?.received || Infinity) ? r : low, null) : null const peakHour = hourlyRows.length > 0 ? hourlyRows.reduce((best: any, r: any) => Number(r.bill_count || 0) > Number(best?.bill_count || 0) ? r : best, null) : null const maxBillCount = hourlyRows.length > 0 ? Math.max(...hourlyRows.map((r: any) => Number(r.bill_count || 0))) : 0 const maxReceived = hourlyRows.length > 0 ? Math.max(...hourlyRows.map((r: any) => Number(r.received || 0))) : 0 const yLeftMax = Math.ceil(maxReceived * 1.15 / 10000) * 10000 const yRightMax = Math.ceil(maxBillCount * 1.15 / 10000) * 10000 const channelTotals = channelRows.length > 0 ? Object.keys(channelRows[0]).filter(k => k !== 'business_date' && k !== 'meituan_commission' && k !== 'taobao_commission' && k !== 'jd_commission') .map(k => ({ channel: k, total: channelRows.reduce((s: number, r: any) => s + Number(r[k] || 0), 0), })) .filter(c => c.total > 0) .sort((a, b) => b.total - a.total) : [] if (pageLoading) { return } return (

时间维度分析

星期规律 · 小时分布 · 渠道趋势 · 2026年4月

{/* 概览指标 */}
{/* 星期趋势 */} v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} /> v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} /> name === '实收' ? formatCurrency(v) : name === '客单价' ? formatCurrency(v) : formatNumber(v)} /> {/* 小时分布 */} `${v}:00`} tick={{ fontSize: 10 }} /> v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} domain={[0, yLeftMax]} /> v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} domain={[0, yRightMax]} /> `${v}:00`} formatter={(v: any, name: any) => name === '实收' ? formatCurrency(v) : formatNumber(v)} /> {/* 渠道趋势 */} v?.substring(5, 10)} tick={{ fontSize: 10 }} /> v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} /> v?.substring(0, 10)} formatter={(v: any) => formatCurrency(v)} /> {channelTotals.slice(0, 6).map(c => ( ))} {/* 渠道汇总 */}
{channelTotals.map(c => (

{channelLabel(c.channel)}

{formatCurrency(c.total)}

))}
) }