Phase 6-11: 驾驶舱图表/区域经理基准对比+周检录入/店长SKU备货+指标进度/门店详情7Tab/月度验收7Tab/区域汇总

- Phase 6: 驾驶舱新增象限散点图、P0/P1实收覆盖瀑布图、平台成本率TOP15柱状图
- Phase 7: 区域经理页面新增同类门店基准对比表、周检录入弹窗
- Phase 8: 店长页面新增核心SKU备货提醒、本周指标进度条,经营概览与进度并排
- Phase 9: 门店详情页7Tab(概览/餐段/品类/成本/会员/异常/任务),5个新API
- Phase 10: 月度验收7Tab(验收汇总/升降级/完成率/活动清单/SKU治理/经验标准化/复盘优化),4个新API+阈值调整API
- Phase 11: 前后端编译通过,API测试通过,浏览器验证0 error
- 门店区域划分:从store_location_master填充dim_store.region(87家),v_region_summary视图+API+前端表格
- MetricCard增加text格式支持
This commit is contained in:
freedakgmail
2026-07-26 23:16:52 +08:00
parent a7874d79b5
commit 521ba88937
9 changed files with 1069 additions and 107 deletions
+95 -1
View File
@@ -1,6 +1,6 @@
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 { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Legend, ScatterChart, Scatter, ZAxis, ReferenceLine, ComposedChart } from 'recharts'
import api from '@/lib/api'
import { MetricCard } from '@/components/MetricCard'
import { Badge } from '@/components/Badge'
@@ -43,6 +43,16 @@ export function DashboardPage() {
queryFn: () => api.get('/tasks/loop-health'),
})
const { data: quadrantData } = useQuery({
queryKey: ['stores/quadrant'],
queryFn: () => api.get('/stores/quadrant'),
})
const { data: platformData } = useQuery({
queryKey: ['platform/economics'],
queryFn: () => api.get('/platform/economics'),
})
const pageLoading = odLoading || dailyLoading || riskLoading || priorityLoading || loopLoading
const od = (overview as any)?.data
@@ -50,6 +60,39 @@ export function DashboardPage() {
const priorityRows = (priorityData as any)?.data || []
const dailyRows = (daily as any)?.data || []
const lh = (loopHealth as any)?.data
const quadrantRows = (quadrantData as any)?.data || []
const platformRows = (platformData as any)?.data || []
const QUADRANT_COLORS: Record<string, string> = {
'明星门店': '#22c55e', '稳健经营': '#3b82f6', '规模承压': '#eab308', '重点改善': '#ef4444', '高效潜力': '#a855f7',
}
const quadrantScatterData = quadrantRows.map((r: any) => ({
name: r.store_name,
x: Number(r.avg_daily_received || 0),
y: Number(r.theoretical_margin_pct || 0),
quadrant: r.management_quadrant,
fill: QUADRANT_COLORS[r.management_quadrant] || '#999',
}))
const platformCostData = platformRows.slice(0, 15).map((r: any) => ({
name: r.store_name?.substring(0, 6) || '',
meituan: Number(r.meituan_cost_rate_pct || 0),
eleme: Number(r.eleme_cost_rate_pct || 0),
douyin: Number(r.douyin_cost_rate_pct || 0),
}))
const p0Stores = priorityRows.filter((s: any) => s.action_priority?.startsWith('P0'))
const p1Stores = priorityRows.filter((s: any) => s.action_priority?.startsWith('P1'))
const p0Received = p0Stores.reduce((s: number, r: any) => s + Number(r.received || 0), 0)
const p1Received = p1Stores.reduce((s: number, r: any) => s + Number(r.received || 0), 0)
const otherReceived = od?.received ? Number(od.received) - p0Received - p1Received : 0
const waterfallData = [
{ name: 'P0门店', value: p0Received, fill: '#ef4444' },
{ name: 'P1门店', value: p1Received, fill: '#eab308' },
{ name: '其他门店', value: otherReceived, fill: '#22c55e' },
{ name: '合计', value: Number(od?.received || 0), fill: '#3b82f6' },
]
const riskSummary = riskRows.reduce((acc: any, r: any) => {
acc[r.risk_level] = (acc[r.risk_level] || 0) + 1
@@ -214,6 +257,57 @@ export function DashboardPage() {
</CollapsibleSection>
</div>
{/* 第二行图表 */}
<div className="grid gap-4 lg:grid-cols-3">
{/* 象限散点图 */}
<CollapsibleSection title="门店经营象限" subtitle="日均实收 × 毛利率">
<ResponsiveContainer width="100%" height={250}>
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" dataKey="x" name="日均实收" tickFormatter={(v) => `${(v / 10000).toFixed(0)}`} tick={{ fontSize: 10 }} />
<YAxis type="number" dataKey="y" name="毛利率" unit="%" tick={{ fontSize: 10 }} />
<ZAxis range={[40, 40]} />
<Tooltip cursor={{ strokeDasharray: '3 3' }} formatter={(v: any, n: string) => n === '毛利率' ? `${Number(v).toFixed(1)}%` : n === '日均实收' ? `¥${Number(v).toFixed(0)}` : v} />
{Object.entries(QUADRANT_COLORS).map(([name, color]) => (
<Scatter key={name} name={name} data={quadrantScatterData.filter((d: any) => d.quadrant === name)} fill={color} />
))}
<Legend wrapperStyle={{ fontSize: 10 }} />
</ScatterChart>
</ResponsiveContainer>
</CollapsibleSection>
{/* P0/P1实收覆盖瀑布图 */}
<CollapsibleSection title="P0/P1实收覆盖" subtitle="按优先级拆分实收">
<ResponsiveContainer width="100%" height={250}>
<BarChart data={waterfallData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" tick={{ fontSize: 10 }} />
<YAxis tickFormatter={(v) => v >= 10000 ? `${(v / 10000).toFixed(0)}` : v} tick={{ fontSize: 10 }} />
<Tooltip formatter={(v: any) => formatCurrency(v)} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{waterfallData.map((entry, idx) => <Cell key={idx} fill={entry.fill} />)}
</Bar>
</BarChart>
</ResponsiveContainer>
</CollapsibleSection>
{/* 平台成本率柱状图 */}
<CollapsibleSection title="平台成本率TOP15" subtitle="三平台成本率对比">
<ResponsiveContainer width="100%" height={250}>
<BarChart data={platformCostData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" tick={{ fontSize: 9 }} angle={-30} textAnchor="middle" height={50} />
<YAxis unit="%" tick={{ fontSize: 10 }} />
<Tooltip />
<Legend wrapperStyle={{ fontSize: 10 }} />
<Bar dataKey="meituan" name="美团" fill="#f97316" />
<Bar dataKey="eleme" name="饿了么" fill="#3b82f6" />
<Bar dataKey="douyin" name="抖音" fill="#ec4899" />
</BarChart>
</ResponsiveContainer>
</CollapsibleSection>
</div>
{/* P0/P1 门店列表 */}
<CollapsibleSection
title={`P0/P1 重点整改门店 (${p0p1Stores.length})`}