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
|
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 联调实测
|
// 响应字段按 2026-09-17 联调实测
|
||||||
export interface HomeDevice {
|
export interface HomeDevice {
|
||||||
installId: string
|
installId: string
|
||||||
|
|
@ -34,6 +46,7 @@ export interface HomeDevice {
|
||||||
status: number
|
status: number
|
||||||
onlineStatus: number // 1-在线 0-离线
|
onlineStatus: number // 1-在线 0-离线
|
||||||
lastOnlineTime: string | null
|
lastOnlineTime: string | null
|
||||||
|
runTimeInfo?: { properties?: VitalSigns } | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHomeDeviceList(params: HomeDeviceListReq): Promise<HomeDevice[]> {
|
export function getHomeDeviceList(params: HomeDeviceListReq): Promise<HomeDevice[]> {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,11 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heartTime {
|
||||||
|
font-size: 24px;
|
||||||
|
color: $color-text-tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
.metricGrid {
|
.metricGrid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -98,6 +103,11 @@
|
||||||
|
|
||||||
.metricValue { font-size: 44px; font-weight: 700; }
|
.metricValue { font-size: 44px; font-weight: 700; }
|
||||||
|
|
||||||
|
.metricBlue { color: $color-primary-deep; }
|
||||||
|
.metricOrange { color: #fa8c16; }
|
||||||
|
.metricGreen { color: $color-success; }
|
||||||
|
.metricRed { color: $color-danger; }
|
||||||
|
|
||||||
.metricUnit {
|
.metricUnit {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,66 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useRouter } from '@tarojs/taro'
|
import { useRouter } from '@tarojs/taro'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
import { View, Text } from '@tarojs/components'
|
import { View, Text } from '@tarojs/components'
|
||||||
import { Navbar } from '@/components/navbar'
|
import { Navbar } from '@/components/navbar'
|
||||||
import { ListRow, ListRowGroup } from '@/components/list-row'
|
import { ListRow, ListRowGroup } from '@/components/list-row'
|
||||||
import { ROUTES } from '@/constants/routes'
|
import { ROUTES } from '@/constants/routes'
|
||||||
import { navigateTo } from '@/utils/navigation'
|
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 type { DeviceModel } from '@/constants/enums'
|
||||||
import styles from './detail.module.scss'
|
import styles from './detail.module.scss'
|
||||||
|
|
||||||
// 体征数据后端暂无对应接口:详情页先保留设备信息静态展示,
|
// 体征轮询间隔:与 web 端 VitalSignsDashboard 的 SWR refreshInterval 对齐
|
||||||
// 心率/呼吸等实时体征待接口定型后接入
|
const POLL_INTERVAL = 3000
|
||||||
|
|
||||||
export default function Detail() {
|
export default function Detail() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const params = router.params as { id?: string; name?: string; model?: DeviceModel; online?: string }
|
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 deviceName = params.name ? decodeURIComponent(params.name) : '设备详情'
|
||||||
const deviceModel = params.model ?? ''
|
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 goBleConfig = () => navigateTo(ROUTES.DEVICE_BLE_CONFIG)
|
||||||
const goWifiConfig = () => 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.heartCard}>
|
||||||
<View className={styles.heartLeft}>
|
<View className={styles.heartLeft}>
|
||||||
<Text className={styles.heartIcon}>❤</Text>
|
<Text className={styles.heartIcon}>❤</Text>
|
||||||
<Text className={styles.heartValue}>--</Text>
|
<Text className={styles.heartValue}>{fmt(signs?.heartRate)}</Text>
|
||||||
<Text className={styles.heartUnit}>BPM</Text>
|
<Text className={styles.heartUnit}>BPM</Text>
|
||||||
<View className={`${styles.badge} ${online ? styles.badgeOn : styles.badgeOff}`}>
|
<View className={`${styles.badge} ${online ? styles.badgeOn : styles.badgeOff}`}>
|
||||||
<View className={`${styles.badgeDot} ${online ? styles.badgeDotOn : styles.badgeDotOff}`} />
|
<View className={`${styles.badgeDot} ${online ? styles.badgeDotOn : styles.badgeDotOff}`} />
|
||||||
|
|
@ -36,14 +80,59 @@ export default function Detail() {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className={styles.heartRight}>
|
<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>
|
</View>
|
||||||
|
|
||||||
<View className={styles.infoCard}>
|
<View className={styles.infoCard}>
|
||||||
<ListRowGroup>
|
<ListRowGroup>
|
||||||
<ListRow label="设备型号" value={deviceModel || '-'} />
|
<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={goWifiConfig} />
|
||||||
<ListRow label="设备配置" arrow onClick={goBleConfig} />
|
<ListRow label="设备配置" arrow onClick={goBleConfig} />
|
||||||
</ListRowGroup>
|
</ListRowGroup>
|
||||||
|
|
|
||||||
|
|
@ -100,11 +100,13 @@ export default function DeviceList() {
|
||||||
loadRooms()
|
loadRooms()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// EventChannel 把整个设备对象传给详情页:首屏零延迟渲染体征快照
|
||||||
const goDetail = (device: HomeDevice) => {
|
const goDetail = (device: HomeDevice) => {
|
||||||
const name = encodeURIComponent(deviceLabel(device))
|
const name = encodeURIComponent(deviceLabel(device))
|
||||||
navigateTo(
|
Taro.navigateTo({
|
||||||
`${ROUTES.DEVICE_DETAIL}?id=${device.installId}&name=${name}&model=${device.subDeviceType ?? ''}&online=${device.onlineStatus ? 1 : 0}`
|
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 goAdd = () => navigateTo(ROUTES.DEVICE_ADD)
|
||||||
const goRoom = () => navigateTo(ROUTES.SCENE_SELECT)
|
const goRoom = () => navigateTo(ROUTES.SCENE_SELECT)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue