From df7b58c2f8f8c1750515b7c796df143bf71cedc3 Mon Sep 17 00:00:00 2001 From: imbytecat Date: Mon, 11 May 2026 20:51:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E9=87=8D=E5=BB=BA=20SoH=20?= =?UTF-8?q?=E7=9C=8B=E6=9D=BF=E9=A6=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/index.tsx | 687 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 616 insertions(+), 71 deletions(-) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index bc7fc53..4236d35 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,87 +1,632 @@ -import { useMutation, useSuspenseQuery } from '@tanstack/react-query' -import { createFileRoute } from '@tanstack/react-router' +import { useSuspenseQuery } from '@tanstack/react-query' +import { createFileRoute, Link } from '@tanstack/react-router' import { orpc } from '@/client/orpc' -import { useInvalidateTodos } from '@/client/queries/todo' -import { TodoForm } from '@/components/TodoForm' -import { TodoItem } from '@/components/TodoItem' +import type { DashboardSnapshot, DeviceStatus } from '@/domain/battery' export const Route = createFileRoute('/')({ - component: Todos, + component: Dashboard, loader: async ({ context }) => { - await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions()) + await context.queryClient.ensureQueryData(orpc.battery.dashboard.queryOptions()) }, + errorComponent: ({ error }) => ( +
+
+

数据加载失败

+

{error.message}

+
+
+ ), }) -function Todos() { - const listQuery = useSuspenseQuery(orpc.todo.list.queryOptions()) - const invalidateTodos = useInvalidateTodos() +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, + })) - const createMutation = useMutation(orpc.todo.create.mutationOptions({ onSuccess: invalidateTodos })) - const updateMutation = useMutation(orpc.todo.update.mutationOptions({ onSuccess: invalidateTodos })) - const deleteMutation = useMutation(orpc.todo.remove.mutationOptions({ onSuccess: invalidateTodos })) + 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 todos = listQuery.data - const completedCount = todos.filter((todo) => todo.completed).length - const totalCount = todos.length - const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0 + for (let i = 1; 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 statusColorMap: Record = { + 健康: 'text-emerald-400', + 关注: 'text-amber-400', + 预警: 'text-red-400', +} + +const severityColorMap: Record = { + 高: 'text-red-400', + 中: 'text-amber-400', + 低: 'text-zinc-400', +} + +function SimpleChart({ data }: { data: ReturnType }) { + if (data.length === 0) { + return
暂无数据
+ } + + const minVal = Math.floor(Math.min(...data.map((d) => Math.min(d.history ?? 100, d.forecast ?? 100)))) - 2 + const maxVal = Math.ceil(Math.max(...data.map((d) => Math.max(d.history ?? 0, d.forecast ?? 0)))) + 2 + const range = maxVal - minVal + + const width = 1000 + const height = 280 + const padding = { top: 20, right: 20, bottom: 30, left: 40 } + const innerWidth = width - padding.left - padding.right + const innerHeight = height - padding.top - padding.bottom + + const getX = (index: number) => padding.left + (index / (data.length - 1)) * innerWidth + const getY = (val: number) => padding.top + innerHeight - ((val - minVal) / range) * innerHeight + + const historyPoints = data + .map((d, i) => (d.history !== undefined ? `${getX(i)},${getY(d.history)}` : null)) + .filter(Boolean) + .join(' ') + + const forecastPoints = data + .map((d, i) => (d.forecast !== undefined ? `${getX(i)},${getY(d.forecast)}` : null)) + .filter(Boolean) + .join(' ') + + const lastHistoryIndex = data.findLastIndex((d) => d.history !== undefined) + const historyArea = historyPoints + ? `${historyPoints} ${getX(lastHistoryIndex)},${padding.top + innerHeight} ${getX(0)},${padding.top + innerHeight}` + : '' + + const forecastStartIndex = data.findIndex((d) => d.forecast !== undefined) + const forecastArea = forecastPoints + ? `${forecastPoints} ${getX(data.length - 1)},${padding.top + innerHeight} ${getX(forecastStartIndex)},${padding.top + innerHeight}` + : '' return ( -
-
-
-
-

我的待办

-

保持专注,逐个击破

-
-
-
- {completedCount} - /{totalCount} -
-
已完成
-
-
+ + SoH Trend Chart + + + + + + + + + + - createMutation.mutate({ title })} isPending={createMutation.isPending} /> + {/* Grid */} + {[0, 0.25, 0.5, 0.75, 1].map((ratio) => { + const y = padding.top + innerHeight * ratio + const val = maxVal - range * ratio + return ( + + + + {val.toFixed(0)}% + + + ) + })} - {totalCount > 0 && ( -
-
-
- )} + {/* 85% Reference Line */} + {85 >= minVal && 85 <= maxVal && ( + + + + 85% 预警线 + + + )} -
- {todos.length === 0 ? ( -
-
- -
-

没有待办事项

-

输入上方内容添加您的第一个任务

-
- ) : ( - todos.map((todo) => ( - updateMutation.mutate({ id, data: { completed: !completed } })} - onDelete={(id) => deleteMutation.mutate({ id })} - /> - )) - )} -
-
-
+ {/* X Axis */} + {data.map((d, i) => ( + + {d.month} + + ))} + + {/* Areas */} + {historyArea && } + {forecastArea && } + + {/* Lines */} + {historyPoints && } + {forecastPoints && ( + + )} + + {/* Dots */} + {data.map((d, i) => { + const dots = [] + if (d.history !== undefined) { + dots.push( + , + ) + } + if (d.forecast !== undefined) { + dots.push( + , + ) + } + return dots + })} + + ) +} + +function Dashboard() { + const { data } = useSuspenseQuery(orpc.battery.dashboard.queryOptions()) + + 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 */} +
+
+
+ 智能戒指电池健康预测模型 (v2.4) +
+

SoH 预测与风险洞察

+
+
+
+
+ 模型回测准确率 (MAE) + 1.2% +
+
+
+ 预测命中率 + 94.5% +
+
+

数据更新时间: {updatedAt}

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

执行摘要

+

{executiveSummary}

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

当前平均 SoH

+
+

{avgSoh.toFixed(1)}

+ % +
+
+ + + 基线健康 + + | + 共 {totalDevices} 台设备 +
+
+ + {/* Regular KPIs */} +
+

30 天预测均值

+
+

{avgSoh30d.toFixed(1)}

+ % +
+
+ + {(avgSoh - avgSoh30d).toFixed(1)}% 衰减 +
+
+ +
+

90 天预测均值

+
+

{avgSoh90d.toFixed(1)}

+ % +
+
+ + {(avgSoh - avgSoh90d).toFixed(1)}% 衰减 +
+
+ +
+

高风险设备

+
+

{warningCount}

+ +
+
+ + + 占比 {totalDevices > 0 ? ((warningCount / totalDevices) * 100).toFixed(1) : 0}% + +
+
+
+ + {/* Divider */} +
+ + {/* SoH Trend Chart */} +
+
+
+
+

SoH 衰减趋势与 90 天预测

+

基于历史 12 个月真实数据与未来 3 个月模型预测区间

+
+
+ + + 历史观测值 + + + + 模型预测值 + +
+
+ +
+ +
+
+
+ + {/* Two-column grid */} +
+ {/* Left Column */} +
+ {/* Risk Distribution */} +
+

风险分层与结构

+
+
+
+ 健康 (SoH > 90%) + + {healthyCount} 台{' '} + + / {totalDevices > 0 ? ((healthyCount / totalDevices) * 100).toFixed(1) : 0}% + + +
+
+
0 ? (healthyCount / totalDevices) * 100 : 0}%` }} + /> +
+
+
+
+ 关注 (85% < SoH ≤ 90%) + + {watchCount} 台{' '} + + / {totalDevices > 0 ? ((watchCount / totalDevices) * 100).toFixed(1) : 0}% + + +
+
+
0 ? (watchCount / totalDevices) * 100 : 0}%` }} + /> +
+
+
+
+ 预警 (SoH ≤ 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} +
+
+
+
+
+ {item.avgSoh.toFixed(1)}% +
+ )) + ) : ( +
暂无数据
+ )} +
+
+
+ + {/* Right Column */} +
+ {/* Event Timeline */} +
+

异常特征时间轴

+
+ {events.length > 0 ? ( + events.map((event) => ( +
+
+
+ {event.time} + {event.severity}风险 +
+

{event.title}

+

{event.detail}

+
+ )) + ) : ( +
暂无数据
+ )} +
+
+ + {/* Risk Factor Frequency */} +
+

主要风险因子分布

+
+ {riskFactorCounts.length > 0 ? ( + riskFactorCounts.map((item) => ( +
+ {item.factor} + + {item.count} + +
+ )) + ) : ( +
暂无数据
+ )} +
+
+
+
+ + {/* Divider */} +
+ + {/* Device Table */} +
+
+
+

高风险设备清单与预测明细

+

按综合风险评分排序,展示未来 30/60/90 天衰减预测

+
+
+
+ + + + + + + + + + + + + + + {devices.length > 0 ? ( + devices + .slice() + .sort((a, b) => b.riskScore - a.riskScore) + .map((unit) => ( + + + + + + + + + + + )) + ) : ( + + + + )} + +
设备标识生产批次当前 SoH30天预测90天预测风险评分状态主要风险因子
{unit.id}{unit.batch}{unit.soh.toFixed(1)}%{unit.soh30d.toFixed(1)}%{unit.soh90d.toFixed(1)}% +
+
+
= 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} + {factor !== unit.riskFactors[unit.riskFactors.length - 1] && '、'} + + )) + ) : ( + - + )} +
+
+ 暂无设备数据 +
+
+
+ + {/* 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} 台设备
+
+
+
{item.avgSoh.toFixed(1)}%
+
平均 SoH
+
+
+ )) + ) : ( +
暂无数据
+ )} +
+
+
+
+
+
) }