SEC_Warehouse/src/pages/device/feature/detail.tsx

206 lines
8.0 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from 'react'
import { useRouter } from '@tarojs/taro'
import Taro, { useDidShow, useDidHide } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { Navbar } from '@/components/navbar'
import { ListRow, ListRowGroup } from '@/components/list-row'
import { VitalWave } from '@/components/vital-wave'
import { ROUTES } from '@/constants/routes'
import { navigateTo, navigateBack } from '@/utils/navigation'
import { showToast } from '@/utils/toast'
import { MOVE_STATE_MAP, BODY_STATUS_MAP, ONBED_STATUS_MAP } from '@/constants/enums'
import {
getHomeDeviceDetail, getVitalRecords, unbindHomeDevice, type HomeDevice, type VitalSigns
} from '@/api/homeDevice'
import type { DeviceModel } from '@/constants/enums'
import styles from './detail.module.scss'
// 轮询间隔:与 web 端 VitalSignsDashboard SWR refreshInterval 对齐
const POLL_INTERVAL = 3000
export default function Detail() {
const router = useRouter()
const params = router.params as { id?: string; name?: string; model?: DeviceModel; online?: string }
const installId = params.id ?? ''
const deviceName = params.name ? decodeURIComponent(params.name) : '设备详情'
const deviceModel = params.model ?? ''
// 列表页 EventChannel 传入的设备对象:首屏零延迟
const [device, setDevice] = useState<HomeDevice | null>(null)
const [online, setOnline] = useState(params.online === '1')
const [wavePoints, setWavePoints] = useState<number[]>([])
const [unbinding, setUnbinding] = useState(false)
const deviceNameRef = useRef<string | null>(null)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => {
const inst = Taro.getCurrentInstance()
const page = inst.page as unknown as { getOpenerEventChannel?: () => { on: (e: string, cb: (d: HomeDevice) => void) => void } } | undefined
page?.getOpenerEventChannel?.()?.on('device', (d) => {
setDevice(d)
setOnline(d.onlineStatus === 1)
deviceNameRef.current = d.deviceName
})
}, [])
// 单次刷新:设备详情(实时快照)+ 历史心率(波形)
const fetchAll = useCallback(async () => {
if (!installId) return
try {
const d = await getHomeDeviceDetail(installId)
setDevice(d)
setOnline(d.onlineStatus === 1)
deviceNameRef.current = d.deviceName
const records = await getVitalRecords(d.deviceName)
// 倒序返回(最新在前),波形需按时间正序绘制
const pts = (records ?? []).map((r) => r.heartRate).filter((v): v is number => typeof v === 'number')
setWavePoints(pts.reverse())
} catch {
// 单次轮询失败不中断,下个周期重试
}
}, [installId])
// 首拉 + useDidShow/useDidHide 控制轮询启停:页面压栈后台不空跑
useEffect(() => {
fetchAll()
}, [fetchAll])
useDidShow(() => {
if (timerRef.current) clearInterval(timerRef.current)
timerRef.current = setInterval(() => { fetchAll() }, POLL_INTERVAL)
})
useDidHide(() => {
if (timerRef.current) {
clearInterval(timerRef.current)
timerRef.current = null
}
})
useEffect(() => {
return () => {
if (timerRef.current) clearInterval(timerRef.current)
}
}, [])
const signs: VitalSigns | undefined = device?.runTimeInfo?.properties
const fmt = (v: number | undefined): string => (v === undefined || v === null ? '--' : String(v))
const goBleConfig = () => navigateTo(ROUTES.DEVICE_BLE_CONFIG)
void goBleConfig
// 解绑确认:防误触,成功后停轮询返回列表页
const confirmUnbind = () => {
if (unbinding || !installId) return
Taro.showModal({
title: '解绑设备',
content: `确定解绑「${deviceName}」吗?解绑后需重新添加。`,
confirmText: '解绑',
confirmColor: '#EE4B4B',
success: (res) => {
if (!res.confirm) return
doUnbind()
}
})
}
const doUnbind = async () => {
setUnbinding(true)
try {
await unbindHomeDevice({ installId })
showToast('已解绑')
if (timerRef.current) {
clearInterval(timerRef.current)
timerRef.current = null
}
navigateBack()
} catch (e) {
showToast(e instanceof Error ? e.message : '解绑失败')
setUnbinding(false)
}
}
return (
<View className={styles.page}>
<Navbar title={deviceName} variant="hero" />
<View className={styles.heartCard}>
<View className={styles.heartLeft}>
<View className={styles.heartIconWrap}>
<Text className={styles.heartIcon}>❤</Text>
</View>
<Text className={styles.heartValue}>{fmt(signs?.heartRate)}</Text>
<Text className={styles.heartUnit}>BPM</Text>
<View className={`${styles.badge} ${online ? styles.badgeOn : styles.badgeOff}`}>
<View className={`${styles.badgeDot} ${online ? styles.badgeDotOn : styles.badgeDotOff}`} />
<Text>{online ? '在线' : '离线'}</Text>
</View>
</View>
<View className={styles.heartRight}>
<Text className={styles.heartHealthLabel}>心率健康</Text>
<VitalWave points={wavePoints} canvasId="heartWave" />
{signs?.lastTime && (
<Text className={styles.heartTime}>更新于 {signs.lastTime}</Text>
)}
</View>
</View>
<View className={styles.metricGrid}>
<View className={styles.metricCard}>
<Text className={`${styles.metricLabel} ${styles.metricLabelBlue}`}>呼吸率</Text>
<View className={styles.metricRow}>
<Text className={styles.metricValue}>{fmt(signs?.respiratoryRate)}</Text>
<Text className={styles.metricUnit}>/min</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={`${styles.metricLabel} ${styles.metricLabelOrange}`}>体动</Text>
<View className={styles.metricRow}>
<Text className={styles.metricValue}>{fmt(signs?.bodyMovement)}</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={`${styles.metricLabel} ${styles.metricLabelGreen}`}>运动状态</Text>
<View className={styles.metricRow}>
<Text className={styles.metricValue}>{MOVE_STATE_MAP[signs?.moveState ?? -1] ?? '--'}</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={`${styles.metricLabel} ${styles.metricLabelRed}`}>在床状态</Text>
<View className={styles.metricRow}>
<Text className={styles.metricValue}>{ONBED_STATUS_MAP[signs?.onbedStatus ?? -1] ?? '--'}</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={`${styles.metricLabel} ${styles.metricLabelBlue}`}>人体状态</Text>
<View className={styles.metricRow}>
<Text className={styles.metricValue}>{BODY_STATUS_MAP[signs?.bodyStatus ?? -1] ?? '--'}</Text>
</View>
</View>
<View className={styles.metricCard}>
<Text className={`${styles.metricLabel} ${styles.metricLabelGreen}`}>人体位置</Text>
<View className={styles.metricRow}>
<Text className={styles.metricValue}>{fmt(signs?.bodyPosition)}</Text>
<Text className={styles.metricUnit}>分米</Text>
</View>
</View>
</View>
<Text className={styles.infoTitle}>设备信息</Text>
<View className={styles.infoCard}>
<ListRowGroup>
<ListRow label="设备型号" value={deviceModel || '-'} />
<ListRow label="设备 ID" value={installId || '-'} />
<ListRow label="安装房间" value={device?.roomName ?? '-'} />
{/* BLE 配置流程联调未完成,暂隐藏两个配置入口
<ListRow label="网络配置" arrow onClick={goWifiConfig} />
<ListRow label="设备配置" arrow onClick={goBleConfig} />
*/}
<ListRow label="解绑设备" arrow danger onClick={confirmUnbind} />
</ListRowGroup>
</View>
</View>
)
}