import { useQuery } from '@tanstack/react-query' import { createFileRoute, Link } from '@tanstack/react-router' import { Activity, AlertTriangle, ArrowRight, ShieldCheck, Tags, TrendingDown } from 'lucide-react' import { Area, CartesianGrid, ComposedChart, Line, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts' import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent' import { orpc } from '@/client/orpc' import { Badge, Card, SectionTitle } from '@/components/ui' import type { DashboardSnapshot, DeviceStatus } from '@/domain/battery' import { BATTERY_LIST_SORT, DEVICE_STATUS } from '@/domain/battery' export const Route = createFileRoute('/')({ component: Dashboard, errorComponent: ({ error }) => (

数据加载失败

{error.message}

), }) function buildChartData(soh: DashboardSnapshot['soh']) { const chartData: { month: string; history?: number; forecast?: number }[] = soh.history.map((h) => ({ month: h.month, history: h.value, forecast: undefined, })) if (chartData.length > 0 && soh.forecast.length > 0) { // Overlap: last history point is also first forecast point const last = chartData[chartData.length - 1] if (last) { last.forecast = soh.forecast[0]?.value } } const forecastStart = chartData.length > 0 ? 1 : 0 for (let i = forecastStart; i < soh.forecast.length; i++) { const f = soh.forecast[i] if (f) { chartData.push({ month: f.month, history: undefined, forecast: f.value, }) } } return chartData } const statusVariantMap: Record = { [DEVICE_STATUS.HEALTHY]: 'success', [DEVICE_STATUS.WATCH]: 'warning', [DEVICE_STATUS.WARNING]: 'danger', } const severityVariantMap: Record = { 高: 'danger', 中: 'warning', 低: 'muted', } function formatChartTooltip(value: ValueType | undefined, name: NameType | undefined) { const numericValue = typeof value === 'number' ? value : Number(value) return [ `${Number.isFinite(numericValue) ? numericValue.toFixed(1) : (value ?? '-')}%`, name === 'history' ? '历史观测' : '趋势预测', ] } function formatPercent(value: number | null) { return value === null ? '—' : value.toFixed(1) } function formatPercentWithUnit(value: number | null) { return value === null ? '预测不可用' : `${value.toFixed(1)}%` } function formatDelta(from: number | null, to: number | null) { if (from === null || to === null) return '预测不可用' return `${(from - to).toFixed(1)}% 衰减` } function widthPercent(value: number | null) { return `${Math.max(0, Math.min(100, value ?? 0))}%` } function Dashboard() { const { data, error, isPending } = useQuery(orpc.battery.dashboard.queryOptions()) if (error) { return (

数据加载失败

{error.message}

) } if (isPending || !data) { return (

加载中…

) } const { devices, soh, events, strategies, summary } = data const { totalDevices, avgSoh, avgSoh30d, avgSoh90d, warningCount, watchCount, healthyCount, batchPerformance, riskFactorCounts, firmwareHealth, updatedAt, executiveSummary, } = summary const chartData = buildChartData(soh) return (
{/* Background gradient */}
{/* Header */}
实时数据与健康预测

电池健康与风险洞察

基于当前可用数据生成

数据更新时间: {updatedAt}

设备电池实时状态
{/* Executive Summary */}

执行摘要

{executiveSummary}

{/* Primary KPI Row */}
{/* Hero KPI */}

当前平均健康度

{formatPercent(avgSoh)}

{avgSoh !== null && %}
{avgSoh === null ? '健康预测暂不可用' : '预测已返回'} | 共 {totalDevices} 台设备
{/* Regular KPIs */}

30 天预测均值

{formatPercent(avgSoh30d)}

{avgSoh30d !== null && %}
{formatDelta(avgSoh, avgSoh30d)}

90 天预测均值

{formatPercent(avgSoh90d)}

{avgSoh90d !== null && %}
{formatDelta(avgSoh, avgSoh90d)}

高风险设备

{warningCount}

占比 {totalDevices > 0 ? ((warningCount / totalDevices) * 100).toFixed(1) : 0}%
{/* Divider */}
{/* Health trend chart */}
} title="健康趋势预测" description="展示当前健康度与未来 30/90 天趋势;数据不足时保持空态,避免误导判断。" />
{soh.history.length > 0 && ( 历史观测值 )} 预测趋势
{chartData.length > 0 ? ( Math.floor(min) - 2, (max: number) => Math.ceil(max) + 2]} tick={{ fill: '#71717A', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={(v: number) => `${v}%`} width={48} /> ) : (
暂无可用的健康趋势数据。
)}
{/* Two-column grid */}
{/* Left Column */}
{/* Risk Distribution */}
} title="健康分布" />
健康 (> 90%) {healthyCount} 台{' '} / {totalDevices > 0 ? ((healthyCount / totalDevices) * 100).toFixed(1) : 0}%
0 ? (healthyCount / totalDevices) * 100 : 0}%` }} />
关注 (85% - 90%) {watchCount} 台{' '} / {totalDevices > 0 ? ((watchCount / totalDevices) * 100).toFixed(1) : 0}%
0 ? (watchCount / totalDevices) * 100 : 0}%` }} />
预警 (≤ 85%) {warningCount} 台{' '} / {totalDevices > 0 ? ((warningCount / totalDevices) * 100).toFixed(1) : 0}%
0 ? (warningCount / totalDevices) * 100 : 0}%` }} />
{/* Regional Performance */}

设备型号健康度概览

{batchPerformance.length > 0 ? ( batchPerformance.map((item) => (
{item.batch}
{formatPercentWithUnit(item.avgSoh)}
)) ) : (
暂无数据
)}
{/* Right Column */}
{/* Event Timeline */}
} title="风险与趋势概览" />
{events.length > 0 ? ( events.map((event) => (
{event.time} {event.severity}风险

{event.title}

{event.detail}

)) ) : (
暂无数据
)}
{/* Risk Factor Frequency */}
} title="主要风险因子分布" />
{riskFactorCounts.length > 0 ? ( riskFactorCounts.map((item) => ( {item.factor} {item.count} )) ) : (
暂无数据
)}
{/* Divider */}
{/* Device Table */}

重点关注设备

按风险优先级展示当前健康度与未来 30/90 天趋势。

{devices.length > 0 ? ( devices .slice() .sort((a, b) => b.riskScore - a.riskScore) .map((unit) => ( )) ) : ( )}
设备标识 设备型号 当前健康度 30天预测 90天预测 风险评分 状态 主要风险因子
{unit.id} {unit.batch} {formatPercentWithUnit(unit.soh)} {formatPercentWithUnit(unit.soh30d)} {formatPercentWithUnit(unit.soh90d)}
= 75 ? 'bg-red-400' : unit.riskScore >= 45 ? 'bg-amber-400' : 'bg-emerald-400' }`} style={{ width: `${unit.riskScore}%` }} />
{unit.riskScore}
{unit.status}
{unit.riskFactors.length > 0 ? ( unit.riskFactors.map((factor) => ( {factor} )) ) : ( - )}
暂无设备数据
{/* Bottom Row */}
{/* Strategy Cards */}

行动建议

{strategies.length > 0 ? ( strategies.map((item, index) => (

{item.name}

{item.impact}

范围: {item.scope} 时效: {item.eta}
)) ) : (
暂无策略建议
)}
{/* Firmware Comparison */}

备注分组健康度对比

{firmwareHealth.length > 0 ? ( firmwareHealth.map((item) => (
{item.firmware}
{item.count} 台设备
{formatPercentWithUnit(item.avgSoh)}
平均健康度
)) ) : (
暂无数据
)}
) }