feat(device): 详情页接专用接口——单设备详情轮询 + 历史心率波形
- homeDevice.ts 新增 getHomeDeviceDetail(GET /ylHomeDevice/get/{installId},实测含 runTimeInfo)
与 getVitalRecords(web 端同源 /iot/propertyRecord/getPropertyRecords,按 deviceName 查近一天倒序)
- 详情页轮询从「整列表再过滤」改为单设备详情接口,减小轮询载荷
- 恢复 VitalWave 心率波形 Canvas:历史 heartRate 序列按时间正序绘制贝塞尔平滑曲线 + 渐变填充
- 轮询生命周期 useDidShow 启停 / useDidHide 暂停,页面压栈后台不空跑
This commit is contained in:
parent
d7bd20eb79
commit
05d46368d1
|
|
@ -53,6 +53,54 @@ export function getHomeDeviceList(params: HomeDeviceListReq): Promise<HomeDevice
|
|||
return request.post('/yanglao/app/ylHomeDevice/getList', params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
// 单设备详情(GET + 路径参数,含 runTimeInfo 实时快照)
|
||||
export function getHomeDeviceDetail(installId: string): Promise<HomeDevice> {
|
||||
return request.get(`/yanglao/app/ylHomeDevice/get/${installId}`)
|
||||
}
|
||||
|
||||
// —— 历史体征记录(web 端 IotPropertyRecord 同源接口,2026-09-18 实测)——
|
||||
export interface VitalRecord {
|
||||
recordId: string
|
||||
deviceName: string
|
||||
name: string | null
|
||||
time: string
|
||||
heartRate?: number
|
||||
respiratoryRate?: number
|
||||
moveState?: number
|
||||
bodyStatus?: number
|
||||
bodyPosition?: number
|
||||
bodyMovement?: number
|
||||
productKey?: string
|
||||
}
|
||||
|
||||
interface PropertyRecordsReq {
|
||||
pageIndex: number
|
||||
pageSize: number
|
||||
queryParams: {
|
||||
searchType: 'deviceName'
|
||||
deviceName: string
|
||||
createTime?: string // '1' = 近一天(web 端约定)
|
||||
sortColumn?: string
|
||||
sortDirection?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 按 deviceName 查历史体征,倒序返回(最新的在前)
|
||||
export function getVitalRecords(deviceName: string, pageSize = 50): Promise<VitalRecord[]> {
|
||||
const params: PropertyRecordsReq = {
|
||||
pageIndex: 1,
|
||||
pageSize,
|
||||
queryParams: {
|
||||
searchType: 'deviceName',
|
||||
deviceName,
|
||||
createTime: '1',
|
||||
sortColumn: 'createTime',
|
||||
sortDirection: 'desc'
|
||||
}
|
||||
}
|
||||
return request.post('/iot/propertyRecord/getPropertyRecords', params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
export function addHomeDevice(params: HomeDeviceAddReq): Promise<void> {
|
||||
return request.post('/yanglao/app/ylHomeDevice/add', params as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useRouter } from '@tarojs/taro'
|
||||
import Taro 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 } 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 {
|
||||
getHomeDeviceDetail, getVitalRecords, type HomeDevice, type VitalSigns
|
||||
} from '@/api/homeDevice'
|
||||
import type { DeviceModel } from '@/constants/enums'
|
||||
import styles from './detail.module.scss'
|
||||
|
||||
// 体征轮询间隔:与 web 端 VitalSignsDashboard 的 SWR refreshInterval 对齐
|
||||
// 轮询间隔:与 web 端 VitalSignsDashboard SWR refreshInterval 对齐
|
||||
const POLL_INTERVAL = 3000
|
||||
|
||||
export default function Detail() {
|
||||
|
|
@ -25,7 +28,9 @@ export default function Detail() {
|
|||
// 列表页 EventChannel 传入的设备对象:首屏零延迟
|
||||
const [device, setDevice] = useState<HomeDevice | null>(null)
|
||||
const [online, setOnline] = useState(params.online === '1')
|
||||
const homeIdRef = useRef<string | null>(null)
|
||||
const [wavePoints, setWavePoints] = useState<number[]>([])
|
||||
const deviceNameRef = useRef<string | null>(null)
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const inst = Taro.getCurrentInstance()
|
||||
|
|
@ -33,33 +38,51 @@ export default function Detail() {
|
|||
page?.getOpenerEventChannel?.()?.on('device', (d) => {
|
||||
setDevice(d)
|
||||
setOnline(d.onlineStatus === 1)
|
||||
homeIdRef.current = d.homeId
|
||||
deviceNameRef.current = d.deviceName
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 轮询列表接口按 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 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 {
|
||||
// 单次轮询失败不中断,下个周期重试
|
||||
}
|
||||
const timer = setInterval(poll, POLL_INTERVAL)
|
||||
return () => clearInterval(timer)
|
||||
}, [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)
|
||||
|
|
@ -81,6 +104,7 @@ export default function Detail() {
|
|||
</View>
|
||||
<View className={styles.heartRight}>
|
||||
<Text className={styles.heartHealthLabel}>心率健康</Text>
|
||||
<VitalWave points={wavePoints} canvasId="heartWave" />
|
||||
{signs?.lastTime && (
|
||||
<Text className={styles.heartTime}>更新于 {signs.lastTime}</Text>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue