)
diff --git a/client/src/pages/SituationalAwarenessPage.tsx b/client/src/pages/SituationalAwarenessPage.tsx
index 41a26a0..86eb9e8 100644
--- a/client/src/pages/SituationalAwarenessPage.tsx
+++ b/client/src/pages/SituationalAwarenessPage.tsx
@@ -11,6 +11,7 @@ import { Badge } from '@/components/Badge'
import { formatCurrency, cn } from '@/lib/utils'
import { Activity, AlertTriangle, Link2, TrendingUp } from 'lucide-react'
import { MonthPicker } from '@/components/MonthPicker'
+import { SearchSelect } from '@/components/SearchSelect'
const PAGE_SIZE = 20
@@ -414,20 +415,31 @@ function AlertList({ alerts }: { alerts: any[] }) {
return (
<>
{paged.map((a: any, i: number) => (
diff --git a/client/src/pages/StorePage.tsx b/client/src/pages/StorePage.tsx
index bb94547..51b9f34 100644
--- a/client/src/pages/StorePage.tsx
+++ b/client/src/pages/StorePage.tsx
@@ -1,18 +1,44 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from '@/lib/api'
import { Badge } from '@/components/Badge'
-import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
-import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
+import { MetricCard } from '@/components/MetricCard'
+import { FilterableTable } from '@/components/FilterableTable'
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, PieChart, Pie, Legend } from 'recharts'
+import { formatCurrency, formatPercent, formatNumber, cn } from '@/lib/utils'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { MonthPicker } from '@/components/MonthPicker'
+import { SearchSelect } from '@/components/SearchSelect'
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 }[] = [
+ { key: 'overview', label: '概览' },
+ { key: 'meal', label: '餐段分析' },
+ { key: 'category', label: '品类结构' },
+ { key: 'cost', label: '成本分析' },
+ { key: 'member', label: '会员分析' },
+ { key: 'anomaly', label: '异常账单' },
+]
+
export function StorePage() {
const queryClient = useQueryClient()
const [executeText, setExecuteText] = useState('')
const [activeTaskId, setActiveTaskId] = useState
(null)
const [storeCode, setStoreCode] = useState('1111')
const [month, setMonth] = useState('2026-04')
+ const [detailTab, setDetailTab] = useState('overview')
const { data: storesData, isLoading: storesLoading } = useQuery({
queryKey: ['stores-list', month],
@@ -55,6 +81,47 @@ export function StorePage() {
queryFn: () => api.get('/sku/abc', { params: { month } }),
})
+ // StoreDetailPage queries
+ const { data: storeDetailData } = useQuery({
+ queryKey: ['store-detail', storeCode, month],
+ queryFn: () => api.get(`/stores/${storeCode}`, { params: { month } }),
+ })
+
+ const { data: healthData } = useQuery({
+ queryKey: ['store-health', storeCode, month],
+ queryFn: () => api.get('/situational-awareness/health-score', { params: { month } }),
+ })
+
+ const { data: mealData } = useQuery({
+ queryKey: ['store', storeCode, 'meal-period', month],
+ queryFn: () => api.get(`/stores/${storeCode}/meal-period`, { params: { month } }),
+ enabled: detailTab === 'meal',
+ })
+
+ const { data: catData } = useQuery({
+ queryKey: ['store', storeCode, 'category-mix', month],
+ queryFn: () => api.get(`/stores/${storeCode}/category-mix`, { params: { month } }),
+ enabled: detailTab === 'category',
+ })
+
+ const { data: costData } = useQuery({
+ queryKey: ['store', storeCode, 'cost', month],
+ queryFn: () => api.get(`/stores/${storeCode}/cost`, { params: { month } }),
+ enabled: detailTab === 'cost',
+ })
+
+ const { data: memberData } = useQuery({
+ queryKey: ['store', storeCode, 'member', month],
+ queryFn: () => api.get(`/stores/${storeCode}/member`, { params: { month } }),
+ enabled: detailTab === 'member',
+ })
+
+ const { data: anomalyDetailData } = useQuery({
+ queryKey: ['store', storeCode, 'anomalies', month],
+ queryFn: () => api.get(`/stores/${storeCode}/anomalies`, { params: { month, page_size: 200 } }),
+ enabled: detailTab === 'anomaly',
+ })
+
const card = (cardData as any)?.data
const anomalies = card?.anomalies || []
const tasks = (tasksData as any)?.data || []
@@ -68,6 +135,55 @@ export function StorePage() {
const priorityRows = (priorityData as any)?.data || []
const priorityInfo = priorityRows.find((p: any) => p.store_code === storeCode)
+ // StoreDetailPage derived data
+ const sd = (storeDetailData as any)?.data
+ const healthRows = ((healthData as any)?.data) || []
+ const health = healthRows.find((r: any) => r.store_code === storeCode)
+ const mealRows = ((mealData as any)?.data) || []
+ const cat = (catData as any)?.data
+ const cost = (costData as any)?.data
+ const member = (memberData as any)?.data
+ const anomalyDetailRows = ((anomalyDetailData as any)?.data) || []
+ const anomalyDetailTotal = (anomalyDetailData as any)?.meta?.total || anomalyDetailRows.length
+
+ const sc = sd?.scorecard
+ const action = sd?.action
+ const risk = sd?.risk
+
+ const riskDesc: Record = {
+ '绿色': { color: 'text-green-600 bg-green-50 border-green-200', desc: '各项指标健康,经营状态良好,维持现有运营策略即可。' },
+ '黄色': { color: 'text-yellow-600 bg-yellow-50 border-yellow-200', desc: '存在单项指标偏差,需关注并针对性改善,避免风险升级。' },
+ '红色': { color: 'text-red-600 bg-red-50 border-red-200', desc: '多项指标异常,经营风险较高,需立即介入并制定整改计划。' },
+ }
+
+ const quadrantDesc: Record = {
+ '明星门店': '高营收高毛利,是公司的核心利润来源。应总结其成功经验并推广至其他门店。',
+ '现金牛门店': '高营收但毛利偏低,通过成本优化有较大利润提升空间。',
+ '潜力门店': '营收偏低但毛利较好,具备增长潜力,需提升客流和营收规模。',
+ '问题门店': '营收和毛利均偏低,需全面诊断并考虑调整经营策略或选址。',
+ }
+
+ const riskInfo = risk?.risk_level ? riskDesc[risk.risk_level] : null
+ const quadrant = action?.management_quadrant || ''
+ const quadrantInfo = quadrantDesc[quadrant]
+
+ const companyAvg = { daily_rev: 20144, bill: 36.8, discount: 20.5, margin: 71.5, member: 15.9, repeat: 37.0, delivery: 36.1, combo: 20.6, items: 4.05 }
+
+ const deviations: { label: string; store: number; company: number; unit: string; good: 'high' | 'low' }[] = [
+ { label: '日均营收', store: Number(risk?.avg_daily_received) || 0, company: companyAvg.daily_rev, unit: '元', good: 'high' },
+ { label: '客单价', store: Number(risk?.avg_bill_value) || 0, company: companyAvg.bill, unit: '元', good: 'high' },
+ { label: '优惠率', store: Number(risk?.discount_rate_pct) || 0, company: companyAvg.discount, unit: '%', good: 'low' },
+ { label: '毛利率', store: Number(risk?.theoretical_margin_pct) || 0, company: companyAvg.margin, unit: '%', good: 'high' },
+ { label: '会员占比', store: Number(risk?.member_bill_share_pct) || 0, company: companyAvg.member, unit: '%', good: 'high' },
+ { label: '复购率', store: Number(action?.repeat_rate_pct) || 0, company: companyAvg.repeat, unit: '%', good: 'high' },
+ { label: '外卖占比', store: Number(action?.delivery_bill_share_pct) || 0, company: companyAvg.delivery, unit: '%', good: 'low' },
+ { label: '成本差异率', store: Number(action?.variance_to_theoretical_pct) || 0, company: 20, unit: '%', good: 'low' },
+ ]
+ const problemDeviations = deviations.filter(d => {
+ const diff = d.store - d.company
+ return d.good === 'high' ? diff < 0 : diff > 0
+ })
+
const storeInfo = stores.find((s: any) => s.code === storeCode)
const storeName = storeInfo?.name || ''
const primaryIssue = storeInfo?.issue || priorityInfo?.problem_combination || '-'
@@ -113,15 +229,13 @@ export function StorePage() {
店长工作台
-
+ onChange={setStoreCode}
+ options={stores.map((s: any) => ({ value: s.code, label: s.name }))}
+ emptyLabel="选择门店"
+ className="min-w-[160px]"
+ />