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:
ozh 2026-09-18 17:36:58 +08:00
parent d7bd20eb79
commit 05d46368d1
2 changed files with 97 additions and 25 deletions

View File

@ -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>)
}

View File

@ -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 () => {
// 单次刷新:设备详情(实时快照)+ 历史心率(波形)
const fetchAll = useCallback(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)
}
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>
)}