Browse Source

feat(设备详情): 更新呼吸率图表功能并调整高度范围

- 将呼吸率图表从模拟数据改为接收真实MQTT数据
- 调整设备高度范围从0-50cm改为0-5cm
- 优化MQTT连接日志和错误提示
- 清空呼吸率数据时同时清空图表数据
liujia 2 tháng trước cách đây
mục cha
commit
ce4b576

+ 36 - 83
src/views/device/detail/components/breathLineChart/index.vue

@@ -3,15 +3,26 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, watch } from 'vue'
 import * as echarts from 'echarts'
 
 defineOptions({ name: 'BreathLineChart' })
 
+const props = withDefaults(
+  defineProps<{
+    data: number[]
+  }>(),
+  {
+    data: () => [],
+  }
+)
+
 const lineChartRef = ref<HTMLDivElement | null>(null)
+const lineChart = ref<echarts.ECharts | null>(null)
+const option = ref<echarts.EChartsOption>()
 
 onMounted(() => {
-  const lineChart = echarts.init(lineChartRef.value!)
+  lineChart.value = echarts.init(lineChartRef.value!)
 
   // -----------------------------
   // 配置显示的时间和数据点
@@ -22,25 +33,8 @@ onMounted(() => {
   const xAxisData = Array.from({ length: maxPoints }, (_, i) => i * step)
   const lineData: number[] = Array(maxPoints).fill(20) // 初始基准值为20
 
-  let t = 0 // 时间计数器(秒)
-
-  // -----------------------------
-  // 模拟呼吸参数
-  // -----------------------------
-  const bpm = 50 // 平均呼吸次数(次/分钟),可以修改
-  const basePeriod = 60 / bpm // 平均每次呼吸周期(秒)
-  let period = basePeriod // 当前周期
-  let amplitude = 10 // 波动幅度
-
-  const yBase = 20 // y轴基准线,波形围绕此值波动
-
-  // 用于计算实时BPM
-  const peakTimes: number[] = []
-
-  // -----------------------------
   // 初始化图表配置
-  // -----------------------------
-  const option = {
+  option.value = {
     title: [
       {
         text: '呼吸率曲线', // 主标题
@@ -68,7 +62,7 @@ onMounted(() => {
       type: 'value',
       min: 0,
       max: 40,
-      name: '呼吸幅度(次)',
+      name: '呼吸(次)',
       nameLocation: 'end',
       nameGap: 20,
       nameTextStyle: { fontSize: 12, color: '#888888', fontWeight: 'normal' },
@@ -87,75 +81,34 @@ onMounted(() => {
     animationDuration: 100,
   }
 
-  lineChart.setOption(option)
-
-  // -----------------------------
-  // 定时生成模拟呼吸数据
-  // -----------------------------
-  setInterval(() => {
-    t += step // 更新时间
+  lineChart.value.setOption(option.value)
 
-    // -----------------------------
-    // 调整波形随机性
-    // -----------------------------
-    if (Math.random() < 0.1) {
-      // 每隔一段时间随机调整周期和幅度,使波形更自然
-      period = basePeriod * (0.8 + Math.random() * 0.4) // ±20% ~ ±40%
-      amplitude = 8 + Math.random() * 6 // 幅度 8~14
-    }
-
-    // -----------------------------
-    // 生成当前点的呼吸值
-    // -----------------------------
-    // 正弦波 + 小扰动
-    const val = yBase + amplitude * Math.sin(((2 * Math.PI) / period) * t) + (Math.random() * 2 - 1)
-    const clampedVal = Math.min(40, Math.max(0, val)) // 保证不超过y轴范围
-
-    // -----------------------------
-    // 更新折线数据
-    // -----------------------------
-    lineData.shift() // 删除最旧的数据点
-    lineData.push(clampedVal) // 添加新数据点
+  // 自适应窗口大小
+  window.addEventListener('resize', () => lineChart.value!.resize())
+})
 
-    // -----------------------------
-    // 检测波峰用于计算实时BPM
-    // -----------------------------
-    const lastIndex = lineData.length - 1
-    if (lastIndex >= 2) {
-      // 当前点大于前一点和后一点,则为峰
-      if (
-        lineData[lastIndex - 2] < lineData[lastIndex - 1] &&
-        lineData[lastIndex] < lineData[lastIndex - 1]
-      ) {
-        const peakTime = t
-        peakTimes.push(peakTime)
-        // 保留最近60秒的峰,用于计算BPM
-        while (peakTimes.length && peakTimes[0] < t - 60) peakTimes.shift()
-      }
+watch(
+  () => props.data,
+  (newData) => {
+    const list = newData
+    if (list.length > 60) {
+      list.shift()
     }
-
-    // -----------------------------
-    // 计算实时BPM
-    // -----------------------------
-    const currentBPM = peakTimes.length
-
-    // -----------------------------
-    // 更新图表
-    // -----------------------------
-    lineChart.setOption(
+    lineChart.value!.setOption(
       {
-        series: [{ ...option.series[0], data: lineData.slice() }], // 更新折线数据
-        title: [{}, { text: `BPM: ${currentBPM} 次/分钟` }, {}], // 更新实时BPM
+        series:
+          option.value && option.value.series
+            ? Array.isArray(option.value.series)
+              ? [{ ...option.value.series[0], data: list }]
+              : [{ ...option.value.series, data: list }]
+            : [{ data: list }], // 更新折线数据
+        title: [{}, { text: `BPM: ${newData[newData.length - 1]} 次/分钟` }, {}], // 更新实时BPM
       },
       { notMerge: false }
     )
-  }, 100) // 每0.5秒刷新一次数据
-
-  // -----------------------------
-  // 自适应窗口大小
-  // -----------------------------
-  window.addEventListener('resize', () => lineChart.resize())
-})
+  },
+  { deep: true }
+)
 </script>
 
 <style scoped>

+ 2 - 2
src/views/device/detail/components/deviceBaseConfig/index.vue

@@ -131,7 +131,7 @@
               <template #suffix>
                 <a-tooltip>
                   <template #title>
-                    <div>范围:0 - 50 cm</div>
+                    <div>范围:0 - 5 cm</div>
                   </template>
                   <info-circle-outlined style="color: rgba(0, 0, 0, 0.45)" />
                 </a-tooltip>
@@ -404,7 +404,7 @@ const rules: Record<string, Rule[]> = {
   ],
   zRangeStart: [
     {
-      validator: createRangeValidator(0, 50),
+      validator: createRangeValidator(0, 5),
       trigger: ['change', 'blur'],
     },
   ],

+ 27 - 12
src/views/device/detail/index.vue

@@ -78,7 +78,7 @@
             v-if="furnitureItems && furnitureItems.some((item) => item.type === 'bed')"
             class="breathLine"
           >
-            <BreathLineChart></BreathLineChart>
+            <BreathLineChart :data="breathRpmList"></BreathLineChart>
           </div>
         </div>
       </info-card>
@@ -344,10 +344,14 @@ const resetMqttTimeout = () => {
   if (mqttTimeout) clearTimeout(mqttTimeout)
   mqttTimeout = window.setTimeout(() => {
     Object.keys(targets).forEach((key) => delete targets[Number(key)])
+    breathRpmList.value = []
     console.log('MQTT超时未收到新消息,隐藏所有红点')
   }, MQTT_TIMEOUT_MS)
 }
 
+// 呼吸率
+const breathRpmList = ref<number[]>([])
+
 onMounted(() => {
   console.log('onMounted', mqttClient)
   const mqttConfig = {
@@ -361,31 +365,38 @@ onMounted(() => {
     username: mqttConfig.username,
     password: mqttConfig.password,
   })
-  console.log('mqttClient connect ready', mqttClient)
+  console.log('⌛️ mqttClient connect ready', mqttClient)
   mqttClient.on('connect', () => {
-    console.log('MQTT已连接')
+    console.log('MQTT已连接')
     // 订阅所有设备的主题
-    mqttClient?.subscribe(`/mps/${clientId.value}/realtime_pos`, (err) => {
+    const sub = `/dev/${clientId.value}/dsp_data`
+    mqttClient?.subscribe(sub, (err) => {
       if (err) {
-        console.error('MQTT订阅失败', err)
+        console.error('MQTT订阅失败', err)
       } else {
-        console.log(`已订阅主题 /mps/${clientId.value}/realtime_pos`)
+        console.log(`⚛️ 已订阅主题 ${sub}`)
       }
     })
   })
   mqttClient.on('error', (err) => {
-    console.error('MQTT连接错误', err)
+    console.error('MQTT连接错误', err)
   })
   mqttClient.on('message', (topic: string, message: Uint8Array) => {
     resetMqttTimeout()
-    const match = topic.match(/^\/mps\/(.+)\/realtime_pos$/)
+    const subMatch = /^\/dev\/(.+)\/dsp_data$/
+    const match = topic.match(subMatch)
     if (!match) return
     const msgDevId = match[1]
     if (msgDevId !== clientId.value) return // 只处理当前设备
     try {
       const data = JSON.parse(message.toString())
-      const arr = data.targetPoints
-      console.log('收到MQTT消息', data, data.targetPoints)
+      const arr = data.tracker_targets
+      console.log('🚀 收到MQTT消息', data, {
+        '🔴 目标人数': data.tracker_targets.length,
+        '🟢 呼吸率': data.health.breath_rpm,
+        '🟡 点位图': data.tracker_targets,
+      })
+      breathRpmList.value.push(Math.floor(data.health.breath_rpm || 0))
       if (Array.isArray(arr) && arr.length > 0 && Array.isArray(arr[0])) {
         // 记录本次出现的所有id
         const currentIds = new Set<number>()
@@ -407,7 +418,7 @@ onMounted(() => {
             targets[id].lastY = y
             targets[id].displayX = x
             targets[id].displayY = y
-            console.log(`更新目标点: id=${id}, x=${x}, y=${y}`, targets[id])
+            console.log(`🔄 更新目标点: id=${id}, x=${x}, y=${y}`, targets[id])
           } else {
             // 距离太小,忽略本次更新
             // console.log(`忽略微小抖动 id=${id}`)
@@ -424,7 +435,7 @@ onMounted(() => {
       } else {
         // 没有目标时,隐藏所有红点
         Object.keys(targets).forEach((key) => delete targets[Number(key)])
-        // console.log('tracker_targets为空,隐藏所有红点')
+        breathRpmList.value = []
       }
     } catch (e) {
       console.error('MQTT消息解析失败', e)
@@ -432,6 +443,10 @@ onMounted(() => {
   })
 })
 
+// setInterval(() => {
+//   breathRpmList.value.push(Math.floor(Math.random() * 30))
+// }, 100)
+
 const areaAvailable = computed(() => {
   const { length, width } = detailState.value
   return Number(length) < 50 || Number(width) < 50