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:
@@ -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})`}
|
||||
|
||||
Reference in New Issue
Block a user