206 lines
8.0 KiB
TypeScript
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>
|
|
)
|
|
}
|