diff --git a/src/api/homeDevice.ts b/src/api/homeDevice.ts index 89f96c0..415be33 100644 --- a/src/api/homeDevice.ts +++ b/src/api/homeDevice.ts @@ -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 { diff --git a/src/pages/device/feature/detail.module.scss b/src/pages/device/feature/detail.module.scss index cb0e70c..5c126e8 100644 --- a/src/pages/device/feature/detail.module.scss +++ b/src/pages/device/feature/detail.module.scss @@ -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; diff --git a/src/pages/device/feature/detail.tsx b/src/pages/device/feature/detail.tsx index 204075e..d57b20d 100644 --- a/src/pages/device/feature/detail.tsx +++ b/src/pages/device/feature/detail.tsx @@ -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(null) + const [online, setOnline] = useState(params.online === '1') + const homeIdRef = useRef(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() { ❤ - -- + {fmt(signs?.heartRate)} BPM @@ -36,14 +80,59 @@ export default function Detail() { - 体征数据接入中 + 心率健康 + {signs?.lastTime && ( + 更新于 {signs.lastTime} + )} + + + + + + 呼吸率 + + {fmt(signs?.respiratoryRate)} + /min + + + + 体动 + + {fmt(signs?.bodyMovement)} + + + + 运动状态 + + {MOVE_STATE_MAP[signs?.moveState ?? -1] ?? '--'} + + + + 在床状态 + + {ONBED_STATUS_MAP[signs?.onbedStatus ?? -1] ?? '--'} + + + + 人体状态 + + {BODY_STATUS_MAP[signs?.bodyStatus ?? -1] ?? '--'} + + + + 人体位置 + + {fmt(signs?.bodyPosition)} + 分米 + - + + diff --git a/src/pages/device/index.tsx b/src/pages/device/index.tsx index 72d5f16..bef8ded 100644 --- a/src/pages/device/index.tsx +++ b/src/pages/device/index.tsx @@ -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)