import { useQuery } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Legend } from 'recharts' import api from '@/lib/api' import { MetricCard } from '@/components/MetricCard' import { Badge } from '@/components/Badge' import { DataTable } from '@/components/DataTable' import { Pagination } from '@/components/Pagination' import { LoadingSpinner } from '@/components/LoadingSpinner' import { CollapsibleSection } from '@/components/CollapsibleSection' import { formatNumber, formatCurrency, formatPercent } from '@/lib/utils' import { useState, useMemo } from 'react' const RISK_COLORS = { '红色': '#ef4444', '黄色': '#eab308', '绿色': '#22c55e' } const PAGE_SIZE = 10 export function DashboardPage() { const navigate = useNavigate() const [priorityPage, setPriorityPage] = useState(1) const { data: overview, isLoading: odLoading } = useQuery({ queryKey: ['overview'], queryFn: () => api.get('/overview'), }) const { data: daily, isLoading: dailyLoading } = useQuery({ queryKey: ['overview/daily'], queryFn: () => api.get('/overview/daily'), }) const { data: riskData, isLoading: riskLoading } = useQuery({ queryKey: ['stores/risk'], queryFn: () => api.get('/stores/risk'), }) const { data: priorityData, isLoading: priorityLoading } = useQuery({ queryKey: ['stores/priority'], queryFn: () => api.get('/stores/priority'), }) const { data: loopHealth, isLoading: loopLoading } = useQuery({ queryKey: ['loop-health'], queryFn: () => api.get('/tasks/loop-health'), }) const pageLoading = odLoading || dailyLoading || riskLoading || priorityLoading || loopLoading const od = (overview as any)?.data const riskRows = (riskData as any)?.data || [] const priorityRows = (priorityData as any)?.data || [] const dailyRows = (daily as any)?.data || [] const lh = (loopHealth as any)?.data const riskSummary = riskRows.reduce((acc: any, r: any) => { acc[r.risk_level] = (acc[r.risk_level] || 0) + 1 return acc }, {}) const totalStores = riskRows.length const riskPieData = Object.entries(riskSummary).map(([name, value]) => ({ name, value: value as number, pct: totalStores > 0 ? Math.round((value as number / totalStores) * 100) : 0, })) const prioritySummary = priorityRows.reduce((acc: any, r: any) => { const key = r.action_priority acc[key] = (acc[key] || 0) + 1 return acc }, {}) const p0p1Stores = priorityRows.filter((s: any) => s.action_priority?.startsWith('P0') || s.action_priority?.startsWith('P1')) const pagedP0P1 = useMemo(() => p0p1Stores.slice((priorityPage - 1) * PAGE_SIZE, priorityPage * PAGE_SIZE), [p0p1Stores, priorityPage]) // 闭环健康度综合得分 const loopScore = lh ? ( (parseFloat(lh.task_generation_rate) + parseFloat(lh.store_execution_rate) + parseFloat(lh.weekly_check_rate) + parseFloat(lh.monthly_review_rate) + parseFloat(lh.practice_promotion_rate)) / 5 ) : 0 // 计算环比趋势:最后7天 vs 前7天 const last7 = dailyRows.slice(-7) const prev7 = dailyRows.slice(-14, -7) const sumKey = (arr: any[], key: string) => arr.reduce((s, r) => s + Number(r[key] || 0), 0) const trend = (curr: number, prev: number) => prev > 0 ? Math.round((curr - prev) / prev * 1000) / 10 : 0 const trendReceived = trend(sumKey(last7, 'received'), sumKey(prev7, 'received')) const trendBills = trend(sumKey(last7, 'bill_count'), sumKey(prev7, 'bill_count')) const trendAvgBill = trend( last7.length > 0 ? sumKey(last7, 'received') / sumKey(last7, 'bill_count') : 0, prev7.length > 0 ? sumKey(prev7, 'received') / sumKey(prev7, 'bill_count') : 0, ) if (pageLoading) { return } return (

总部驾驶舱

全部门店经营总览 · 2026年4月

{totalStores} 家门店 红色 {riskSummary['红色'] || 0} 黄色 {riskSummary['黄色'] || 0} 绿色 {riskSummary['绿色'] || 0}
{/* 经营指标 */}
{/* 运营指标 */}
{/* 闭环健康度 */} {lh && ( 综合得分 = 80 ? 'text-green-600' : loopScore >= 60 ? 'text-yellow-600' : 'text-red-600'}`}> {loopScore.toFixed(1)}%
} >
{[ { label: '任务生成率', value: lh.task_generation_rate, desc: '系统自动生成的整改任务占应生成任务的比例' }, { label: '店长执行率', value: lh.store_execution_rate, desc: '店长已提交反馈的任务占分配任务的比例' }, { label: '区域周检率', value: lh.weekly_check_rate, desc: '区域经理完成周度检查的比例' }, { label: '月度验收率', value: lh.monthly_review_rate, desc: '月度完成验收的任务比例' }, { label: '经验推广率', value: lh.practice_promotion_rate, desc: '达标经验被推广到其他门店的比例' }, ].map((item) => { const num = parseFloat(item.value) const isLow = num < 60 const isMid = num >= 60 && num < 80 return (

{item.label}

?

{num.toFixed(1)}%

) })}
)} {/* 图表区 */}
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)} /> `${name} ${value}家 (${pct}%)`} > {riskPieData.map((entry) => ( ))}
{/* P0/P1 门店列表 */} {Object.entries(prioritySummary).filter(([k]) => k?.startsWith('P0') || k?.startsWith('P1')).map(([k, v]) => ( {k}: {v as number}家 ))}
} >
}, { key: 'problem_count', label: '问题数', align: 'center' }, { key: 'problem_combination', label: '问题组合' }, { key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) }, ]} data={pagedP0P1} onRowClick={(r) => navigate(`/stores/${r.store_code}`)} />
) }