fix(dashboard): 使用稳定设备标识
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
|||||||
createBatteriesResponse,
|
createBatteriesResponse,
|
||||||
createDashboardSnapshot,
|
createDashboardSnapshot,
|
||||||
DEVICE_STATUS,
|
DEVICE_STATUS,
|
||||||
|
fromMysqlBoolean,
|
||||||
getDeviceStatus,
|
getDeviceStatus,
|
||||||
MYSQL_BOOLEAN,
|
MYSQL_BOOLEAN,
|
||||||
POWER_STATUS,
|
POWER_STATUS,
|
||||||
@@ -27,7 +28,7 @@ const rows = [
|
|||||||
userId: 7,
|
userId: 7,
|
||||||
mac: 'RING-B11',
|
mac: 'RING-B11',
|
||||||
devModel: '2402-B',
|
devModel: '2402-B',
|
||||||
devName: 'RING-B11',
|
devName: '',
|
||||||
isLowPower: MYSQL_BOOLEAN.TRUE,
|
isLowPower: MYSQL_BOOLEAN.TRUE,
|
||||||
powerStatus: POWER_STATUS.CHARGING,
|
powerStatus: POWER_STATUS.CHARGING,
|
||||||
power: 84,
|
power: 84,
|
||||||
@@ -43,6 +44,11 @@ describe('battery domain', () => {
|
|||||||
expect(getDeviceStatus(85)).toBe(DEVICE_STATUS.WARNING)
|
expect(getDeviceStatus(85)).toBe(DEVICE_STATUS.WARNING)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('trims MySQL boolean strings before normalization', () => {
|
||||||
|
expect(fromMysqlBoolean(' true ')).toBe(true)
|
||||||
|
expect(fromMysqlBoolean(' false ')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
test('builds batteries response counters from records', () => {
|
test('builds batteries response counters from records', () => {
|
||||||
const now = new Date('2026-05-11T00:00:00.000Z')
|
const now = new Date('2026-05-11T00:00:00.000Z')
|
||||||
const items = rows.map(toBatteryInfo)
|
const items = rows.map(toBatteryInfo)
|
||||||
@@ -75,6 +81,10 @@ describe('battery domain', () => {
|
|||||||
const snapshot = createDashboardSnapshot(rows.map(toBatteryInfo), now)
|
const snapshot = createDashboardSnapshot(rows.map(toBatteryInfo), now)
|
||||||
|
|
||||||
expect(snapshot.devices).toHaveLength(2)
|
expect(snapshot.devices).toHaveLength(2)
|
||||||
|
expect(snapshot.devices[0]?.id).toBe('RING-A03')
|
||||||
|
expect(snapshot.devices[0]?.displayName).toBe('RING-A03')
|
||||||
|
expect(snapshot.devices[1]?.id).toBe('RING-B11')
|
||||||
|
expect(snapshot.devices[1]?.displayName).toBe('RING-B11')
|
||||||
expect(snapshot.devices.every((device) => device.sohSource === 'unavailable')).toBe(true)
|
expect(snapshot.devices.every((device) => device.sohSource === 'unavailable')).toBe(true)
|
||||||
expect(snapshot.devices.every((device) => device.soh === null)).toBe(true)
|
expect(snapshot.devices.every((device) => device.soh === null)).toBe(true)
|
||||||
expect(snapshot.devices.every((device) => device.soh30d === null)).toBe(true)
|
expect(snapshot.devices.every((device) => device.soh30d === null)).toBe(true)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export type BatteryInfo = z.infer<typeof batteryInfoSchema>
|
|||||||
|
|
||||||
export const fleetUnitSchema = z.object({
|
export const fleetUnitSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
displayName: z.string(),
|
||||||
batch: z.string(),
|
batch: z.string(),
|
||||||
firmware: z.string(),
|
firmware: z.string(),
|
||||||
cycles: z.number().int(),
|
cycles: z.number().int(),
|
||||||
@@ -267,7 +268,8 @@ function toFleetUnit(item: BatteryInfo, prediction?: BatteryPrediction): FleetUn
|
|||||||
const riskScore = Math.round(clamp(prediction?.riskScore ?? fallbackRiskScore, 0, 100))
|
const riskScore = Math.round(clamp(prediction?.riskScore ?? fallbackRiskScore, 0, 100))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.devName || item.mac,
|
id: item.mac,
|
||||||
|
displayName: item.devName || item.mac,
|
||||||
batch: item.devModel,
|
batch: item.devModel,
|
||||||
firmware: item.remark ?? '未提供',
|
firmware: item.remark ?? '未提供',
|
||||||
cycles: prediction?.cyclesUsed ?? 0,
|
cycles: prediction?.cyclesUsed ?? 0,
|
||||||
|
|||||||
@@ -93,7 +93,11 @@ function formatPercentWithUnit(value: number | null) {
|
|||||||
|
|
||||||
function formatDelta(from: number | null, to: number | null) {
|
function formatDelta(from: number | null, to: number | null) {
|
||||||
if (from === null || to === null) return '预测不可用'
|
if (from === null || to === null) return '预测不可用'
|
||||||
return `${(from - to).toFixed(1)}% 衰减`
|
const delta = from - to
|
||||||
|
|
||||||
|
if (delta < 0) return `${Math.abs(delta).toFixed(1)}% 改善`
|
||||||
|
if (delta === 0) return '0.0% 持平'
|
||||||
|
return `${delta.toFixed(1)}% 衰减`
|
||||||
}
|
}
|
||||||
|
|
||||||
function widthPercent(value: number | null) {
|
function widthPercent(value: number | null) {
|
||||||
@@ -540,7 +544,7 @@ function Dashboard() {
|
|||||||
.sort((a, b) => b.riskScore - a.riskScore)
|
.sort((a, b) => b.riskScore - a.riskScore)
|
||||||
.map((unit) => (
|
.map((unit) => (
|
||||||
<tr key={unit.id} className="transition-colors hover:bg-white/[0.04]">
|
<tr key={unit.id} className="transition-colors hover:bg-white/[0.04]">
|
||||||
<td className="px-6 py-4 font-medium text-white">{unit.id}</td>
|
<td className="px-6 py-4 font-medium text-white">{unit.displayName}</td>
|
||||||
<td className="px-6 py-4 text-[#A1A1AA]">{unit.batch}</td>
|
<td className="px-6 py-4 text-[#A1A1AA]">{unit.batch}</td>
|
||||||
<td className="px-6 py-4 tabular-nums text-white">{formatPercentWithUnit(unit.soh)}</td>
|
<td className="px-6 py-4 tabular-nums text-white">{formatPercentWithUnit(unit.soh)}</td>
|
||||||
<td className="px-6 py-4 tabular-nums text-[#A1A1AA]">
|
<td className="px-6 py-4 tabular-nums text-[#A1A1AA]">
|
||||||
|
|||||||
Reference in New Issue
Block a user