import { useQuery } from '@tanstack/react-query' import api from '@/lib/api' import { DataTable } from '@/components/DataTable' import { Pagination } from '@/components/Pagination' import { LoadingSpinner } from '@/components/LoadingSpinner' import { CollapsibleSection } from '@/components/CollapsibleSection' import { MetricCard } from '@/components/MetricCard' import { Badge } from '@/components/Badge' import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils' import { useState, useMemo } from 'react' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ScatterChart, Scatter } from 'recharts' import { MonthPicker } from '@/components/MonthPicker' const PAGE_SIZE = 15 export function CostPage() { const [costPage, setCostPage] = useState(1) const [invPage, setInvPage] = useState(1) const [catPage, setCatPage] = useState(1) const [month, setMonth] = useState('2026-04') const { data: costData, isLoading: costLoading } = useQuery({ queryKey: ['cost/comparison', month], queryFn: () => api.get('/cost/comparison', { params: { month } }), }) const { data: invData, isLoading: invLoading } = useQuery({ queryKey: ['cost/inventory', month], queryFn: () => api.get('/cost/inventory', { params: { month } }), }) const { data: catData, isLoading: catLoading } = useQuery({ queryKey: ['cost/category-benchmark', month], queryFn: () => api.get('/cost/category-benchmark', { params: { month } }), }) const costRows = (costData as any)?.data || [] const invRows = (invData as any)?.data || [] const catRows = (catData as any)?.data || [] const pageLoading = costLoading || invLoading || catLoading const validCostRows = costRows.filter((r: any) => !r.variance_level?.includes('口径异常')) const abnormalRows = costRows.filter((r: any) => r.variance_level?.includes('口径异常')) const avgTheoreticalRate = validCostRows.length > 0 ? validCostRows.reduce((s: number, r: any) => s + Number(r.theoretical_cost_rate_pct || 0), 0) / validCostRows.length : 0 const avgActualRate = validCostRows.length > 0 ? validCostRows.reduce((s: number, r: any) => s + Number(r.actual_food_cost_rate_pct || 0), 0) / validCostRows.length : 0 const highVarianceCount = validCostRows.filter((r: any) => Number(r.variance_to_theoretical_pct || 0) > 20).length const abnormalInvCount = invRows.filter((r: any) => Number(r.estimated_inventory_days) > 7 || Number(r.negative_item_lines) > 0).length const pagedCost = useMemo(() => costRows.slice((costPage - 1) * PAGE_SIZE, costPage * PAGE_SIZE), [costRows, costPage]) const pagedInv = useMemo(() => invRows.slice((invPage - 1) * PAGE_SIZE, invPage * PAGE_SIZE), [invRows, invPage]) const pagedCat = useMemo(() => catRows.slice((catPage - 1) * PAGE_SIZE, catPage * PAGE_SIZE), [catRows, catPage]) const scatterData = validCostRows.map((r: any) => ({ name: r.store_name, theoretical: Number(r.theoretical_cost_rate_pct || 0), actual: Number(r.actual_food_cost_rate_pct || 0), variance: Number(r.variance_to_theoretical_pct || 0), })) if (pageLoading) { return } return (

成本与库存管理

理论vs实际成本 · 原料分类差异 · 库存效率 · {month}

{/* 概览指标 */}
{abnormalRows.length > 0 && (

口径异常门店({abnormalRows.length}家): {abnormalRows.map((r: any) => r.store_name).join('、')} — 理论成本数据缺失或口径不一致,需先修复数据再纳入对比

)}
{/* 成本散点图 */} { if (active && payload && payload.length) { const d = payload[0].payload return (

{d.name}

理论: {d.theoretical.toFixed(1)}%

实际: {d.actual.toFixed(1)}%

偏差: {d.variance > 0 ? '+' : ''}{d.variance.toFixed(1)}%

) } return null }} />
{/* 成本对比表 */}
formatPercent(r.theoretical_cost_rate_pct) }, { key: 'actual_food_cost_rate_pct', label: '实际成本率', align: 'right', render: (r) => formatPercent(r.actual_food_cost_rate_pct) }, { key: 'variance_to_theoretical_pct', label: '偏差', align: 'right', render: (r) => { const v = Number(r.variance_to_theoretical_pct || 0) return 20 ? 'font-medium text-red-600' : v > 10 ? 'text-yellow-600' : 'text-green-600'}>{v > 0 ? '+' : ''}{v.toFixed(1)}% }}, { key: 'variance_level', label: '偏差等级', render: (r) => ( {r.variance_level || '-'} )}, { key: 'actual_food_cost', label: '实际食材成本', align: 'right', render: (r) => formatCurrency(r.actual_food_cost) }, ]} data={pagedCost} />
{/* 库存效率 */}
{ const d = Number(r.estimated_inventory_days || 0) return 7 ? 'font-medium text-red-600' : d > 4 ? 'text-yellow-600' : 'text-green-600'}>{d.toFixed(1)}天 }}, { key: 'ending_inventory_amount', label: '期末库存', align: 'right', render: (r) => formatCurrency(r.ending_inventory_amount) }, { key: 'negative_item_lines', label: '负耗用行数', align: 'center', render: (r) => { const n = Number(r.negative_item_lines || 0) return n > 0 ? {n} : 0 }}, { key: 'abnormal_item_lines', label: '异常货品行', align: 'center', render: (r) => { const n = Number(r.abnormal_item_lines || 0) return n > 0 ? {n} : 0 }}, ]} data={pagedInv} />
{/* 原料分类差异 */}
formatCurrency(r.actual_category_cost) }, { key: 'cost_per_10k_sales', label: '每万元成本', align: 'right', render: (r) => formatCurrency(r.cost_per_10k_sales) }, { key: 'peer_median_cost_per_10k', label: '同业中位', align: 'right', render: (r) => formatCurrency(r.peer_median_cost_per_10k) }, { key: 'excess_vs_peer_per_10k', label: '超出同业', align: 'right', render: (r) => { const v = Number(r.excess_vs_peer_per_10k || 0) return v > 0 ? +{formatCurrency(v)} : {formatCurrency(v)} }}, ]} data={pagedCat} />
) }