diff --git a/bun.lock b/bun.lock index fc62c24..d5f4216 100644 --- a/bun.lock +++ b/bun.lock @@ -23,6 +23,7 @@ "citty": "^0.2.2", "drizzle-orm": "^0.45.2", "lru-cache": "^11.3.6", + "lucide-react": "^1.14.0", "mysql2": "^3.22.3", "react": "^19.2.5", "react-dom": "^19.2.5", @@ -619,6 +620,8 @@ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], + "lucide-react": ["lucide-react@1.14.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], diff --git a/package.json b/package.json index b6d79da..dc62cd1 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "citty": "^0.2.2", "drizzle-orm": "^0.45.2", "lru-cache": "^11.3.6", + "lucide-react": "^1.14.0", "mysql2": "^3.22.3", "react": "^19.2.5", "react-dom": "^19.2.5", diff --git a/src/components/ui.tsx b/src/components/ui.tsx new file mode 100644 index 0000000..9a35629 --- /dev/null +++ b/src/components/ui.tsx @@ -0,0 +1,99 @@ +import type { ComponentPropsWithoutRef, ReactNode } from 'react' + +type Variant = 'default' | 'muted' | 'success' | 'warning' | 'danger' | 'info' + +function cn(...classes: Array) { + return classes.filter(Boolean).join(' ') +} + +const variantClass: Record = { + default: 'border-white/10 bg-white/[0.04] text-zinc-100', + muted: 'border-white/10 bg-zinc-900/70 text-zinc-400', + success: 'border-emerald-400/20 bg-emerald-400/10 text-emerald-300', + warning: 'border-amber-400/20 bg-amber-400/10 text-amber-300', + danger: 'border-red-400/20 bg-red-400/10 text-red-300', + info: 'border-teal-400/20 bg-teal-400/10 text-teal-300', +} + +export function Badge({ + className, + variant = 'default', + children, + ...props +}: ComponentPropsWithoutRef<'span'> & { variant?: Variant }) { + return ( + + {children} + + ) +} + +export function Card({ className, children, ...props }: ComponentPropsWithoutRef<'div'>) { + return ( +
+ {children} +
+ ) +} + +export function Button({ className, children, ...props }: ComponentPropsWithoutRef<'button'>) { + return ( + + ) +} + +export function Input({ className, ...props }: ComponentPropsWithoutRef<'input'>) { + return ( + + ) +} + +export function Select({ className, children, ...props }: ComponentPropsWithoutRef<'select'>) { + return ( + + ) +} + +export function SectionTitle({ icon, title, description }: { icon?: ReactNode; title: string; description?: string }) { + return ( +
+ {icon &&
{icon}
} +
+

{title}

+ {description &&

{description}

} +
+
+ ) +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d8165da..73b344d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { createFileRoute, Link } from '@tanstack/react-router' +import { AlertTriangle, ArrowRight, Database, ShieldCheck, Tags, TrendingDown } from 'lucide-react' import { Area, CartesianGrid, @@ -13,6 +14,7 @@ import { } 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' @@ -43,7 +45,9 @@ function buildChartData(soh: DashboardSnapshot['soh']) { } } - for (let i = 1; i < soh.forecast.length; i++) { + 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({ @@ -57,16 +61,16 @@ function buildChartData(soh: DashboardSnapshot['soh']) { return chartData } -const statusColorMap: Record = { - [DEVICE_STATUS.HEALTHY]: 'text-emerald-400', - [DEVICE_STATUS.WATCH]: 'text-amber-400', - [DEVICE_STATUS.WARNING]: 'text-red-400', +const statusVariantMap: Record = { + [DEVICE_STATUS.HEALTHY]: 'success', + [DEVICE_STATUS.WATCH]: 'warning', + [DEVICE_STATUS.WARNING]: 'danger', } -const severityColorMap: Record = { - 高: 'text-red-400', - 中: 'text-amber-400', - 低: 'text-zinc-400', +const severityVariantMap: Record = { + 高: 'danger', + 中: 'warning', + 低: 'muted', } function formatChartTooltip(value: ValueType | undefined, name: NameType | undefined) { @@ -145,30 +149,20 @@ function Dashboard() { {/* Header */}
-
- 智能戒指电池健康预测模型 (v2.4) -
+ + MySQL 实时记录 + AI 预测 API +

SoH 预测与风险洞察

-
-
- 模型回测准确率 (MAE) - 1.2% -
-
-
- 预测命中率 - 94.5% -
-
+ 仅展示真实记录与预测接口返回值

数据更新时间: {updatedAt}

- 设备电池实时状态 → + 设备电池实时状态
@@ -192,7 +186,7 @@ function Dashboard() {
- {avgSoh === null ? 'AI预测不可用' : '基线健康'} + {avgSoh === null ? 'AI预测不可用' : '预测已返回'} | 共 {totalDevices} 台设备 @@ -244,20 +238,23 @@ function Dashboard() { {/* SoH Trend Chart */}
-
+
-
-

SoH 衰减趋势与 90 天预测

-

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

-
+ } + title="SoH 预测点位" + description="图表只展示 AI 预测 API 返回的当前、30 天、90 天聚合点;没有真实 SoH 历史时不补假趋势。" + />
- - - 历史观测值 - + {soh.history.length > 0 && ( + + + 历史观测值 + + )} - 模型预测值 + API 预测值
@@ -361,7 +358,7 @@ function Dashboard() {
)} - + {/* Two-column grid */} @@ -370,7 +367,9 @@ function Dashboard() {
{/* Risk Distribution */}
-

风险分层与结构

+
+ } title="风险分层与结构" /> +
@@ -428,7 +427,7 @@ function Dashboard() { {/* Regional Performance */}
-

批次健康度概览

+

设备型号健康度概览

{batchPerformance.length > 0 ? ( batchPerformance.map((item) => ( @@ -458,7 +457,9 @@ function Dashboard() {
{/* Event Timeline */}
-

异常特征时间轴

+
+ } title="风险与预测快照" /> +
{events.length > 0 ? ( events.map((event) => ( @@ -470,7 +471,7 @@ function Dashboard() { />
{event.time} - {event.severity}风险 + {event.severity}风险

{event.title}

{event.detail}

@@ -484,19 +485,18 @@ function Dashboard() { {/* Risk Factor Frequency */}
-

主要风险因子分布

+
+ } title="主要风险因子分布" /> +
{riskFactorCounts.length > 0 ? ( riskFactorCounts.map((item) => ( -
- {item.factor} - + + {item.factor} + {item.count} -
+ )) ) : (
暂无数据
@@ -514,81 +514,88 @@ function Dashboard() {

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

-

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

+

+ 按综合风险评分排序,展示 API 返回的当前、30 天与 90 天 SoH 预测 +

-
- - - - - - - - - - - - - - - {devices.length > 0 ? ( - devices - .slice() - .sort((a, b) => b.riskScore - a.riskScore) - .map((unit) => ( - - - - - - - - - - - )) - ) : ( - - + +
+
设备标识生产批次当前 SoH30天预测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} - {factor !== unit.riskFactors[unit.riskFactors.length - 1] && '、'} - - )) - ) : ( - - - )} -
-
- 暂无设备数据 -
+ + + + + + + + + + - )} - -
设备标识设备型号当前 SoH30天预测90天预测风险评分状态主要风险因子
-
+ + + {devices.length > 0 ? ( + devices + .slice() + .sort((a, b) => b.riskScore - a.riskScore) + .map((unit) => ( + + {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 */} @@ -622,7 +629,7 @@ function Dashboard() { {/* Firmware Comparison */}
-

固件版本健康度对比

+

备注分组健康度对比

{firmwareHealth.length > 0 ? ( diff --git a/src/styles.css b/src/styles.css index f1d8c73..0e1eeec 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,24 @@ @import "tailwindcss"; + +:root { + color-scheme: dark; + background: #09090b; +} + +html { + background: #09090b; +} + +body { + min-width: 320px; + margin: 0; + background: + radial-gradient(circle at top, rgba(20, 184, 166, 0.08), transparent 34rem), + linear-gradient(180deg, #09090b 0%, #0b0f12 100%); + color: #f4f4f5; +} + +select option { + background: #09090b; + color: #f4f4f5; +}