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:
@@ -43,12 +43,25 @@ export function StorePage() {
|
||||
queryFn: () => api.get('/tasks/followup'),
|
||||
})
|
||||
|
||||
const { data: skuData } = useQuery({
|
||||
queryKey: ['sku-attach', storeCode],
|
||||
queryFn: () => api.get('/sku/attach'),
|
||||
})
|
||||
|
||||
const { data: abcData } = useQuery({
|
||||
queryKey: ['sku-abc'],
|
||||
queryFn: () => api.get('/sku/abc'),
|
||||
})
|
||||
|
||||
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)
|
||||
@@ -130,27 +143,66 @@ export function StorePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 经营概览 */}
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-bold">最新经营概览(5月1日)</h2>
|
||||
{anomalyCount > 0 && (
|
||||
<span className="rounded-md bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">{anomalyCount} 项异常</span>
|
||||
{/* 最新经营概览 + 本周指标进度 一行 */}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{/* 经营概览 */}
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-bold">最新经营概览(5月1日)</h2>
|
||||
{anomalyCount > 0 && (
|
||||
<span className="rounded-md bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">{anomalyCount} 项异常</span>
|
||||
)}
|
||||
</div>
|
||||
{allAnomalyItems.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">暂无数据</p>
|
||||
) : (
|
||||
<div className="grid gap-2 md:grid-cols-3">
|
||||
{allAnomalyItems.map((item, idx) => (
|
||||
<div key={idx} className={`rounded-md border p-3 ${item.is_anomaly ? 'border-red-200 bg-red-50/50' : ''}`}>
|
||||
<p className="text-xs text-muted-foreground">{item.metric}</p>
|
||||
<p className={`mt-1 text-lg font-bold ${item.is_anomaly ? 'text-red-600' : ''}`}>
|
||||
{item.metric === '实收' ? formatCurrency(item.value) : formatNumber(item.value)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">日基准: {item.metric === '实收' ? formatCurrency(item.baseline) : formatNumber(item.baseline)}{item.is_anomaly && <span className="text-red-500"> ⚠</span>}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{allAnomalyItems.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">暂无数据</p>
|
||||
) : (
|
||||
<div className="grid gap-2 md:grid-cols-3 lg:grid-cols-6">
|
||||
{allAnomalyItems.map((item, idx) => (
|
||||
<div key={idx} className={`rounded-md border p-3 ${item.is_anomaly ? 'border-red-200 bg-red-50/50' : ''}`}>
|
||||
<p className="text-xs text-muted-foreground">{item.metric}</p>
|
||||
<p className={`mt-1 text-lg font-bold ${item.is_anomaly ? 'text-red-600' : ''}`}>
|
||||
{item.metric === '实收' ? formatCurrency(item.value) : formatNumber(item.value)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">日基准: {item.metric === '实收' ? formatCurrency(item.baseline) : formatNumber(item.baseline)}{item.is_anomaly && <span className="text-red-500"> ⚠</span>}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 本周指标进度条 */}
|
||||
{followup && (
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<h2 className="mb-3 text-sm font-bold">本周指标进度</h2>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ 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 (
|
||||
<div key={m.label}>
|
||||
<div className="mb-1 flex items-center justify-between text-xs">
|
||||
<span className="font-medium">{m.label}</span>
|
||||
<span className="text-muted-foreground">
|
||||
目标: {fmt(m.target)} | 实际: {fmt(m.actual)} | {pct}% {achieved ? '✓' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${achieved ? 'bg-green-500' : pct >= 80 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||
style={{ width: `${Math.min(pct, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -236,6 +288,27 @@ export function StorePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 核心SKU备货提醒 */}
|
||||
{coreSKUs.length > 0 && (
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<h2 className="mb-3 text-sm font-bold">核心SKU备货提醒 (A级)</h2>
|
||||
<div className="space-y-2">
|
||||
{coreSKUs.map((s: any, idx: number) => (
|
||||
<div key={idx} className="flex items-center justify-between rounded-md border p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge type="priority" text="A" />
|
||||
<span className="text-sm font-medium">{s.dish_name || s.sku_name || '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span>营收占比: {Number(s.revenue_share_pct || 0).toFixed(1)}%</span>
|
||||
<span>累计: {Number(s.cumulative_revenue_share_pct || 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 待办任务 */}
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<h2 className="mb-3 text-sm font-bold">待办任务 ({todoTasks.length})</h2>
|
||||
|
||||
Reference in New Issue
Block a user