SEC_Warehouse/pages/device/detail.vue

344 lines
8.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- ED713 设备详情页:体征仪表盘(参考 web 端 VitalSignsDashboard 布局) -->
<template>
<view class="page page-base">
<up-navbar :title="deviceName" :fixed="false" :safe-area-inset-top="true" :border="false" auto-back />
<view v-if="loading" class="state-wrap">
<up-loading-icon mode="circle" size="48" text="加载中..." />
</view>
<view v-else-if="loadError" class="state-wrap">
<up-empty mode="wifi" text="加载失败,请检查网络" />
<up-button type="success" shape="circle" text="重试" class="retry-btn" @click="loadVitals" />
</view>
<template v-else>
<!-- 心率区:左侧大数字 + 右侧波形 -->
<view class="heart-card">
<view class="heart-left">
<text class="heart-icon">❤</text>
<text class="heart-value">{{ valueOf(vitals.heartRate) }}</text>
<text class="heart-unit">BPM</text>
<view class="heart-badge" :class="online ? 'heart-badge--on' : 'heart-badge--off'">
<view class="badge-dot" :class="online ? 'badge-dot--on' : 'badge-dot--off'" />
<text>{{ online ? '正常' : '离线' }}</text>
</view>
</view>
<view class="heart-right">
<view class="heart-right-title">
<text class="heart-health-label">心率健康</text>
</view>
<vital-wave :points="wavePoints" canvas-id="heartWave" />
</view>
</view>
<!-- 六个指标 2 列网格 -->
<view class="metric-grid">
<view v-for="m in metrics" :key="m.label" class="metric-card">
<text class="metric-label">{{ m.label }}</text>
<view class="metric-value-row">
<text class="metric-value" :style="{ color: m.color }">{{ m.value }}</text>
<text v-if="m.unit" class="metric-unit">{{ m.unit }}</text>
</view>
</view>
</view>
<!-- 设备信息 -->
<view class="info-card">
<view class="info-row">
<text class="info-label">设备型号</text>
<text class="info-value">{{ deviceModel || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">设备 ID</text>
<text class="info-value">{{ deviceId || '-' }}</text>
</view>
</view>
</template>
</view>
</template>
<script>
import vitalWave from '@/components/vital-wave.vue'
import { getHeartRateHistory, getVitalSigns } from '@/api/device'
// 枚举值中文映射,与 web 端 i18n (zh-CN) 一致
const MOVE_STATE_MAP = { 0: '无人', 1: '轻微活动', 2: '频繁活动', 3: '静卧' }
const BODY_STATUS_MAP = { 0: '无人', 1: '运动', 2: '呼吸', 3: '进入呼吸' }
const ONBED_STATUS_MAP = { 0: '不在床', 1: '在床' }
// 指标卡配色与 web 端 MetricCard color 一致
const METRIC_COLORS = {
breath: '#1677ff',
motion: '#fa8c16',
moveState: '#52c41a',
bedState: '#f5222d',
bodyState: '#1677ff',
bodyPosition: '#52c41a'
}
// 与 web 端 SWR refreshInterval 一致的轮询间隔
const REFRESH_INTERVAL = 3000
export default {
components: { vitalWave },
data() {
return {
deviceId: '',
deviceName: '设备详情',
deviceModel: '',
online: false,
loading: true,
loadError: false,
vitals: {},
wavePoints: [],
pollTimer: null
}
},
computed: {
// 六个指标卡数据(2 列网格顺序:呼吸率/体动/运动状态/在床状态/人体状态/人体位置)
metrics() {
const v = this.vitals
return [
{ label: '呼吸率', value: this.valueOf(v.respiratoryRate), unit: '/min', color: METRIC_COLORS.breath },
{ label: '体动', value: this.valueOf(v.bodyMovement), unit: '', color: METRIC_COLORS.motion },
{ label: '运动状态', value: MOVE_STATE_MAP[v.moveState] ?? this.valueOf(v.moveState), unit: '', color: METRIC_COLORS.moveState },
{ label: '在床状态', value: ONBED_STATUS_MAP[v.onbedStatus] ?? this.valueOf(v.onbedStatus), unit: '', color: METRIC_COLORS.bedState },
{ label: '人体状态', value: BODY_STATUS_MAP[v.bodyStatus] ?? this.valueOf(v.bodyStatus), unit: '', color: METRIC_COLORS.bodyState },
{ label: '人体位置', value: this.valueOf(v.bodyPosition), unit: '分米', color: METRIC_COLORS.bodyPosition }
]
}
},
onLoad(options) {
this.deviceId = options.id || ''
if (options.name) this.deviceName = decodeURIComponent(options.name)
this.deviceModel = options.model || ''
this.online = options.online === '1'
this.loadVitals()
},
onShow() {
this.startPolling()
},
onHide() {
this.stopPolling()
},
onUnload() {
this.stopPolling()
},
methods: {
// 拉取实时体征与心率历史;首拉失败切错误态供重试,轮询失败静默(避免 toast 每 3 秒弹一次)。
async loadVitals() {
this.loading = true
this.loadError = false
try {
await this.fetchVitals()
} catch (error) {
this.loadError = true
} finally {
this.loading = false
}
},
// 仅拉数据并落 state;wavePoints 引用变化触发波形组件重绘。
async fetchVitals() {
const [vitals, history] = await Promise.all([
getVitalSigns(this.deviceId),
getHeartRateHistory(this.deviceId)
])
this.vitals = vitals || {}
this.wavePoints = Array.isArray(history) ? history : []
},
// 轮询:每 3 秒静默刷新体征与波形(页面隐藏/卸载时停止)。
startPolling() {
this.stopPolling()
this.pollTimer = setInterval(() => {
this.fetchVitals().catch(() => {})
}, REFRESH_INTERVAL)
},
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer)
this.pollTimer = null
}
},
// 空值统一显示 '-'
valueOf(v) {
return v === undefined || v === null || v === '' ? '-' : v
}
}
}
</script>
<style lang="scss" scoped>
@import '@/styles/common.css';
.state-wrap {
margin-top: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.retry-btn {
margin-top: 40rpx;
width: 240rpx;
}
/* 心率区 */
.heart-card {
margin: 24rpx;
padding: 32rpx;
background: #ffffff;
border-radius: 24rpx;
display: flex;
align-items: flex-start;
}
.heart-left {
display: flex;
flex-direction: column;
align-items: center;
min-width: 200rpx;
}
.heart-icon {
font-size: 72rpx;
color: #f5222d;
line-height: 1;
}
.heart-value {
margin-top: 16rpx;
font-size: 96rpx;
font-weight: 700;
color: #2a2d33;
line-height: 1.1;
}
.heart-unit {
margin-top: 8rpx;
font-size: 30rpx;
color: #9ea1a6;
}
.heart-badge {
margin-top: 20rpx;
display: flex;
align-items: center;
padding: 8rpx 24rpx;
border-radius: 999rpx;
font-size: 24rpx;
&--on {
background: #f6ffed;
color: #52c41a;
}
&--off {
background: #f5f5f5;
color: #999999;
}
}
.badge-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
margin-right: 10rpx;
&--on {
background: #52c41a;
}
&--off {
background: #999999;
}
}
.heart-right {
flex: 1;
min-width: 0;
margin-left: 32rpx;
}
.heart-right-title {
margin-bottom: 8rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.heart-health-label {
font-size: 30rpx;
font-weight: 500;
color: #2a2d33;
}
/* 六指标 2 列网格 */
.metric-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 24rpx;
}
.metric-card {
width: 48.5%;
box-sizing: border-box;
margin-bottom: 20rpx;
padding: 24rpx;
background: #ffffff;
border-radius: 16rpx;
}
.metric-label {
font-size: 26rpx;
color: #9ea1a6;
}
.metric-value-row {
margin-top: 12rpx;
display: flex;
align-items: baseline;
}
.metric-value {
font-size: 44rpx;
font-weight: 700;
}
.metric-unit {
margin-left: 8rpx;
font-size: 24rpx;
color: #b5b9c0;
}
/* 设备信息 */
.info-card {
margin: 4rpx 24rpx 24rpx;
padding: 8rpx 32rpx;
background: #ffffff;
border-radius: 24rpx;
}
.info-row {
min-height: 104rpx;
display: flex;
align-items: center;
justify-content: space-between;
+ .info-row {
border-top: 1rpx solid #f0f1f3;
}
}
.info-label {
font-size: 28rpx;
color: #9ea1a6;
}
.info-value {
font-size: 28rpx;
color: #2a2d33;
}
</style>