feat: 门店详情页添加健康度评分(标题旁分数+状态, 概览Tab雷达图+得分条)
This commit is contained in:
@@ -225,8 +225,8 @@ const SCORE_DIMENSIONS = [
|
||||
function HealthScoreDetail({ store, onClose, navigate }: { store: any; onClose: () => void; navigate: (path: string) => void }) {
|
||||
const radarData = SCORE_DIMENSIONS.map(d => ({
|
||||
dimension: d.label,
|
||||
score: Number(store[d.key] || 0),
|
||||
fullMark: d.max,
|
||||
score: Math.round(Number(store[d.key] || 0) / d.max * 100),
|
||||
fullMark: 100,
|
||||
}))
|
||||
|
||||
const healthColor = Number(store.health_score) >= 75 ? '#22c55e' : Number(store.health_score) >= 55 ? '#eab308' : '#ef4444'
|
||||
@@ -255,7 +255,7 @@ function HealthScoreDetail({ store, onClose, navigate }: { store: any; onClose:
|
||||
<RadarChart data={radarData}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="dimension" tick={{ fontSize: 10 }} />
|
||||
<PolarRadiusAxis angle={90} domain={[0, 20]} tick={{ fontSize: 8 }} />
|
||||
<PolarRadiusAxis angle={90} domain={[0, 100]} tick={{ fontSize: 8 }} />
|
||||
<Radar dataKey="score" stroke={healthColor} fill={healthColor} fillOpacity={0.3} />
|
||||
<Tooltip />
|
||||
</RadarChart>
|
||||
|
||||
@@ -8,11 +8,22 @@ import { DataTable } from '@/components/DataTable'
|
||||
import { Pagination } from '@/components/Pagination'
|
||||
import { FilterableTable } from '@/components/FilterableTable'
|
||||
import { LoadingSpinner } from '@/components/LoadingSpinner'
|
||||
import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
|
||||
import { formatCurrency, formatPercent, formatNumber, cn } from '@/lib/utils'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
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 }[] = [
|
||||
@@ -37,6 +48,11 @@ export function StoreDetailPage() {
|
||||
queryFn: () => api.get(`/stores/${code}`),
|
||||
})
|
||||
|
||||
const { data: healthData } = useQuery({
|
||||
queryKey: ['store-health', code, month],
|
||||
queryFn: () => api.get('/situational-awareness/health-score', { params: { month } }),
|
||||
})
|
||||
|
||||
const { data: dailyData } = useQuery({
|
||||
queryKey: ['store', code, 'daily'],
|
||||
queryFn: () => api.get(`/stores/${code}/daily`),
|
||||
@@ -80,6 +96,8 @@ export function StoreDetailPage() {
|
||||
|
||||
const sd = (storeData as any)?.data
|
||||
const daily = ((dailyData as any)?.data) || []
|
||||
const healthRows = ((healthData as any)?.data) || []
|
||||
const health = healthRows.find((r: any) => r.store_code === code)
|
||||
const tasks = ((tasksData as any)?.data) || []
|
||||
const mealRows = ((mealData as any)?.data) || []
|
||||
const cat = (catData as any)?.data
|
||||
@@ -137,6 +155,18 @@ export function StoreDetailPage() {
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-xl font-bold">{sc.store_name}</h1>
|
||||
{health && (
|
||||
<>
|
||||
<span className={cn('text-lg font-bold', Number(health.health_score) >= 75 ? 'text-green-600' : Number(health.health_score) >= 55 ? 'text-yellow-600' : 'text-red-600')}>
|
||||
{health.health_score}
|
||||
</span>
|
||||
<span className={cn('inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium',
|
||||
health.health_status === '健康' ? 'bg-green-100 text-green-700 border-green-300' :
|
||||
health.health_status === '亚健康' ? 'bg-yellow-100 text-yellow-700 border-yellow-300' :
|
||||
'bg-red-100 text-red-700 border-red-300')
|
||||
}>{health.health_status}</span>
|
||||
</>
|
||||
)}
|
||||
{risk?.risk_level && <Badge type="risk" text={risk.risk_level} />}
|
||||
{quadrant && <span className="rounded-md bg-muted px-2 py-0.5 text-xs">{quadrant}</span>}
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
@@ -234,6 +264,41 @@ export function StoreDetailPage() {
|
||||
{/* 概览 Tab */}
|
||||
{tab === 'overview' && (
|
||||
<div className="space-y-4">
|
||||
{health && (
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<h2 className="mb-3 text-sm font-bold">健康度评分维度</h2>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
<RadarChart data={SCORE_DIMENSIONS.map(d => ({ dimension: d.label, score: Math.round(Number(health[d.key] || 0) / d.max * 100), fullMark: 100 }))}>
|
||||
<PolarGrid />
|
||||
<PolarAngleAxis dataKey="dimension" tick={{ fontSize: 10 }} />
|
||||
<PolarRadiusAxis angle={90} domain={[0, 100]} tick={{ fontSize: 8 }} />
|
||||
<Radar dataKey="score" stroke={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fill={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fillOpacity={0.3} />
|
||||
<Tooltip />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="space-y-2">
|
||||
{SCORE_DIMENSIONS.map(d => {
|
||||
const score = Number(health[d.key] || 0)
|
||||
const pct = (score / d.max) * 100
|
||||
const color = pct >= 75 ? '#22c55e' : pct >= 50 ? '#eab308' : '#ef4444'
|
||||
return (
|
||||
<div key={d.key}>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-medium">{d.label}</span>
|
||||
<span className="text-muted-foreground">{score.toFixed(1)} / {d.max}</span>
|
||||
</div>
|
||||
<div className="mt-0.5 h-2 overflow-hidden rounded-full bg-muted">
|
||||
<div className="h-full rounded-full" style={{ width: `${pct}%`, backgroundColor: color }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<h2 className="mb-3 text-sm font-bold">日度实收趋势</h2>
|
||||
<ResponsiveContainer width="100%" height={250}>
|
||||
|
||||
Reference in New Issue
Block a user