feat: 混合模式UI优化 - 全局态势状态条 + 预警前置 + 风险分布条 + 健康度仪表盘
- 新建 StatusBanner 组件: 全局态势状态条(门店风险/预警/闭环健康度) - Layout: 侧边栏增加Activity图标,主内容区顶部嵌入StatusBanner - MetricCard: status增加左侧彩色竖条强化视觉层级 - DashboardPage: 预警区域前置到经营指标上方,有红色预警时默认展开 - DashboardPage: 标题下方新增风险分布条(红黄绿百分比) - DashboardPage: 闭环健康度增加RadialBarChart仪表盘 - DashboardPage: 预警卡片增加AlertTriangle图标和左侧彩色竖条 - StatusBanner: 各指标增加title tooltip取数说明
This commit is contained in:
@@ -2,6 +2,7 @@ import { ReactNode, useState } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { LayoutDashboard, Store, ClipboardList, TrendingUp, Settings, LogOut, Menu, X, Package, DollarSign, ShoppingBag, Users, AlertTriangle, Clock, Database, MapPin, PieChart, Wallet, CalendarClock, Activity, Crown, BarChart3, Receipt, Utensils, Landmark, ChefHat, Truck, Network, TrendingUp as TrendingUpIcon } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { StatusBanner } from '@/components/StatusBanner'
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode
|
||||
@@ -114,7 +115,8 @@ export function Layout({ children, user, onLogout }: LayoutProps) {
|
||||
'fixed left-0 top-0 z-40 h-full w-56 border-r bg-card transition-transform lg:translate-x-0',
|
||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
)}>
|
||||
<div className="flex h-14 items-center border-b px-4">
|
||||
<div className="flex h-14 items-center gap-2 border-b px-4">
|
||||
<Activity size={22} className="text-primary" />
|
||||
<span className="text-lg font-bold">态势感知系统</span>
|
||||
</div>
|
||||
<nav className="space-y-4 overflow-y-auto p-3 pb-20" style={{ maxHeight: 'calc(100vh - 3.5rem - 5rem)' }}>
|
||||
@@ -164,6 +166,9 @@ export function Layout({ children, user, onLogout }: LayoutProps) {
|
||||
{/* Main content */}
|
||||
<div className="lg:pl-56">
|
||||
<main className="min-h-screen p-4 pt-16 lg:pt-4">
|
||||
<div className="mb-4">
|
||||
<StatusBanner />
|
||||
</div>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,9 @@ interface MetricCardProps {
|
||||
}
|
||||
|
||||
const STATUS_STYLES: Record<string, string> = {
|
||||
good: 'border-green-200 bg-green-50/40',
|
||||
warn: 'border-yellow-200 bg-yellow-50/40',
|
||||
bad: 'border-red-200 bg-red-50/40',
|
||||
good: 'border-green-200 bg-green-50/40 border-l-4 border-l-green-500',
|
||||
warn: 'border-yellow-200 bg-yellow-50/40 border-l-4 border-l-yellow-500',
|
||||
bad: 'border-red-200 bg-red-50/40 border-l-4 border-l-red-500',
|
||||
}
|
||||
|
||||
const STATUS_VALUE_STYLES: Record<string, string> = {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import api from '@/lib/api'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AlertTriangle, Activity, CheckCircle, XCircle } from 'lucide-react'
|
||||
|
||||
export function StatusBanner({ month }: { month?: string }) {
|
||||
const navigate = useNavigate()
|
||||
const m = month || '2026-04'
|
||||
|
||||
const { data: riskData } = useQuery({
|
||||
queryKey: ['stores/risk', m],
|
||||
queryFn: () => api.get('/stores/risk', { params: { month: m } }),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const { data: alertsData } = useQuery({
|
||||
queryKey: ['sa-alerts', m],
|
||||
queryFn: () => api.get('/situational-awareness/alerts', { params: { month: m } }),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const { data: loopHealth } = useQuery({
|
||||
queryKey: ['loop-health'],
|
||||
queryFn: () => api.get('/tasks/loop-health'),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const riskRows = (riskData as any)?.data || []
|
||||
const alertSummary = (alertsData as any)?.data
|
||||
const lh = (loopHealth as any)?.data
|
||||
|
||||
const red = riskRows.filter((r: any) => r.risk_level === '红色').length
|
||||
const yellow = riskRows.filter((r: any) => r.risk_level === '黄色').length
|
||||
const green = riskRows.filter((r: any) => r.risk_level === '绿色').length
|
||||
const total = riskRows.length || 1
|
||||
|
||||
const alertRed = alertSummary?.red || 0
|
||||
const alertOrange = alertSummary?.orange || 0
|
||||
const alertTotal = alertSummary?.total || 0
|
||||
|
||||
const loopScore = lh ? (
|
||||
(parseFloat(lh.task_generation_rate) +
|
||||
parseFloat(lh.store_execution_rate) +
|
||||
parseFloat(lh.weekly_check_rate) +
|
||||
parseFloat(lh.monthly_review_rate) +
|
||||
parseFloat(lh.practice_promotion_rate)) / 5
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 rounded-lg border bg-card px-4 py-2 text-sm shadow-sm">
|
||||
{/* 门店风险分布 */}
|
||||
<div className="flex items-center gap-2" title="数据来源: /stores/risk · 按风险等级统计门店数量(红/黄/绿)">
|
||||
<Activity size={16} className="text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">门店风险</span>
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
title="红色: 存在严重问题需紧急整改"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors hover:bg-muted"
|
||||
>
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-red-500" />
|
||||
<span className="font-bold text-red-600">{red}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
title="黄色: 存在风险需关注"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors hover:bg-muted"
|
||||
>
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-yellow-500" />
|
||||
<span className="font-bold text-yellow-600">{yellow}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
title="绿色: 经营正常"
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors hover:bg-muted"
|
||||
>
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-green-500" />
|
||||
<span className="font-bold text-green-600">{green}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
{/* 风险分布条 */}
|
||||
<div className="hidden h-2 w-32 overflow-hidden rounded-full md:flex" title={`红${red} 黄${yellow} 绿${green} / 共${total}家`}>
|
||||
<div className="bg-red-500" style={{ width: `${red / total * 100}%` }} />
|
||||
<div className="bg-yellow-500" style={{ width: `${yellow / total * 100}%` }} />
|
||||
<div className="bg-green-500" style={{ width: `${green / total * 100}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
{/* 预警 */}
|
||||
<button
|
||||
onClick={() => navigate('/situational-awareness')}
|
||||
title="数据来源: /situational-awareness/alerts · 营收异动·成本异动·人力异动·平台依赖·任务逾期,每类最多10条"
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded px-2 py-0.5 transition-colors hover:bg-muted',
|
||||
alertRed > 0 && 'bg-red-50'
|
||||
)}
|
||||
>
|
||||
<AlertTriangle size={16} className={alertRed > 0 ? 'text-red-600' : alertOrange > 0 ? 'text-orange-600' : 'text-muted-foreground'} />
|
||||
<span className="text-xs font-medium text-muted-foreground">预警</span>
|
||||
{alertRed > 0 && <span className="rounded bg-red-600 px-1.5 py-0.5 text-xs font-bold text-white" title={`红色 ${alertRed} 条`}>{alertRed}</span>}
|
||||
{alertOrange > 0 && <span className="rounded bg-orange-500 px-1.5 py-0.5 text-xs font-bold text-white" title={`橙色 ${alertOrange} 条`}>{alertOrange}</span>}
|
||||
{alertRed === 0 && alertOrange === 0 && (
|
||||
<span className="flex items-center gap-1 text-xs text-green-600">
|
||||
<CheckCircle size={14} />
|
||||
正常
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{loopScore !== null && (
|
||||
<>
|
||||
<div className="h-4 w-px bg-border" />
|
||||
{/* 闭环健康度 */}
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
title="数据来源: /tasks/loop-health · 任务生成率·店长执行率·区域周检率·月度验收率·经验推广率 五项均值"
|
||||
className="flex items-center gap-1.5 rounded px-2 py-0.5 transition-colors hover:bg-muted"
|
||||
>
|
||||
<span className="text-xs font-medium text-muted-foreground">闭环健康度</span>
|
||||
<span className={cn(
|
||||
'text-sm font-bold',
|
||||
loopScore >= 80 ? 'text-green-600' : loopScore >= 60 ? 'text-yellow-600' : 'text-red-600'
|
||||
)}>
|
||||
{loopScore.toFixed(1)}%
|
||||
</span>
|
||||
<div className="h-1.5 w-12 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full',
|
||||
loopScore >= 80 ? 'bg-green-500' : loopScore >= 60 ? 'bg-yellow-500' : 'bg-red-500'
|
||||
)}
|
||||
style={{ width: `${Math.min(loopScore, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{m}</span>
|
||||
{alertRed > 0 ? (
|
||||
<span className="flex items-center gap-1 text-red-600">
|
||||
<XCircle size={12} />
|
||||
需关注
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-green-600">
|
||||
<CheckCircle size={12} />
|
||||
运行正常
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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, ScatterChart, Scatter, ZAxis, ReferenceLine, ComposedChart, Area, AreaChart } from 'recharts'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Legend, ScatterChart, Scatter, ZAxis, ReferenceLine, ComposedChart, Area, AreaChart, RadialBarChart, RadialBar } from 'recharts'
|
||||
import api from '@/lib/api'
|
||||
import { MetricCard } from '@/components/MetricCard'
|
||||
import { Badge } from '@/components/Badge'
|
||||
@@ -10,7 +10,7 @@ import { CollapsibleSection } from '@/components/CollapsibleSection'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
import { formatNumber, formatCurrency, formatPercent } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
import { TrendingUp, TrendingDown, Minus } from 'lucide-react'
|
||||
import { TrendingUp, TrendingDown, Minus, AlertTriangle } from 'lucide-react'
|
||||
|
||||
const RISK_COLORS = { '红色': '#ef4444', '黄色': '#eab308', '绿色': '#22c55e' }
|
||||
|
||||
@@ -202,15 +202,55 @@ export function DashboardPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
<div className="flex gap-2">
|
||||
<span className="rounded-md bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700">{totalStores} 家门店</span>
|
||||
<span className="rounded-md bg-red-50 px-3 py-1 text-xs font-medium text-red-700">红色 {riskSummary['红色'] || 0}</span>
|
||||
<span className="rounded-md bg-yellow-50 px-3 py-1 text-xs font-medium text-yellow-700">黄色 {riskSummary['黄色'] || 0}</span>
|
||||
<span className="rounded-md bg-green-50 px-3 py-1 text-xs font-medium text-green-700">绿色 {riskSummary['绿色'] || 0}</span>
|
||||
</div>
|
||||
<span className="rounded-md bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700">{totalStores} 家门店</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 风险分布条 */}
|
||||
<div className="rounded-lg border bg-card p-3 shadow-sm">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">门店风险分布</span>
|
||||
<div className="flex gap-3 text-xs">
|
||||
<span className="flex items-center gap-1"><span className="h-2.5 w-2.5 rounded-full bg-red-500" /><span className="font-bold text-red-600">{riskSummary['红色'] || 0}</span> 红色 ({Math.round((riskSummary['红色'] || 0) / totalStores * 100)}%)</span>
|
||||
<span className="flex items-center gap-1"><span className="h-2.5 w-2.5 rounded-full bg-yellow-500" /><span className="font-bold text-yellow-600">{riskSummary['黄色'] || 0}</span> 黄色 ({Math.round((riskSummary['黄色'] || 0) / totalStores * 100)}%)</span>
|
||||
<span className="flex items-center gap-1"><span className="h-2.5 w-2.5 rounded-full bg-green-500" /><span className="font-bold text-green-600">{riskSummary['绿色'] || 0}</span> 绿色 ({Math.round((riskSummary['绿色'] || 0) / totalStores * 100)}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-3 w-full overflow-hidden rounded-full">
|
||||
<div className="bg-red-500 transition-all" style={{ width: `${(riskSummary['红色'] || 0) / totalStores * 100}%` }} />
|
||||
<div className="bg-yellow-500 transition-all" style={{ width: `${(riskSummary['黄色'] || 0) / totalStores * 100}%` }} />
|
||||
<div className="bg-green-500 transition-all" style={{ width: `${(riskSummary['绿色'] || 0) / totalStores * 100}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 态势感知预警 - 前置 */}
|
||||
{alerts.length > 0 && (
|
||||
<CollapsibleSection
|
||||
title="态势感知预警"
|
||||
subtitle="营收异动 · 成本异动 · 人力异动 · 平台依赖 · 任务逾期"
|
||||
defaultOpen={alertSummary?.red > 0}
|
||||
headerRight={
|
||||
<div className="flex gap-2">
|
||||
{alertSummary?.red > 0 && <span className="rounded-md bg-red-600 px-2 py-0.5 text-xs font-bold text-white">{alertSummary.red} 红色</span>}
|
||||
{alertSummary?.orange > 0 && <span className="rounded-md bg-orange-500 px-2 py-0.5 text-xs font-bold text-white">{alertSummary.orange} 橙色</span>}
|
||||
<span className="text-xs text-muted-foreground">共 {alertSummary?.total || 0} 条</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
{alerts.slice(0, 6).map((a, i) => (
|
||||
<div key={i} className={`flex items-start gap-2 rounded-md border p-2.5 ${a.level === 'red' ? 'border-red-300 bg-red-50/60 border-l-4 border-l-red-500' : a.level === 'orange' ? 'border-orange-300 bg-orange-50/60 border-l-4 border-l-orange-500' : 'border-yellow-300 bg-yellow-50/60 border-l-4 border-l-yellow-500'}`}>
|
||||
<AlertTriangle size={16} className={`mt-0.5 shrink-0 ${a.level === 'red' ? 'text-red-600' : a.level === 'orange' ? 'text-orange-600' : 'text-yellow-600'}`} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs font-medium">{a.title}</p>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">{a.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* 经营指标 */}
|
||||
<CollapsibleSection title="经营指标" subtitle="营业收入、优惠、实收等核心经营数据">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
@@ -296,62 +336,48 @@ export function DashboardPage() {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-5">
|
||||
{[
|
||||
{ label: '任务生成率', value: lh.task_generation_rate, desc: '系统自动生成的整改任务占应生成任务的比例' },
|
||||
{ label: '店长执行率', value: lh.store_execution_rate, desc: '店长已提交反馈的任务占分配任务的比例' },
|
||||
{ label: '区域周检率', value: lh.weekly_check_rate, desc: '区域经理完成周度检查的比例' },
|
||||
{ label: '月度验收率', value: lh.monthly_review_rate, desc: '月度完成验收的任务比例' },
|
||||
{ label: '经验推广率', value: lh.practice_promotion_rate, desc: '达标经验被推广到其他门店的比例' },
|
||||
].map((item) => {
|
||||
const num = parseFloat(item.value)
|
||||
const isLow = num < 60
|
||||
const isMid = num >= 60 && num < 80
|
||||
return (
|
||||
<div key={item.label} className={`rounded-md border p-3 ${isLow ? 'border-red-200 bg-red-50/50' : isMid ? 'border-yellow-200 bg-yellow-50/50' : 'border-green-200 bg-green-50/50'}`} title={item.desc}>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xs text-muted-foreground">{item.label}</p>
|
||||
<span className="flex h-3.5 w-3.5 cursor-help items-center justify-center rounded-full bg-muted text-[9px] text-muted-foreground" title={item.desc}>?</span>
|
||||
</div>
|
||||
<p className={`mt-1 text-xl font-bold ${isLow ? 'text-red-600' : isMid ? 'text-yellow-600' : 'text-green-600'}`}>{num.toFixed(1)}%</p>
|
||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={`h-full rounded-full ${isLow ? 'bg-red-500' : isMid ? 'bg-yellow-500' : 'bg-green-500'}`}
|
||||
style={{ width: `${Math.min(num, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* 态势感知预警 */}
|
||||
{alerts.length > 0 && (
|
||||
<CollapsibleSection
|
||||
title="态势感知预警"
|
||||
subtitle="营收异动 · 成本异动 · 人力异动 · 平台依赖 · 任务逾期"
|
||||
headerRight={
|
||||
<div className="flex gap-2">
|
||||
{alertSummary?.red > 0 && <span className="rounded-md bg-red-50 px-2 py-0.5 text-xs font-medium text-red-700">红色 {alertSummary.red}</span>}
|
||||
{alertSummary?.orange > 0 && <span className="rounded-md bg-orange-50 px-2 py-0.5 text-xs font-medium text-orange-700">橙色 {alertSummary.orange}</span>}
|
||||
<span className="text-xs text-muted-foreground">共 {alertSummary?.total || 0} 条</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
{alerts.slice(0, 6).map((a, i) => (
|
||||
<div key={i} className={`flex items-start gap-2 rounded-md border p-2.5 ${a.level === 'red' ? 'border-red-200 bg-red-50/50' : a.level === 'orange' ? 'border-orange-200 bg-orange-50/50' : 'border-yellow-200 bg-yellow-50/50'}`}>
|
||||
<span className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[10px] font-bold ${a.level === 'red' ? 'text-red-700' : a.level === 'orange' ? 'text-orange-700' : 'text-yellow-700'}`}>
|
||||
{a.level === 'red' ? '红' : a.level === 'orange' ? '橙' : '黄'}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-xs font-medium">{a.title}</p>
|
||||
<p className="mt-0.5 truncate text-xs text-muted-foreground">{a.detail}</p>
|
||||
</div>
|
||||
<div className="grid gap-4 lg:grid-cols-5">
|
||||
{/* 仪表盘 */}
|
||||
<div className="flex flex-col items-center justify-center lg:col-span-1">
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<RadialBarChart innerRadius="65%" outerRadius="100%" data={[{ name: '综合得分', value: loopScore, fill: loopScore >= 80 ? '#22c55e' : loopScore >= 60 ? '#eab308' : '#ef4444' }]} startAngle={90} endAngle={-270}>
|
||||
<RadialBar background dataKey="value" cornerRadius={8} />
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="-mt-24 text-center">
|
||||
<p className={`text-2xl font-bold ${loopScore >= 80 ? 'text-green-600' : loopScore >= 60 ? 'text-yellow-600' : 'text-red-600'}`}>{loopScore.toFixed(1)}%</p>
|
||||
<p className="text-xs text-muted-foreground">综合健康度</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 5个指标条 */}
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-5 lg:col-span-4">
|
||||
{[
|
||||
{ label: '任务生成率', value: lh.task_generation_rate, desc: '系统自动生成的整改任务占应生成任务的比例' },
|
||||
{ label: '店长执行率', value: lh.store_execution_rate, desc: '店长已提交反馈的任务占分配任务的比例' },
|
||||
{ label: '区域周检率', value: lh.weekly_check_rate, desc: '区域经理完成周度检查的比例' },
|
||||
{ label: '月度验收率', value: lh.monthly_review_rate, desc: '月度完成验收的任务比例' },
|
||||
{ label: '经验推广率', value: lh.practice_promotion_rate, desc: '达标经验被推广到其他门店的比例' },
|
||||
].map((item) => {
|
||||
const num = parseFloat(item.value)
|
||||
const isLow = num < 60
|
||||
const isMid = num >= 60 && num < 80
|
||||
return (
|
||||
<div key={item.label} className={`rounded-md border p-3 ${isLow ? 'border-red-200 bg-red-50/50' : isMid ? 'border-yellow-200 bg-yellow-50/50' : 'border-green-200 bg-green-50/50'}`} title={item.desc}>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-xs text-muted-foreground">{item.label}</p>
|
||||
<span className="flex h-3.5 w-3.5 cursor-help items-center justify-center rounded-full bg-muted text-[9px] text-muted-foreground" title={item.desc}>?</span>
|
||||
</div>
|
||||
<p className={`mt-1 text-xl font-bold ${isLow ? 'text-red-600' : isMid ? 'text-yellow-600' : 'text-green-600'}`}>{num.toFixed(1)}%</p>
|
||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className={`h-full rounded-full ${isLow ? 'bg-red-500' : isMid ? 'bg-yellow-500' : 'bg-green-500'}`}
|
||||
style={{ width: `${Math.min(num, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user