import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import api from '@/lib/api' import { FilterableTable } from '@/components/FilterableTable' import { LoadingSpinner } from '@/components/LoadingSpinner' import { CollapsibleSection } from '@/components/CollapsibleSection' import { MetricCard } from '@/components/MetricCard' import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils' import { useState } from 'react' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts' import { Tabs } from '@/components/Tabs' import { MonthPicker } from '@/components/MonthPicker' const GRADE_COLORS: Record = { A: '#22c55e', B: '#3b82f6', C: '#eab308', D: '#ef4444' } const RISK_COLORS: Record = { '红色': '#ef4444', '黄色': '#eab308', '绿色': '#22c55e' } export function StoreGradePage() { const [month, setMonth] = useState(new Date().toISOString().slice(0, 7)) const [selectedStore, setSelectedStore] = useState(null) const [tab, setTab] = useState('grade') const queryClient = useQueryClient() const { data: gradeData, isLoading: gradeLoading } = useQuery({ queryKey: ['store-grade/grades', month], queryFn: () => api.get('/store-grade/grades', { params: { month } }), enabled: tab === 'grade', }) const { data: closureData, isLoading: closureLoading } = useQuery({ queryKey: ['store-grade/closure-analysis', month], queryFn: () => api.get('/store-grade/closure-analysis', { params: { month } }), enabled: tab === 'closure', }) const autoGradeMutation = useMutation({ mutationFn: () => api.post('/store-grade/auto-grade', null, { params: { month } }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['store-grade/grades'] }) }, }) const gradeRows = (gradeData as any)?.data || [] const gradeDistribution = gradeRows.reduce((acc: Record, r: any) => { acc[r.grade] = (acc[r.grade] || 0) + 1 return acc }, {}) const gradePieData = Object.entries(gradeDistribution).map(([name, value]) => ({ name: `${name}级`, value })) const riskDistribution = gradeRows.reduce((acc: Record, r: any) => { acc[r.risk_level] = (acc[r.risk_level] || 0) + 1 return acc }, {}) const riskPieData = [ { name: '红色', value: riskDistribution['红色'] || 0 }, { name: '黄色', value: riskDistribution['黄色'] || 0 }, { name: '绿色', value: riskDistribution['绿色'] || 0 }, ] const avgScore = gradeRows.length > 0 ? gradeRows.reduce((s: number, r: any) => s + Number(r.overall_score || 0), 0) / gradeRows.length : 0 const aGradeCount = gradeDistribution.A || 0 const dGradeCount = gradeDistribution.D || 0 const redRiskCount = riskDistribution['红色'] || 0 if (gradeLoading && tab === 'grade') { return } if (closureLoading && tab === 'closure') { return } const closure = (closureData as any)?.data || {} const closureSummary = closure.summary || {} const closureStores = closure.stores || [] const closurePieData = [ { name: '建议关停', value: closureSummary.recommend_close || 0 }, { name: '整改观察', value: closureSummary.recommend_rectify || 0 }, { name: '持续经营', value: (closureSummary.total_stores || 0) - (closureSummary.recommend_close || 0) - (closureSummary.recommend_rectify || 0) }, ].filter(d => d.value > 0) const CLOSURE_COLORS: Record = { '建议关停': '#ef4444', '整改观察': '#eab308', '持续经营': '#22c55e' } return (

门店分级与风险等级

自动分级 · 风险监控 · 关停测算 · {month}

{tab === 'grade' && ( )}
{tab === 'closure' && ( <>
{closurePieData.length > 0 && ( {`${name}: ${value}`}}> {closurePieData.map((e, i) => )} [formatNumber(v), '门店数']} /> )} { const colors: Record = { A: 'text-green-600', B: 'text-blue-600', C: 'text-yellow-600', D: 'text-red-600' } return {r.grade} }}, { key: 'monthly_revenue', label: '月收入', align: 'right', render: (r) => formatCurrency(r.monthly_revenue) }, { key: 'monthly_profit', label: '月利润', align: 'right', render: (r) => { const v = Number(r.monthly_profit || 0) return {formatCurrency(v)} }}, { key: 'monthly_rent', label: '月租', align: 'right', render: (r) => formatCurrency(r.monthly_rent) }, { key: 'monthly_wage', label: '月人工', align: 'right', render: (r) => formatCurrency(r.monthly_wage) }, { key: 'total_closure_cost', label: '关停成本', align: 'right', render: (r) => {formatCurrency(r.total_closure_cost)} }, { key: 'projected_12m_loss', label: '12月亏损', align: 'right', render: (r) => { const v = Number(r.projected_12m_loss || 0) return v ? {formatCurrency(v)} : '-' }}, { key: 'monthly_savings_if_closed', label: '月节省', align: 'right', render: (r) => { const v = Number(r.monthly_savings_if_closed || 0) return v ? {formatCurrency(v)} : '-' }}, { key: 'payback_months', label: '回本(月)', align: 'right', render: (r) => r.payback_months ? `${Number(r.payback_months).toFixed(1)}月` : '-' }, { key: 'recommendation', label: '建议', align: 'center', render: (r) => { const colors: Record = { '建议关停': 'bg-red-500 text-white', '整改观察3个月': 'bg-yellow-500 text-white', '重点整改': 'bg-blue-500 text-white', '持续经营': 'bg-green-500 text-white' } return {r.recommendation} }}, { key: 'reason', label: '原因' }, ]} /> )} {tab === 'grade' && ( <>
= 75 ? 'good' : avgScore >= 60 ? 'warn' : 'bad'} /> 0 ? 'bad' : 'good'} />
{`${name}: ${value}`}}> {gradePieData.map((entry, i) => )} [formatNumber(v), '门店数']} /> {`${name}: ${value}`}}> {riskPieData.map((entry, i) => )} [formatNumber(v), '门店数']} />
setSelectedStore(r)} columns={[ { key: 'store_code', label: '门店编码' }, { key: 'store_name', label: '门店名称' }, { key: 'region', label: '区域' }, { key: 'grade', label: '分级', align: 'center', render: (r) => { const colors: Record = { A: 'text-green-600', B: 'text-blue-600', C: 'text-yellow-600', D: 'text-red-600' } return {r.grade} }}, { key: 'risk_level', label: '风险', align: 'center', render: (r) => { const colors: Record = { '红色': 'bg-red-500 text-white', '黄色': 'bg-yellow-500 text-white', '绿色': 'bg-green-500 text-white' } return {r.risk_level || '-'} }}, { key: 'overall_score', label: '综合分', align: 'right', render: (r) => Number(r.overall_score || 0).toFixed(1) }, { key: 'revenue_achievement_pct', label: '营收达成率', align: 'right', render: (r) => { const v = Number(r.revenue_achievement_pct || 0) return = 100 ? 'text-green-600' : v >= 80 ? 'text-yellow-600' : 'text-red-600'}>{v.toFixed(1)}% }}, { key: 'reason', label: '分级原因' }, ]} /> {selectedStore && setSelectedStore(null)} />} )}
) } const GRADE_DIMENSIONS = [ { key: 'revenue_achievement_pct', label: '营收达成', max: 100 }, { key: 'overall_score', label: '综合分', max: 100 }, { key: 'margin_pct', label: '毛利率', max: 50 }, { key: 'anomaly_rate', label: '异常率', max: 20, inverse: true }, ] function StoreDetailModal({ store, onClose }: { store: any; onClose: () => void }) { const gradeColor = GRADE_COLORS[store.grade] || '#999' const riskColor = RISK_COLORS[store.risk_level] || '#999' const radarData = GRADE_DIMENSIONS.map(d => { const raw = Number(store[d.key] || 0) const pct = d.inverse ? Math.max(0, 100 - (raw / d.max) * 100) : Math.min((raw / d.max) * 100, 100) return { dimension: d.label, score: Math.round(pct), fullMark: 100 } }) const metrics = [ { label: '门店编码', value: store.store_code }, { label: '区域', value: store.region || '-' }, { label: '分级', value: store.grade, color: gradeColor }, { label: '综合分', value: Number(store.overall_score || 0).toFixed(1) }, { label: '营收达成率', value: `${Number(store.revenue_achievement_pct || 0).toFixed(1)}%` }, { label: '营收', value: formatCurrency(store.revenue) }, { label: '毛利率', value: formatPercent(store.margin_pct) }, { label: '优惠率', value: store.discount_rate != null ? `${Number(store.discount_rate).toFixed(1)}%` : '-' }, { label: '异常率', value: store.anomaly_rate != null ? `${Number(store.anomaly_rate).toFixed(1)}%` : '-' }, { label: '会员占比', value: store.member_pct != null ? `${Number(store.member_pct).toFixed(1)}%` : '-' }, { label: '客单价', value: formatCurrency(store.avg_bill_value) }, { label: '活跃天数', value: store.active_days != null ? `${store.active_days}天` : '-' }, { label: '风险等级', value: store.risk_level || '-', color: riskColor }, ] return (
e.stopPropagation()}>

{store.store_name}

{store.grade}级 {store.risk_level || '-'}
[v, '得分']} />
{metrics.map(m => (
{m.label} {m.value}
))} {store.reason && (
分级原因:{store.reason}
)} {store.primary_issue && (
风险原因:{store.primary_issue}
)}
) }