From ac8e39e2f95807371fad1fadf9124be765a29d11 Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Sat, 1 Aug 2026 15:38:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=97=A8=E5=BA=97=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=E6=B7=BB=E5=8A=A0=E5=81=A5=E5=BA=B7=E5=BA=A6=E8=AF=84?= =?UTF-8?q?=E5=88=86(=E6=A0=87=E9=A2=98=E6=97=81=E5=88=86=E6=95=B0+?= =?UTF-8?q?=E7=8A=B6=E6=80=81,=20=E6=A6=82=E8=A7=88Tab=E9=9B=B7=E8=BE=BE?= =?UTF-8?q?=E5=9B=BE+=E5=BE=97=E5=88=86=E6=9D=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/SituationalAwarenessPage.tsx | 6 +- client/src/pages/StoreDetailPage.tsx | 67 ++++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/client/src/pages/SituationalAwarenessPage.tsx b/client/src/pages/SituationalAwarenessPage.tsx index 0d079c0..00b075e 100644 --- a/client/src/pages/SituationalAwarenessPage.tsx +++ b/client/src/pages/SituationalAwarenessPage.tsx @@ -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: - + diff --git a/client/src/pages/StoreDetailPage.tsx b/client/src/pages/StoreDetailPage.tsx index e25cffc..b566cb3 100644 --- a/client/src/pages/StoreDetailPage.tsx +++ b/client/src/pages/StoreDetailPage.tsx @@ -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() {

{sc.store_name}

+ {health && ( + <> + = 75 ? 'text-green-600' : Number(health.health_score) >= 55 ? 'text-yellow-600' : 'text-red-600')}> + {health.health_score} + + {health.health_status} + + )} {risk?.risk_level && } {quadrant && {quadrant}} @@ -234,6 +264,41 @@ export function StoreDetailPage() { {/* 概览 Tab */} {tab === 'overview' && (
+ {health && ( +
+

健康度评分维度

+
+ + ({ dimension: d.label, score: Math.round(Number(health[d.key] || 0) / d.max * 100), fullMark: 100 }))}> + + + + = 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} /> + + + +
+ {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 ( +
+
+ {d.label} + {score.toFixed(1)} / {d.max} +
+
+
+
+
+ ) + })} +
+
+
+ )} +

日度实收趋势