feat(device): 设备详情接入实时体征——EventChannel 传设备对象 + 3s 轮询刷新
- HomeDevice 补 runTimeInfo.properties 体征字段类型(2026-09-18 联调实测) - 列表页跳详情改用 Taro.navigateTo 原生调用,EventChannel emit 整个 device 对象 - 详情页 getOpenerEventChannel 接收首屏零延迟渲染;setInterval 3s 轮询列表接口 按 installId 定位刷新(对齐 web 端 VitalSignsDashboard SWR refreshInterval=3000) - 体征六指标卡:呼吸率/体动/运动状态/在床状态/人体状态/人体位置, 枚举文案复用 MOVE_STATE_MAP/BODY_STATUS_MAP/ONBED_STATUS_MAP - 详情信息行新增安装房间
This commit is contained in:
parent
ccca973d62
commit
d7bd20eb79
|
|
@ -18,6 +18,18 @@ export interface HomeDeviceUnbindReq {
|
|||
installId: string // 设备安装 ID
|
||||
}
|
||||
|
||||
// 体征实时快照(runTimeInfo.properties,2026-09-18 联调实测;字段与 web 端 VitalSignsDashboard 一致)
|
||||
export interface VitalSigns {
|
||||
lastTime?: string
|
||||
heartRate?: number
|
||||
respiratoryRate?: number
|
||||
moveState?: number
|
||||
bodyStatus?: number
|
||||
onbedStatus?: number
|
||||
bodyPosition?: number
|
||||
bodyMovement?: number
|
||||
}
|
||||
|
||||
// 响应字段按 2026-09-17 联调实测
|
||||
export interface HomeDevice {
|
||||
installId: string
|
||||
|
|
@ -34,6 +46,7 @@ export interface HomeDevice {
|
|||
status: number
|
||||
onlineStatus: number // 1-在线 0-离线
|
||||
lastOnlineTime: string | null
|
||||
runTimeInfo?: { properties?: VitalSigns } | null
|
||||
}
|
||||
|
||||
export function getHomeDeviceList(params: HomeDeviceListReq): Promise<HomeDevice[]> {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.heartTime {
|
||||
font-size: 24px;
|
||||
color: $color-text-tertiary;
|
||||
}
|
||||
|
||||
.metricGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
@ -98,6 +103,11 @@
|
|||
|
||||
.metricValue { font-size: 44px; font-weight: 700; }
|
||||
|
||||
.metricBlue { color: $color-primary-deep; }
|
||||
.metricOrange { color: #fa8c16; }
|
||||
.metricGreen { color: $color-success; }
|
||||
.metricRed { color: $color-danger; }
|
||||
|
||||
.metricUnit {
|
||||
margin-left: 8px;
|
||||
font-size: 24px;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,66 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useRouter } from '@tarojs/taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Navbar } from '@/components/navbar'
|
||||
import { ListRow, ListRowGroup } from '@/components/list-row'
|
||||
import { ROUTES } from '@/constants/routes'
|
||||
import { navigateTo } from '@/utils/navigation'
|
||||
import { MOVE_STATE_MAP, BODY_STATUS_MAP, ONBED_STATUS_MAP } from '@/constants/enums'
|
||||
import { getHomeDeviceList, 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 deviceId = params.id ?? ''
|
||||
const installId = params.id ?? ''
|
||||
const deviceName = params.name ? decodeURIComponent(params.name) : '设备详情'
|
||||
const deviceModel = params.model ?? ''
|
||||
const online = params.online === '1'
|
||||
|
||||
// 列表页 EventChannel 传入的设备对象:首屏零延迟
|
||||
const [device, setDevice] = useState<HomeDevice | null>(null)
|
||||
const [online, setOnline] = useState(params.online === '1')
|
||||
const homeIdRef = useRef<string | 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)
|
||||
homeIdRef.current = d.homeId
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 轮询列表接口按 installId 定位目标设备,刷新体征快照(对齐 web 端 SWR 轮询模式)
|
||||
useEffect(() => {
|
||||
const poll = async () => {
|
||||
if (!installId) return
|
||||
const homeId = homeIdRef.current
|
||||
if (!homeId) return
|
||||
try {
|
||||
const list = await getHomeDeviceList({ homeId })
|
||||
const next = (list ?? []).find((d) => d.installId === installId)
|
||||
if (next) {
|
||||
setDevice(next)
|
||||
setOnline(next.onlineStatus === 1)
|
||||
}
|
||||
} catch {
|
||||
// 单次轮询失败不中断定时器,下个周期重试
|
||||
}
|
||||
}
|
||||
const timer = setInterval(poll, POLL_INTERVAL)
|
||||
return () => clearInterval(timer)
|
||||
}, [installId])
|
||||
|
||||
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)
|
||||
const goWifiConfig = () => navigateTo(ROUTES.DEVICE_BLE_CONFIG)
|
||||
|
|
@ -28,7 +72,7 @@ export default function Detail() {
|
|||
<View className={styles.heartCard}>
|
||||
<View className={styles.heartLeft}>
|
||||
<Text className={styles.heartIcon}>❤</Text>
|
||||
<Text className={styles.heartValue}>--</Text>
|
||||
<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}`} />
|
||||
|
|
@ -36,14 +80,59 @@ export default function Detail() {
|
|||
</View>
|
||||
</View>
|
||||
<View className={styles.heartRight}>
|
||||
<Text className={styles.heartHealthLabel}>体征数据接入中</Text>
|
||||
<Text className={styles.heartHealthLabel}>心率健康</Text>
|
||||
{signs?.lastTime && (
|
||||
<Text className={styles.heartTime}>更新于 {signs.lastTime}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className={styles.metricGrid}>
|
||||
<View className={styles.metricCard}>
|
||||
<Text className={styles.metricLabel}>呼吸率</Text>
|
||||
<View className={styles.metricRow}>
|
||||
<Text className={`${styles.metricValue} ${styles.metricBlue}`}>{fmt(signs?.respiratoryRate)}</Text>
|
||||
<Text className={styles.metricUnit}>/min</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.metricCard}>
|
||||
<Text className={styles.metricLabel}>体动</Text>
|
||||
<View className={styles.metricRow}>
|
||||
<Text className={`${styles.metricValue} ${styles.metricOrange}`}>{fmt(signs?.bodyMovement)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.metricCard}>
|
||||
<Text className={styles.metricLabel}>运动状态</Text>
|
||||
<View className={styles.metricRow}>
|
||||
<Text className={`${styles.metricValue} ${styles.metricGreen}`}>{MOVE_STATE_MAP[signs?.moveState ?? -1] ?? '--'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.metricCard}>
|
||||
<Text className={styles.metricLabel}>在床状态</Text>
|
||||
<View className={styles.metricRow}>
|
||||
<Text className={`${styles.metricValue} ${styles.metricRed}`}>{ONBED_STATUS_MAP[signs?.onbedStatus ?? -1] ?? '--'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.metricCard}>
|
||||
<Text className={styles.metricLabel}>人体状态</Text>
|
||||
<View className={styles.metricRow}>
|
||||
<Text className={`${styles.metricValue} ${styles.metricBlue}`}>{BODY_STATUS_MAP[signs?.bodyStatus ?? -1] ?? '--'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className={styles.metricCard}>
|
||||
<Text className={styles.metricLabel}>人体位置</Text>
|
||||
<View className={styles.metricRow}>
|
||||
<Text className={`${styles.metricValue} ${styles.metricGreen}`}>{fmt(signs?.bodyPosition)}</Text>
|
||||
<Text className={styles.metricUnit}>分米</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className={styles.infoCard}>
|
||||
<ListRowGroup>
|
||||
<ListRow label="设备型号" value={deviceModel || '-'} />
|
||||
<ListRow label="设备 ID" value={deviceId || '-'} />
|
||||
<ListRow label="设备 ID" value={installId || '-'} />
|
||||
<ListRow label="安装房间" value={device?.roomName ?? '-'} />
|
||||
<ListRow label="网络配置" arrow onClick={goWifiConfig} />
|
||||
<ListRow label="设备配置" arrow onClick={goBleConfig} />
|
||||
</ListRowGroup>
|
||||
|
|
|
|||
|
|
@ -100,11 +100,13 @@ export default function DeviceList() {
|
|||
loadRooms()
|
||||
})
|
||||
|
||||
// EventChannel 把整个设备对象传给详情页:首屏零延迟渲染体征快照
|
||||
const goDetail = (device: HomeDevice) => {
|
||||
const name = encodeURIComponent(deviceLabel(device))
|
||||
navigateTo(
|
||||
`${ROUTES.DEVICE_DETAIL}?id=${device.installId}&name=${name}&model=${device.subDeviceType ?? ''}&online=${device.onlineStatus ? 1 : 0}`
|
||||
)
|
||||
Taro.navigateTo({
|
||||
url: `${ROUTES.DEVICE_DETAIL}?id=${device.installId}&name=${name}&model=${device.subDeviceType ?? ''}&online=${device.onlineStatus ? 1 : 0}`,
|
||||
success: (res) => res.eventChannel.emit('device', device)
|
||||
})
|
||||
}
|
||||
const goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
|
||||
const goRoom = () => navigateTo(ROUTES.SCENE_SELECT)
|
||||
|
|
|
|||
Loading…
Reference in New Issue