import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import api from '@/lib/api' import { Badge } from '@/components/Badge' import { MetricCard } from '@/components/MetricCard' import { FilterableTable } from '@/components/FilterableTable' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, PieChart, Pie, Legend } from 'recharts' import { formatCurrency, formatPercent, formatNumber, cn } from '@/lib/utils' import { LoadingSpinner } from '@/components/LoadingSpinner' import { MonthPicker } from '@/components/MonthPicker' import { SearchSelect } from '@/components/SearchSelect' import { KPISection } from '@/components/KPISection' import { useState } from 'react' const SCORE_DIMENSIONS = [ { key: 'score_revenue', label: '营收规模', max: 20 }, { key: 'score_cost', label: '成本控制', max: 20 }, { key: 'score_margin', label: '毛利率', max: 15 }, { key: 'score_risk', label: '风险控制', max: 10 }, { key: 'score_repeat', label: '复购率', max: 10 }, { key: 'score_member', label: '会员占比', max: 10 }, { key: 'score_task', label: '任务完成', max: 10 }, { key: 'score_bill', label: '客单价', max: 5 }, ] type DetailTab = 'overview' | 'meal' | 'category' | 'cost' | 'member' | 'anomaly' | 'tasks' const TABS: { key: DetailTab; label: string }[] = [ { key: 'overview', label: '概览' }, { key: 'meal', label: '餐段分析' }, { key: 'category', label: '品类结构' }, { key: 'cost', label: '成本分析' }, { key: 'member', label: '会员分析' }, { key: 'anomaly', label: '异常账单' }, ] export function StorePage() { const queryClient = useQueryClient() const [executeText, setExecuteText] = useState('') const [activeTaskId, setActiveTaskId] = useState(null) const [storeCode, setStoreCode] = useState('1111') const [month, setMonth] = useState('2026-04') const [detailTab, setDetailTab] = useState('overview') const { data: storesData, isLoading: storesLoading } = useQuery({ queryKey: ['stores-list', month], queryFn: () => api.get('/stores/risk', { params: { month } }), }) const stores = ((storesData as any)?.data || []).map((s: any) => ({ code: s.store_code, name: s.store_name, risk: s.risk_level, issue: s.primary_issue })) const { data: priorityData, isLoading: priorityLoading } = useQuery({ queryKey: ['stores-priority-detail', month], queryFn: () => api.get('/stores/priority', { params: { month } }), }) const { data: cardData, isLoading: cardLoading } = useQuery({ queryKey: ['store', storeCode, 'daily-card', month], queryFn: () => api.get(`/tasks/stores/${storeCode}/daily-card`, { params: { month } }), }) const { data: tasksData, isLoading: tasksLoading } = useQuery({ queryKey: ['store', storeCode, 'tasks', month], queryFn: () => api.get('/tasks', { params: { store_code: storeCode, month, page_size: 50 } }), }) const { data: dailyData, isLoading: dailyLoading } = useQuery({ queryKey: ['store', storeCode, 'daily', month], queryFn: () => api.get(`/stores/${storeCode}/daily`, { params: { month } }), }) const { data: followupData, isLoading: followupLoading } = useQuery({ queryKey: ['store', storeCode, 'followup', month], queryFn: () => api.get('/tasks/followup', { params: { month } }), }) const { data: skuData, isLoading: skuLoading } = useQuery({ queryKey: ['sku-attach', storeCode], queryFn: () => api.get('/sku/attach', { params: { month } }), }) const { data: abcData, isLoading: abcLoading } = useQuery({ queryKey: ['sku-abc'], queryFn: () => api.get('/sku/abc', { params: { month } }), }) // StoreDetailPage queries const { data: storeDetailData, isLoading: storeDetailLoading } = useQuery({ queryKey: ['store-detail', storeCode, month], queryFn: () => api.get(`/stores/${storeCode}`, { params: { month } }), }) const { data: healthData, isLoading: healthLoading } = useQuery({ queryKey: ['store-health', storeCode, month], queryFn: () => api.get('/situational-awareness/health-score', { params: { month } }), }) const { data: mealData } = useQuery({ queryKey: ['store', storeCode, 'meal-period', month], queryFn: () => api.get(`/stores/${storeCode}/meal-period`, { params: { month } }), enabled: detailTab === 'meal', }) const { data: catData } = useQuery({ queryKey: ['store', storeCode, 'category-mix', month], queryFn: () => api.get(`/stores/${storeCode}/category-mix`, { params: { month } }), enabled: detailTab === 'category', }) const { data: costData } = useQuery({ queryKey: ['store', storeCode, 'cost', month], queryFn: () => api.get(`/stores/${storeCode}/cost`, { params: { month } }), enabled: detailTab === 'cost', }) const { data: memberData } = useQuery({ queryKey: ['store', storeCode, 'member', month], queryFn: () => api.get(`/stores/${storeCode}/member`, { params: { month } }), enabled: detailTab === 'member', }) const { data: anomalyDetailData } = useQuery({ queryKey: ['store', storeCode, 'anomalies', month], queryFn: () => api.get(`/stores/${storeCode}/anomalies`, { params: { month, page_size: 200 } }), enabled: detailTab === 'anomaly', }) const card = (cardData as any)?.data const anomalies = card?.anomalies || [] const tasks = (tasksData as any)?.data || [] const dailyRows = (dailyData as any)?.data || [] const followupRows = (followupData as any)?.data || [] const followup = followupRows.find((f: any) => f.store_code === storeCode) const skuRows = (skuData as any)?.data || [] const abcRows = (abcData as any)?.data || [] const coreSKUs = abcRows.filter((r: any) => r.abc_class === 'A').slice(0, 10) const priorityRows = (priorityData as any)?.data || [] const priorityInfo = priorityRows.find((p: any) => p.store_code === storeCode) // StoreDetailPage derived data const sd = (storeDetailData as any)?.data const healthRows = ((healthData as any)?.data) || [] const health = healthRows.find((r: any) => r.store_code === storeCode) const mealRows = ((mealData as any)?.data) || [] const cat = (catData as any)?.data const cost = (costData as any)?.data const member = (memberData as any)?.data const anomalyDetailRows = ((anomalyDetailData as any)?.data) || [] const anomalyDetailTotal = (anomalyDetailData as any)?.meta?.total || anomalyDetailRows.length const sc = sd?.scorecard const action = sd?.action const risk = sd?.risk const riskDesc: Record = { '绿色': { color: 'text-green-600 bg-green-50 border-green-200', desc: '各项指标健康,经营状态良好,维持现有运营策略即可。' }, '黄色': { color: 'text-yellow-600 bg-yellow-50 border-yellow-200', desc: '存在单项指标偏差,需关注并针对性改善,避免风险升级。' }, '红色': { color: 'text-red-600 bg-red-50 border-red-200', desc: '多项指标异常,经营风险较高,需立即介入并制定整改计划。' }, } const quadrantDesc: Record = { '明星门店': '高营收高毛利,是公司的核心利润来源。应总结其成功经验并推广至其他门店。', '现金牛门店': '高营收但毛利偏低,通过成本优化有较大利润提升空间。', '潜力门店': '营收偏低但毛利较好,具备增长潜力,需提升客流和营收规模。', '问题门店': '营收和毛利均偏低,需全面诊断并考虑调整经营策略或选址。', } const riskInfo = risk?.risk_level ? riskDesc[risk.risk_level] : null const quadrant = action?.management_quadrant || '' const quadrantInfo = quadrantDesc[quadrant] const companyAvg = { daily_rev: 20144, bill: 36.8, discount: 20.5, margin: 71.5, member: 15.9, repeat: 37.0, delivery: 36.1, combo: 20.6, items: 4.05 } const deviations: { label: string; store: number; company: number; unit: string; good: 'high' | 'low' }[] = [ { label: '日均营收', store: Number(risk?.avg_daily_received) || 0, company: companyAvg.daily_rev, unit: '元', good: 'high' }, { label: '客单价', store: Number(risk?.avg_bill_value) || 0, company: companyAvg.bill, unit: '元', good: 'high' }, { label: '优惠率', store: Number(risk?.discount_rate_pct) || 0, company: companyAvg.discount, unit: '%', good: 'low' }, { label: '毛利率', store: Number(risk?.theoretical_margin_pct) || 0, company: companyAvg.margin, unit: '%', good: 'high' }, { label: '会员占比', store: Number(risk?.member_bill_share_pct) || 0, company: companyAvg.member, unit: '%', good: 'high' }, { label: '复购率', store: Number(action?.repeat_rate_pct) || 0, company: companyAvg.repeat, unit: '%', good: 'high' }, { label: '外卖占比', store: Number(action?.delivery_bill_share_pct) || 0, company: companyAvg.delivery, unit: '%', good: 'low' }, { label: '成本差异率', store: Number(action?.variance_to_theoretical_pct) || 0, company: 20, unit: '%', good: 'low' }, ] const problemDeviations = deviations.filter(d => { const diff = d.store - d.company return d.good === 'high' ? diff < 0 : diff > 0 }) const storeInfo = stores.find((s: any) => s.code === storeCode) const storeName = storeInfo?.name || '' const primaryIssue = storeInfo?.issue || priorityInfo?.problem_combination || '-' const todoTasks = tasks.filter((t: any) => t.status === '待启动' || t.status === '进行中') const doneTasks = tasks.filter((t: any) => t.status === '已验收' || t.status === '已回滚') const passCount = tasks.filter((t: any) => t.verification_result === '达标').length const improvingCount = tasks.filter((t: any) => t.verification_result === '改善中').length const allAnomalyItems: { module: string; metric: string; value: number; baseline: number; is_anomaly: boolean }[] = [] anomalies.forEach((a: any) => { const list = typeof a.anomalies === 'string' ? JSON.parse(a.anomalies) : a.anomalies || [] list.forEach((item: any) => { allAnomalyItems.push({ module: a.module, metric: item.metric, value: item.value, baseline: item.baseline, is_anomaly: item.is_anomaly }) }) }) const anomalyCount = allAnomalyItems.filter((i: any) => i.is_anomaly).length const executeMutation = useMutation({ mutationFn: ({ id, text }: { id: number; text: string }) => api.put(`/tasks/${id}/execute`, { process_evidence: text }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['store', storeCode, 'tasks'] }) setExecuteText('') setActiveTaskId(null) }, }) const riskColor = (level: string) => { if (level === '红色') return 'bg-red-100 text-red-700 border-red-300' if (level === '黄色') return 'bg-yellow-100 text-yellow-700 border-yellow-300' return 'bg-green-100 text-green-700 border-green-300' } const pageLoading = storesLoading || priorityLoading || cardLoading || tasksLoading || dailyLoading || followupLoading || skuLoading || abcLoading || storeDetailLoading || healthLoading if (pageLoading) { return } return (

店长工作台

({ value: s.code, label: s.name }))} emptyLabel="选择门店" className="min-w-[160px]" />
{/* 门店概况 */}

门店名称

{storeName}

风险等级

{storeInfo?.risk && {storeInfo.risk}}

主要问题

{primaryIssue}

任务完成率

{tasks.length > 0 ? Math.round(doneTasks.length / tasks.length * 100) : 0}%

{doneTasks.length}/{tasks.length} 已完成

{/* KPI达成率 */} {/* 最新经营概览 */}

最新经营日概览

{card?.target_date ? `${String(card.target_date).substring(0, 10)} 当日数据 vs ${String(card.baseline_date).substring(0, 10)} 一周前同日对比` : `${month} 月末单日 vs 一周前同日对比`}

{anomalyCount > 0 && ( {anomalyCount} 项异常 )}
{allAnomalyItems.length === 0 ? (

暂无数据

) : (
{allAnomalyItems.map((item, idx) => { const isCurrency = item.metric === '实收' || item.metric === '营业收入' || item.metric === '优惠' return (

{item.metric}

{isCurrency ? formatCurrency(item.value) : formatNumber(item.value)}

日基准: {isCurrency ? formatCurrency(item.baseline) : formatNumber(item.baseline)}{item.is_anomaly && }

) })}
)}
{/* 本周指标进度 */}
{/* 本周指标进度条 */} {followup && (

本周指标进度

{[ { label: '实收', baseline: Number(followup.baseline_received), target: Number(followup.target_received), actual: Number(followup.actual_received), lower: false, isCurrency: true }, { label: '客单价', baseline: Number(followup.baseline_avg_bill), target: Number(followup.target_avg_bill), actual: Number(followup.actual_avg_bill), lower: false, isCurrency: true }, { label: '优惠率', baseline: Number(followup.baseline_discount_rate), target: Number(followup.target_discount_rate), actual: Number(followup.actual_discount_rate), lower: true, isCurrency: false }, { label: '毛利率', baseline: Number(followup.baseline_margin_rate), target: Number(followup.target_margin_rate), actual: Number(followup.actual_margin_rate), lower: false, isCurrency: false }, ].map((m) => { const pct = !isNaN(m.target) && m.target !== 0 && !isNaN(m.actual) ? Math.min(Math.round((m.actual / m.target) * 100), 200) : 0 const achieved = !isNaN(m.actual) && !isNaN(m.target) && (m.lower ? m.actual <= m.target : m.actual >= m.target) const fmt = (v: number) => isNaN(v) ? '-' : m.isCurrency ? formatCurrency(v) : `${formatNumber(v)}%` return (
{m.label} 目标: {fmt(m.target)} | 实际: {fmt(m.actual)} | {pct}% {achieved ? '✓' : ''}
= 80 ? 'bg-yellow-500' : 'bg-red-500'}`} style={{ width: `${Math.min(pct, 100)}%` }} />
) })}
)}
{/* 日度趋势 + 月度指标对比 */}
{/* 日度实收趋势 */}

日度实收趋势

{dailyRows.length > 0 ? ( v?.substring(5, 10)} tick={{ fontSize: 10 }} /> v?.substring(0, 10)} /> ) :

暂无数据

}
{/* 月度指标对比 */}

月度指标对比

{followup ? ( {[ { label: '实收', baseline: Number(followup.baseline_received), target: Number(followup.target_received), actual: Number(followup.actual_received), lower: false, isCurrency: true }, { label: '客单价', baseline: Number(followup.baseline_avg_bill), target: Number(followup.target_avg_bill), actual: Number(followup.actual_avg_bill), lower: false, isCurrency: true }, { label: '优惠率(%)', baseline: Number(followup.baseline_discount_rate), target: Number(followup.target_discount_rate), actual: Number(followup.actual_discount_rate), lower: true, isCurrency: false }, { label: '理论毛利率(%)', baseline: Number(followup.baseline_margin_rate), target: Number(followup.target_margin_rate), actual: Number(followup.actual_margin_rate), lower: false, isCurrency: false }, { label: '异常率(%)', baseline: Number(followup.baseline_anomaly_rate), target: Number(followup.target_anomaly_rate), actual: Number(followup.actual_anomaly_rate), lower: true, isCurrency: false }, { label: '会员占比(%)', baseline: Number(followup.baseline_member_share), target: Number(followup.target_member_share), actual: Number(followup.actual_member_share), lower: false, isCurrency: false }, ].map((m) => { const achieved = !isNaN(m.actual) && !isNaN(m.target) && (m.lower ? m.actual <= m.target : m.actual >= m.target) const rate = !isNaN(m.actual) && !isNaN(m.baseline) && m.baseline !== 0 ? Math.round((m.actual / m.baseline) * 100) : null const fmt = (v: number) => isNaN(v) ? '-' : m.isCurrency ? formatCurrency(v) : formatNumber(v) return ( ) })}
指标 月基线 目标 实际 达成率
{m.label} {fmt(m.baseline)} {fmt(m.target)} {fmt(m.actual)} {rate !== null ? `${rate}%` : '-'} {achieved ? ' ✓' : ' ✗'}
) :

暂无数据

}
{/* 工作重点和行动计划 */} {followup && (followup.work_focus || followup.action_plan) && (
{followup.work_focus && (

工作重点

{followup.work_focus}

)} {followup.action_plan && (

行动计划

{followup.action_plan}

)}
)} {/* 核心SKU备货提醒 */} {coreSKUs.length > 0 && (

核心SKU备货提醒 (A级)

{coreSKUs.map((s: any, idx: number) => (
{s.dish_name || s.sku_name || '-'}
营收占比: {Number(s.revenue_share_pct || 0).toFixed(1)}% 累计: {Number(s.cumulative_revenue_share_pct || 0).toFixed(1)}%
))}
)} {/* 待办任务 */}

待办任务 ({todoTasks.length})

{todoTasks.length === 0 ? (

暂无待办任务

) : todoTasks.map((t: any) => (
{t.problem_indicator}

{t.action_required}

截止日: {t.deadline?.substring(0, 10)}

{activeTaskId === t.task_id ? (