fix(ui): 修复充电状态筛选重置

This commit is contained in:
2026-05-12 00:18:00 +08:00
parent 4571cee2a1
commit 50e8e32bac
3 changed files with 46 additions and 46 deletions
+10 -10
View File
@@ -251,10 +251,10 @@ function toFleetUnit(item: BatteryInfo, prediction?: BatteryPrediction): FleetUn
if (item.isLowPower || item.power <= SOH_THRESHOLDS.LOW_POWER) riskFactors.push('低电量')
if (item.powerStatus === POWER_STATUS.CHARGING) riskFactors.push('充电中')
if (!hasPrediction) riskFactors.push('SoH预测不可用')
if (!hasPrediction) riskFactors.push('健康预测不可用')
if (prediction && status === DEVICE_STATUS.WARNING) riskFactors.push('衰减加速')
if (item.remark?.includes('v3.7')) riskFactors.push('旧固件')
if (prediction?.riskLevel) riskFactors.push(`AI风险:${prediction.riskLevel}`)
if (prediction?.riskLevel) riskFactors.push(`预测风险:${prediction.riskLevel}`)
const soh30d = prediction ? round1(clamp(prediction.monthSoh, 0, 100)) : null
const soh90d = prediction ? round1(clamp(prediction.trmonthSoh, 0, 100)) : null
@@ -392,8 +392,8 @@ function createSummary(devices: FleetUnit[], now: Date) {
updatedAt: formatDateTime(now),
executiveSummary:
avgSoh === null
? '当前 AI SoH 预测不可用,页面仅展示 MySQL 采集电量、充电状态与低电量风险。请检查预测服务配置或历史数据量。'
: `当前共有 ${predictedDevices} 台设备返回 SoH 预测,${missingPredictionDevices} 台设备暂无预测重点关注 ${weakestModel} 型号与 ${weakestRemark} 备注设备,优先处理低电量和充电中的设备,并在下次同步后复查缺失预测与未来 30/90 天模型预测`,
? '当前健康预测不可用,系统仍会展示设备电量、充电状态与低电量风险。请稍后复查或联系管理员。'
: `当前共有 ${predictedDevices} 台设备具备健康预测,${missingPredictionDevices} 台设备暂无预测结果。建议重点关注 ${weakestModel} 型号与 ${weakestRemark} 备注设备,优先处理低电量和充电中的设备,并在下次同步后复查未来 30/90 天健康趋势`,
}
}
@@ -408,7 +408,7 @@ function createEvents(devices: FleetUnit[], now: Date) {
{
time: formatDateTime(now),
title: '风险快照',
detail: `本次快照包含 ${devices.length} 台设备,其中 ${predictedDevices.length}返回真实 SoH 预测,${warningDevices.length} 台处于预警状态。`,
detail: `本次概览包含 ${devices.length} 台设备,其中 ${predictedDevices.length}具备健康预测,${warningDevices.length} 台处于预警状态。`,
severity: warningDevices.length > 0 ? EVENT_SEVERITY.HIGH : EVENT_SEVERITY.LOW,
},
{
@@ -416,8 +416,8 @@ function createEvents(devices: FleetUnit[], now: Date) {
title: '预测可用性快照',
detail:
missingPredictionDevices > 0
? `当前有 ${missingPredictionDevices} 台设备暂无 SoH 预测,对应图表与卡片保留为空值`
: '当前所有设备均已返回 SoH 预测,图表仅展示真实预测点。',
? `当前有 ${missingPredictionDevices} 台设备暂无健康预测,相关趋势将暂不展示`
: '当前所有设备均已具备健康预测,可继续观察趋势变化。',
severity: missingPredictionDevices > 0 ? EVENT_SEVERITY.MEDIUM : EVENT_SEVERITY.LOW,
},
] satisfies DashboardSnapshot['events']
@@ -440,11 +440,11 @@ function createStrategies(devices: FleetUnit[]) {
eta: '本次巡检周期内',
},
{
name: '补齐预测覆盖',
name: '提升预测覆盖',
impact:
missingPredictionDevices.length > 0
? `当前有 ${missingPredictionDevices.length} 台设备暂无 SoH 预测,建议在下次同步后复查。`
: `当前已有 ${devices.length} 台设备返回预测结果,可继续观察真实变化。`,
? `当前有 ${missingPredictionDevices.length} 台设备暂无健康预测,建议在下次同步后复查。`
: `当前已有 ${devices.length} 台设备具备预测结果,可继续观察健康变化。`,
scope:
powerAttentionDevices.length > 0
? `${powerAttentionDevices.length} 台充电中或低电量设备`
+14 -12
View File
@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query'
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import { ArrowLeft, Battery, BatteryCharging, BatteryLow, Database, FilterX, Search, Zap } from 'lucide-react'
import { ArrowLeft, Battery, BatteryCharging, BatteryLow, FilterX, Search, Zap } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { z } from 'zod'
import { orpc } from '@/client/orpc'
@@ -77,6 +77,8 @@ function parseSort(value: string): BatteryListSort {
}
function parsePowerStatus(value: string): PowerStatus | undefined {
if (value === '') return undefined
const parsed = Number(value)
return POWER_STATUS_VALUES.find((option) => option === parsed)
@@ -242,11 +244,11 @@ function BatteriesPage() {
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<Badge variant="info" className="mb-4">
<Database className="size-3.5" /> MySQL
<Battery className="size-3.5" />
</Badge>
<h1 className="text-3xl font-light tracking-tight text-white"></h1>
<h1 className="text-3xl font-light tracking-tight text-white"></h1>
<p className="mt-2 max-w-2xl text-sm leading-6 text-zinc-400">
ls_battery_info ORPC
</p>
<p className="mt-2 text-xs tabular-nums text-zinc-500">
{data ? `更新于 ${new Date(data.updatedAt).toLocaleString('zh-CN')}` : '加载中…'}
@@ -257,7 +259,7 @@ function BatteriesPage() {
to="/"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-sm text-zinc-300 transition-colors hover:border-teal-400/30 hover:text-teal-300"
>
<ArrowLeft className="size-4" /> SoH
<ArrowLeft className="size-4" />
</Link>
</nav>
</div>
@@ -290,7 +292,7 @@ function BatteriesPage() {
<SectionTitle
icon={<Search className="size-4" />}
title="筛选设备"
description="按真实采集字段筛选,不在前端伪造或补齐记录。"
description="按设备名称、编号、电量与充电状态快速缩小排查范围。"
/>
{hasActiveFilters && (
<Button type="button" className="h-9 px-3 text-xs" onClick={clearFilters}>
@@ -301,14 +303,14 @@ function BatteriesPage() {
<div className="flex flex-wrap items-end gap-4">
<div className="flex flex-col gap-2 min-w-[260px] flex-1">
<label htmlFor="search-input" className="text-xs font-medium text-zinc-500">
/ MAC
/
</label>
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-zinc-600" />
<Input
id="search-input"
type="text"
placeholder="搜索设备名称或 MAC..."
placeholder="搜索设备名称或编号..."
maxLength={100}
className="pl-9"
value={localSearch}
@@ -369,7 +371,7 @@ function BatteriesPage() {
<div className="flex flex-col gap-2">
<label htmlFor="page-size-select" className="text-xs font-medium text-zinc-500">
</label>
<Select
id="page-size-select"
@@ -411,7 +413,7 @@ function BatteriesPage() {
})
}
/>
<Zap className="size-4 text-amber-300" />
<Zap className="size-4 text-amber-300" />
</label>
</div>
</Card>
@@ -440,7 +442,7 @@ function BatteriesPage() {
) : data?.items.length === 0 ? (
<tr>
<td colSpan={columns.length} className="h-32 text-center text-zinc-500">
</td>
</tr>
) : (
@@ -461,7 +463,7 @@ function BatteriesPage() {
<div className="mt-6 flex items-center justify-between text-sm text-zinc-500">
<div>
{data?.items.length ?? 0}
{data?.items.length ?? 0}
{data?.total ? ` (共 ${data.total} 台)` : ''}
</div>
<div className="flex items-center gap-2">
+22 -24
View File
@@ -1,6 +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 { Activity, AlertTriangle, ArrowRight, ShieldCheck, Tags, TrendingDown } from 'lucide-react'
import {
Area,
CartesianGrid,
@@ -78,7 +78,7 @@ function formatChartTooltip(value: ValueType | undefined, name: NameType | undef
return [
`${Number.isFinite(numericValue) ? numericValue.toFixed(1) : (value ?? '-')}%`,
name === 'history' ? '历史观测' : '模型预测',
name === 'history' ? '历史观测' : '趋势预测',
]
}
@@ -150,12 +150,12 @@ function Dashboard() {
<header className="animate-fade-up mb-12 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl">
<Badge variant="info" className="mb-4">
<Database className="size-3.5" /> MySQL + AI API
<Activity className="size-3.5" />
</Badge>
<h1 className="text-4xl font-light tracking-tight text-white sm:text-5xl">SoH </h1>
<h1 className="text-4xl font-light tracking-tight text-white sm:text-5xl"></h1>
</div>
<div className="flex flex-col items-end gap-3 text-right">
<Badge variant="muted"></Badge>
<Badge variant="muted"></Badge>
<p className="text-xs tabular-nums text-[#71717A]">: {updatedAt}</p>
<Link
to="/batteries"
@@ -178,7 +178,7 @@ function Dashboard() {
{/* Hero KPI */}
<article className="relative overflow-hidden rounded-2xl border border-white/10 bg-white/[0.03] p-8 lg:col-span-2">
<div className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-teal-500/50 to-transparent" />
<p className="text-sm font-medium text-[#A1A1AA]"> SoH</p>
<p className="text-sm font-medium text-[#A1A1AA]"></p>
<div className="mt-4 flex items-baseline gap-2">
<h2 className="text-6xl font-light tabular-nums text-white">{formatPercent(avgSoh)}</h2>
{avgSoh !== null && <span className="text-2xl text-[#71717A]">%</span>}
@@ -186,7 +186,7 @@ function Dashboard() {
<div className="mt-6 flex items-center gap-3 text-sm">
<span className="inline-flex items-center gap-1.5 text-emerald-400">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
{avgSoh === null ? 'AI预测不可用' : '预测已返回'}
{avgSoh === null ? '健康预测不可用' : '预测已返回'}
</span>
<span className="text-[#71717A]">|</span>
<span className="text-[#A1A1AA]"> {totalDevices} </span>
@@ -236,14 +236,14 @@ function Dashboard() {
{/* Divider */}
<hr className="my-12 border-white/5" />
{/* SoH Trend Chart */}
{/* Health trend chart */}
<section className="animate-fade-up delay-300 mb-12">
<Card className="p-8">
<header className="mb-8 flex flex-wrap items-end justify-between gap-4">
<SectionTitle
icon={<TrendingDown className="size-4" />}
title="SoH 预测点位"
description="图表只展示 AI 预测 API 返回的当前、30 天、90 天聚合点;没有真实 SoH 历史时不补假趋势。"
title="健康趋势预测"
description="展示当前健康度与未来 30/90 天趋势;数据不足时保持空态,避免误导判断。"
/>
<div className="flex items-center gap-6 text-sm text-[#A1A1AA]">
{soh.history.length > 0 && (
@@ -254,7 +254,7 @@ function Dashboard() {
)}
<span className="inline-flex items-center gap-2">
<span className="h-2 w-4 rounded-full bg-indigo-400" />
API
</span>
</div>
</header>
@@ -354,7 +354,7 @@ function Dashboard() {
</ResponsiveContainer>
) : (
<div className="flex h-full items-center justify-center rounded-xl border border-dashed border-white/10 bg-white/[0.02] text-sm text-[#71717A]">
AI SoH
</div>
)}
</div>
@@ -368,12 +368,12 @@ function Dashboard() {
{/* Risk Distribution */}
<div>
<div className="mb-6">
<SectionTitle icon={<ShieldCheck className="size-4" />} title="风险分层与结构" />
<SectionTitle icon={<ShieldCheck className="size-4" />} title="健康分布" />
</div>
<div className="space-y-5">
<div>
<div className="mb-2 flex justify-between text-sm">
<span className="text-[#A1A1AA]"> (SoH &gt; 90%)</span>
<span className="text-[#A1A1AA]"> (&gt; 90%)</span>
<span className="tabular-nums text-white">
{healthyCount} {' '}
<span className="text-[#71717A]">
@@ -390,7 +390,7 @@ function Dashboard() {
</div>
<div>
<div className="mb-2 flex justify-between text-sm">
<span className="text-[#A1A1AA]"> (85% &lt; SoH &le; 90%)</span>
<span className="text-[#A1A1AA]"> (85% - 90%)</span>
<span className="tabular-nums text-white">
{watchCount} {' '}
<span className="text-[#71717A]">
@@ -407,7 +407,7 @@ function Dashboard() {
</div>
<div>
<div className="mb-2 flex justify-between text-sm">
<span className="text-[#A1A1AA]"> (SoH &le; 85%)</span>
<span className="text-[#A1A1AA]"> (&le; 85%)</span>
<span className="tabular-nums text-white">
{warningCount} {' '}
<span className="text-[#71717A]">
@@ -458,7 +458,7 @@ function Dashboard() {
{/* Event Timeline */}
<div>
<div className="mb-6">
<SectionTitle icon={<AlertTriangle className="size-4" />} title="风险与预测快照" />
<SectionTitle icon={<AlertTriangle className="size-4" />} title="风险与趋势概览" />
</div>
<div className="relative border-l border-white/10 pl-5">
{events.length > 0 ? (
@@ -513,10 +513,8 @@ function Dashboard() {
<section className="animate-fade-up delay-500 mb-12">
<div className="mb-6 flex items-end justify-between">
<div>
<h3 className="text-xl font-medium text-white"></h3>
<p className="mt-1 text-sm text-[#A1A1AA]">
API 30 90 SoH
</p>
<h3 className="text-xl font-medium text-white"></h3>
<p className="mt-1 text-sm text-[#A1A1AA]"> 30/90 </p>
</div>
</div>
<Card className="overflow-hidden">
@@ -526,7 +524,7 @@ function Dashboard() {
<tr className="border-b border-white/10 bg-white/[0.02] text-zinc-400">
<th className="px-6 py-4 font-medium whitespace-nowrap"></th>
<th className="px-6 py-4 font-medium whitespace-nowrap"></th>
<th className="px-6 py-4 font-medium whitespace-nowrap"> SoH</th>
<th className="px-6 py-4 font-medium whitespace-nowrap"></th>
<th className="px-6 py-4 font-medium whitespace-nowrap">30</th>
<th className="px-6 py-4 font-medium whitespace-nowrap">90</th>
<th className="px-6 py-4 font-medium whitespace-nowrap"></th>
@@ -602,7 +600,7 @@ function Dashboard() {
<section className="animate-fade-up delay-500 grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* Strategy Cards */}
<div className="lg:col-span-2">
<h3 className="mb-6 text-lg font-medium text-white"></h3>
<h3 className="mb-6 text-lg font-medium text-white"></h3>
<div className="grid gap-4 sm:grid-cols-2">
{strategies.length > 0 ? (
strategies.map((item, index) => (
@@ -641,7 +639,7 @@ function Dashboard() {
</div>
<div className="text-right">
<div className="text-sm tabular-nums text-white">{formatPercentWithUnit(item.avgSoh)}</div>
<div className="text-xs text-[#71717A]"> SoH</div>
<div className="text-xs text-[#71717A]"></div>
</div>
</div>
))