fix(ui): 修复充电状态筛选重置
This commit is contained in:
+10
-10
@@ -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
@@ -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
@@ -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 > 90%)</span>
|
||||
<span className="text-[#A1A1AA]">健康 (> 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% < SoH ≤ 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 ≤ 85%)</span>
|
||||
<span className="text-[#A1A1AA]">预警 (≤ 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>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user