Browse Source

增加手柄+音频+信号+电量+码数等设备状态
Signed-off-by: caner <5658514@qq.com>

caner 3 years ago
parent
commit
0cbc054194
8 changed files with 527 additions and 22 deletions
  1. 10 0
      README.md
  2. 1 0
      package.json
  3. 91 22
      src/App.vue
  4. 2 0
      src/assets/ct.svg
  5. 106 0
      src/components/battery.vue
  6. 109 0
      src/components/gauge.vue
  7. 80 0
      src/components/record.vue
  8. 128 0
      src/components/signal.vue

+ 10 - 0
README.md

@@ -13,3 +13,13 @@
 1. yarn
 1. yarn
 2. yarn build (www文件即是打包后的)
 2. yarn build (www文件即是打包后的)
 ```
 ```
+
+## 注意
+```
+1. 遥控手柄自行连接电脑
+2. 左摇杆云台控制
+3. 右摇杆方向及车速控制
+4. A键是2档
+5. B键是1档
+6. R2是语音(按住说话)
+```

+ 1 - 0
package.json

@@ -8,6 +8,7 @@
     "lint": "vue-cli-service lint"
     "lint": "vue-cli-service lint"
   },
   },
   "dependencies": {
   "dependencies": {
+    "echarts": "^5.3.3",
     "socket.io-client": "^4.4.1",
     "socket.io-client": "^4.4.1",
     "view-design": "^4.7.0",
     "view-design": "^4.7.0",
     "vue": "^2.6.11"
     "vue": "^2.6.11"

+ 91 - 22
src/App.vue

@@ -2,7 +2,42 @@
   <div id="app">
   <div id="app">
     <template v-if="isLogin">
     <template v-if="isLogin">
       <video id="v2" autoplay playsinline muted></video>
       <video id="v2" autoplay playsinline muted></video>
-      <div class="marke"></div>
+      <div class="marke">
+        <!-- 手柄状态 -->
+        <svg
+          viewBox="0 -50 1024 1024"
+          version="1.1"
+          xmlns="http://www.w3.org/2000/svg"
+          width="30"
+          height="30"
+        >
+          <path
+            d="M817.68 803.17a130.23 130.23 0 0 1-125.6-96.37l-14-52.19a54.08 54.08 0 0 0-52.16-40H398.07a54.08 54.08 0 0 0-52.16 40l-14 52.19c-15.54 58-68.21 96.37-125.6 96.37A130 130 0 0 1 80.78 639.51l66.72-249a181.66 181.66 0 0 1 63.19-97.15A177.79 177.79 0 0 1 322 254.58h380a177.79 177.79 0 0 1 111.31 38.79 181.66 181.66 0 0 1 63.19 97.15l66.72 249a130 130 0 0 1-125.54 163.65zM322 274.58A160 160 0 0 0 166.87 395.5v0.13l-66.73 249a110 110 0 0 0 212.5 56.94l14-52.19a74.11 74.11 0 0 1 71.48-54.85h227.81a74.11 74.11 0 0 1 71.48 54.85l14 52.19a110 110 0 0 0 212.5-56.94L857.13 395.5A160 160 0 0 0 702 274.58z"
+            :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
+          ></path>
+          <path
+            d="M580 213.86a12 12 0 0 1 12 12v28H432v-28a12 12 0 0 1 12-12h136m0-20H444a32 32 0 0 0-32 32v48h200v-48a32 32 0 0 0-32-32z"
+            :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
+          ></path>
+          <path
+            d="M512 213.86a10 10 0 0 1-10-10v-63a60.07 60.07 0 0 1 60-60 10 10 0 0 1 0 20 40 40 0 0 0-40 40v63a10 10 0 0 1-10 10zM330 344.86a90 90 0 1 1-90 90 90.1 90.1 0 0 1 90-90m0-20a110 110 0 1 0 110 110 110 110 0 0 0-110-110z"
+            :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
+          ></path>
+          <path
+            d="M330 384.86a50 50 0 1 1-50 50 50.06 50.06 0 0 1 50-50m0-20a70 70 0 1 0 70 70 70 70 0 0 0-70-70zM697 344.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM697 496.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM773 420.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM621 420.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34z"
+            :fill="contrlState ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
+          ></path>
+        </svg>
+        <!-- 音频 -->
+        <Record />
+        <!-- 信号 -->
+        <Signal :signalValue="4" />
+        <!-- 电量 -->
+        <Battery :quantity="60" />
+      </div>
+      <div class="gauge">
+        <Gauge :value="50" :gears="speed" />
+      </div>
       <Loading v-if="showLoading" />
       <Loading v-if="showLoading" />
     </template>
     </template>
     <Login v-else :err="error" @loginBack="login" />
     <Login v-else :err="error" @loginBack="login" />
@@ -13,19 +48,25 @@
 const { io } = require("socket.io-client");
 const { io } = require("socket.io-client");
 import Login from "@/components/login";
 import Login from "@/components/login";
 import Loading from "@/components/loading";
 import Loading from "@/components/loading";
+import Record from "@/components/record";
+import Signal from "@/components/signal";
+import Battery from "@/components/battery";
+import Gauge from "@/components/gauge";
 import { Message } from "view-design";
 import { Message } from "view-design";
+
 const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
 const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
 export default {
 export default {
-  components: { Login, Loading },
+  components: { Login, Loading, Record, Signal, Battery, Gauge },
   data() {
   data() {
     return {
     return {
       socket: null,
       socket: null,
       HOST: "wss://car.caner.top",
       HOST: "wss://car.caner.top",
       Peer: null,
       Peer: null,
-      isLogin: false,
+      isLogin: true,
       error: "",
       error: "",
       remoteVideo: null,
       remoteVideo: null,
-      showLoading: true,
+      showLoading: false,
+      contrlState: false,
       speed: 1, //1低速档 | 2 高速档
       speed: 1, //1低速档 | 2 高速档
       iceServers: [
       iceServers: [
         {
         {
@@ -51,7 +92,6 @@ export default {
       // socket
       // socket
       this.socket.on("connect", () => {
       this.socket.on("connect", () => {
         try {
         try {
-
           this.isLogin = true;
           this.isLogin = true;
           // init webrtc
           // init webrtc
           this.Peer = new RTCPeerConnection({
           this.Peer = new RTCPeerConnection({
@@ -79,7 +119,11 @@ export default {
           this.Peer.oniceconnectionstatechange = async () => {
           this.Peer.oniceconnectionstatechange = async () => {
             const state = this.Peer.iceConnectionState;
             const state = this.Peer.iceConnectionState;
             console.log("ICE状态", state);
             console.log("ICE状态", state);
-            if ( state === "failed" || state === "disconnected" || state === "closed" ) {
+            if (
+              state === "failed" ||
+              state === "disconnected" ||
+              state === "closed"
+            ) {
               this.close("P2P通信失败");
               this.close("P2P通信失败");
             }
             }
 
 
@@ -87,12 +131,11 @@ export default {
             if (state === "connected") {
             if (state === "connected") {
               // init Control
               // init Control
               window.addEventListener("gamepadconnected", this.conControl);
               window.addEventListener("gamepadconnected", this.conControl);
-              window.addEventListener("gamepaddisconnected", this.disControl);              
+              window.addEventListener("gamepaddisconnected", this.disControl);
               await sleep(3000);
               await sleep(3000);
               this.showLoading = false;
               this.showLoading = false;
             }
             }
           };
           };
-          
         } catch (error) {
         } catch (error) {
           this.socket.disconnect();
           this.socket.disconnect();
           this.isLogin = false;
           this.isLogin = false;
@@ -133,13 +176,15 @@ export default {
       // 挡位选择AB
       // 挡位选择AB
       if (db.buttons[1].touched) this.speed = 2;
       if (db.buttons[1].touched) this.speed = 2;
       if (db.buttons[0].touched) this.speed = 1;
       if (db.buttons[0].touched) this.speed = 1;
+      // 语音按键
+      if (db.buttons[7].touched) console.log("R2");
       const params = {
       const params = {
         v0: Math.floor(db.axes[0] * 128 + 128),
         v0: Math.floor(db.axes[0] * 128 + 128),
         v1: Math.floor(db.axes[1] * 128 + 128),
         v1: Math.floor(db.axes[1] * 128 + 128),
         v2: Math.floor(db.axes[2] * 128 + 128),
         v2: Math.floor(db.axes[2] * 128 + 128),
         v3: this.Gear(this.speed, Math.floor(db.axes[3] * 128 + 128)),
         v3: this.Gear(this.speed, Math.floor(db.axes[3] * 128 + 128)),
       };
       };
-      console.log(params);
+
       if (this.socket.connected) {
       if (this.socket.connected) {
         this.socket.emit("msg", { type: "conctrl", conctrl: params });
         this.socket.emit("msg", { type: "conctrl", conctrl: params });
       }
       }
@@ -199,24 +244,20 @@ export default {
     },
     },
 
 
     // 手柄连接
     // 手柄连接
-    conControl(event) {
-      Message.success({
-        content: event.gamepad.id + "手柄已连接",
-        duration: 3,
-      });
+    conControl() {
+      this.contrlState = true;
       this.ControlData();
       this.ControlData();
     },
     },
 
 
     // 手柄断开连接
     // 手柄断开连接
-    disControl(event) {
-      Message.error({
-        content: event.gamepad.id + "手柄已断开",
-        duration: 3,
-      });
+    disControl() {
+      this.contrlState = false;
       cancelAnimationFrame(this.ControlData);
       cancelAnimationFrame(this.ControlData);
     },
     },
   },
   },
   mounted() {
   mounted() {
+    window.addEventListener("gamepadconnected", this.conControl);
+    window.addEventListener("gamepaddisconnected", this.disControl);
     this.intSoketRtc(this.HOST);
     this.intSoketRtc(this.HOST);
   },
   },
   destroyed() {
   destroyed() {
@@ -236,6 +277,8 @@ body {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
   overflow: hidden;
   overflow: hidden;
+  min-width: 1000px;
+  min-height: 900px;
 }
 }
 
 
 video {
 video {
@@ -245,11 +288,37 @@ video {
 }
 }
 .marke {
 .marke {
   position: fixed;
   position: fixed;
-  bottom: 0;
-  left: 0;
+  top: 0;
+  left: 50%;
+  width: 555px;
   height: 30px;
   height: 30px;
+  transform: translate(-50%, 0);
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.marke::before {
+  position: absolute;
+  z-index: 0;
+  content: "";
   width: 100%;
   width: 100%;
-  background: rgba(0, 0, 0, 0.3);
+  height: 0;
+  border-top: 30px solid rgba(0, 0, 0, 0.25);
+  border-left: 15px solid transparent;
+  border-right: 15px solid transparent;
+}
+.marke > div {
+  margin: 0 3px;
+}
+.gauge {
+  position: fixed;
+  bottom: 0;
+  left: 50%;
+  transform: translate(-50%, 0);
+  width: 500px;
+  height: 185px;
+  z-index: 9;
 }
 }
 /* 隐藏滚动条 */
 /* 隐藏滚动条 */
 ::-webkit-scrollbar {
 ::-webkit-scrollbar {

+ 2 - 0
src/assets/ct.svg

@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1660212107424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4774" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
+</style></defs><path d="M817.68 803.17a130.23 130.23 0 0 1-125.6-96.37l-14-52.19a54.08 54.08 0 0 0-52.16-40H398.07a54.08 54.08 0 0 0-52.16 40l-14 52.19c-15.54 58-68.21 96.37-125.6 96.37A130 130 0 0 1 80.78 639.51l66.72-249a181.66 181.66 0 0 1 63.19-97.15A177.79 177.79 0 0 1 322 254.58h380a177.79 177.79 0 0 1 111.31 38.79 181.66 181.66 0 0 1 63.19 97.15l66.72 249a130 130 0 0 1-125.54 163.65zM322 274.58A160 160 0 0 0 166.87 395.5v0.13l-66.73 249a110 110 0 0 0 212.5 56.94l14-52.19a74.11 74.11 0 0 1 71.48-54.85h227.81a74.11 74.11 0 0 1 71.48 54.85l14 52.19a110 110 0 0 0 212.5-56.94L857.13 395.5A160 160 0 0 0 702 274.58z" fill="#26263D" p-id="4775"></path><path d="M580 213.86a12 12 0 0 1 12 12v28H432v-28a12 12 0 0 1 12-12h136m0-20H444a32 32 0 0 0-32 32v48h200v-48a32 32 0 0 0-32-32z" fill="#26263D" p-id="4776"></path><path d="M512 213.86a10 10 0 0 1-10-10v-63a60.07 60.07 0 0 1 60-60 10 10 0 0 1 0 20 40 40 0 0 0-40 40v63a10 10 0 0 1-10 10zM330 344.86a90 90 0 1 1-90 90 90.1 90.1 0 0 1 90-90m0-20a110 110 0 1 0 110 110 110 110 0 0 0-110-110z" fill="#26263D" p-id="4777"></path><path d="M330 384.86a50 50 0 1 1-50 50 50.06 50.06 0 0 1 50-50m0-20a70 70 0 1 0 70 70 70 70 0 0 0-70-70zM697 344.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM697 496.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM773 420.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34zM621 420.86a14 14 0 1 1-14 14 14 14 0 0 1 14-14m0-20a34 34 0 1 0 34 34 34 34 0 0 0-34-34z" fill="#26263D" p-id="4778"></path></svg>

+ 106 - 0
src/components/battery.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="electric-panel" :class="bgClass">
+    <div class="panel">
+      <div class="remainder" :style="{ width: quantity + '%' }" />
+    </div>
+    <div class="berText">50%</div>
+  </div>
+</template>
+
+<script>
+/**
+ * 电池电量Icon
+ */
+export default {
+  name: "ElectricQuantity",
+  props: ["quantity"],
+  computed: {
+    bgClass() {
+      if (this.quantity >= 50) {
+        return "success";
+      } else if (this.quantity >= 20) {
+        return "warning";
+      } else if (this.quantity >= 1) {
+        return "danger";
+      } else {
+        return "danger";
+      }
+    },
+  },
+};
+</script>
+
+<style scoped>
+.electric-panel {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+  z-index: 3;
+  color: white;
+}
+
+.panel {
+  box-sizing: border-box;
+  width: 22px;
+  height: 14px;
+  position: relative;
+  border: 2px solid #ccc;
+  padding: 1px;
+  border-radius: 3px;
+  margin-right: 5px;
+  transform: rotate(-90deg);
+}
+
+.panel::before {
+  content: "";
+  border-radius: 0 1px 1px 0;
+  height: 6px;
+  background: #ccc;
+  width: 3px;
+  position: absolute;
+  top: 50%;
+  right: -4px;
+  transform: translateY(-50%);
+}
+
+.panel .remainder {
+  border-radius: 1px;
+  position: relative;
+  height: 100%;
+  width: 0%;
+  left: 0;
+  top: 0;
+  background: #fff;
+}
+
+.success .panel {
+  border-color: #40d7c1;
+}
+.success .panel:before {
+  background: #40d7c1;
+}
+.success .remainder {
+  background: #40d7c1;
+}
+
+.warning .panel {
+  border-color: #f90;
+}
+.warning .panel:before {
+  background: #f90;
+}
+.warning .remainder {
+  background: #f90;
+}
+
+.danger .panel {
+  border-color: #ed4014;
+}
+.danger .panel:before {
+  background: #ed4014;
+}
+.danger .remainder {
+  background: #ed4014;
+}
+</style>

+ 109 - 0
src/components/gauge.vue

@@ -0,0 +1,109 @@
+<template>
+  <div id="charts"></div>
+</template>
+<script>
+import * as echarts from "echarts/core";
+import { GaugeChart } from "echarts/charts";
+import { LabelLayout, UniversalTransition } from "echarts/features";
+import { CanvasRenderer } from "echarts/renderers";
+echarts.use([GaugeChart, LabelLayout, UniversalTransition, CanvasRenderer]);
+export default {
+  props: ["value", "gears"],
+  data() {
+    return {
+      mychart: null,
+      option: {
+        series: {
+          name: "Pressure",
+          type: "gauge",
+          itemStyle: {
+            color: "#FFFFFF",
+          },
+          startAngle: 180,
+          max: 60,
+          endAngle: 0,
+          axisLine: {
+            lineStyle: {
+              color: "#FFFFFF",
+              width: 1,
+            },
+          },
+          axisTick: {
+            distance: 0,
+            length: 10,
+            lineStyle: {
+              color: "#FFFFFF",
+            },
+          },
+          splitLine: {
+            length: 15,
+            distance: 0,
+            lineStyle: {
+              color: "#FFFFFF",
+            },
+          },
+          axisLabel: {
+            distance: 8,
+            color: "#FFFFFF",
+          },
+          progress: {
+            show: true,
+          },
+          radius: "160%",
+          center: ["50%", "90%"],
+          detail: {
+            offsetCenter: [0, -25],
+            valueAnimation: true,
+            formatter: (value) => {
+              return `{value|${value.toFixed(0)}}{unit|km/h}\n{num|${
+                this.gears
+              }}`;
+            },
+            rich: {
+              value: {
+                fontSize: 20,
+                fontWeight: "bolder",
+                color: "#FFFFFF",
+              },
+              unit: {
+                fontSize: 20,
+                color: "#FFFFFF",
+                padding: [0, 0, 0, 10],
+              },
+              num: {
+                fontSize: 20,
+                color: "#FFFFFF",
+                padding: [0, 0, 0, 10],
+              },
+            },
+          },
+          pointer: {
+            show: false,
+          },
+          data: [50],
+        },
+      },
+    };
+  },
+  mounted() {
+    this.mychart = echarts.init(document.getElementById("charts"));
+    this.mychart.setOption(this.option);
+  },
+  watch: {
+    value(v) {
+      if (!this.mychart) return;
+      if (v >= 100) v = 100;
+      if (v <= 0) v = 0;
+      this.option.series.data[0] = v;
+      this.mychart.setOption(this.option);
+      console.log(1, v);
+    },
+  },
+};
+</script>
+<style scoped>
+#charts {
+  width: 500px;
+  height: 200px;
+}
+</style>

+ 80 - 0
src/components/record.vue

@@ -0,0 +1,80 @@
+<template>
+  <div style="width:23px;height:23px;position:relative;z-index: 3;">
+    <svg
+      viewBox="0 50 1024 1024"
+      version="1.1"
+      xmlns="http://www.w3.org/2000/svg"
+      width="25"
+      height="25"
+    >
+      <path
+        d="M801.728 364.8a32 32 0 0 0-32 32v91.392c0 129.28-115.648 234.432-257.728 234.432S254.272 617.408 254.272 488.192V393.216a32 32 0 0 0-64 0v94.976c0 157.888 133.248 286.208 300.672 296.448v99.392H357.632c-16.128 0-29.184 14.336-29.184 32.064 0 17.664 13.056 31.936 29.184 31.936h319.04c16.064 0 29.184-14.272 29.184-31.936 0-17.728-13.12-32.064-29.184-32.064H554.944v-101.376c156.992-19.776 278.784-143.488 278.784-294.464V396.8c0-17.728-14.272-32-32-32z"
+        :fill="show ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
+      ></path>
+      <path
+        d="M517.12 678.656a199.104 199.104 0 0 0 198.912-198.848V268.736A199.168 199.168 0 0 0 517.12 69.888a199.04 199.04 0 0 0-198.784 198.848v211.072a199.04 199.04 0 0 0 198.784 198.848z m85.056-126.784a49.856 49.856 0 1 1 0-99.648 49.856 49.856 0 0 1 0 99.648zM382.336 268.736c0-74.368 60.48-134.848 134.784-134.848a135.04 135.04 0 0 1 134.912 134.848v28.48H382.336v-28.48z"
+        :fill="show ? '#00CED1' : 'rgba(0, 0, 0, 0.3)'"
+      ></path>
+    </svg>
+  </div>
+</template>
+<script>
+import { Message } from "view-design";
+export default {
+  data() {
+    return {
+      chunks: [],
+      mediaRecorder: null,
+      show: false, // true 开启,false 停止
+    };
+  },
+  methods: {
+    // 初始化音频
+    async initRecorder() {
+      try {
+        const stream = await navigator.mediaDevices.getUserMedia({
+          audio: true,
+        });
+        this.mediaRecorder = new MediaRecorder(stream);
+        // 事件监听
+        this.mediaRecorder.ondataavailable = (e) => {
+          this.chunks.push(e.data);
+        };
+        this.mediaRecorder.onstart = () => {
+          this.chunks = [];
+        };
+        this.mediaRecorder.onstop = () => {
+          this.show = false;
+          const blob = new Blob(this.chunks, {
+            type: "audio/webm;codecs=opus",
+          });
+          this.$emit("callBack", blob);
+        };
+      } catch (error) {
+        this.show = false;
+        Message.error({
+          content: "不支持音频输入",
+        });
+      }
+    },
+    // 录音
+    async audioCapture() {
+      this.show = true;
+      this.mediaRecorder.start();
+    },
+    // blob2AudioBuffer
+    blob2audioBuffer(blob) {
+      const reader = new FileReader();
+      reader.onload = function () {
+        console.log(123, this.result);
+        const audioCtx = new AudioContext();
+        audioCtx.decodeAudioData(this.result, function (audioBuffer) {
+          // AudioBuffer
+          console.log(audioBuffer);
+        });
+      };
+      reader.readAsArrayBuffer(blob);
+    },
+  },
+};
+</script>

+ 128 - 0
src/components/signal.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="signal-box">
+    <ul v-if="signalValue">
+      <li v-for="(item, idex) in list" :key="idex" :class="item.class"></li>
+    </ul>
+    <span v-if="signalText" style="color: white">{{ signalText }}</span>
+  </div>
+</template>
+ 
+<script>
+export default {
+  name: "SignalTower",
+  props: ["signalValue", "signalText"],
+  data() {
+    return {
+      list: [
+        {
+          id: 1,
+          class: "signal-default",
+        },
+        {
+          id: 2,
+          class: "signal-default",
+        },
+        {
+          id: 3,
+          class: "signal-default",
+        },
+        {
+          id: 4,
+          class: "signal-default",
+        },
+        {
+          id: 5,
+          class: "signal-default",
+        },
+      ],
+    };
+  },
+  watch: {
+    signalValue(v) {
+      console.log(111, v);
+      for (let j = 0; j < this.list.length; j++) {
+        const el = this.list[j];
+        if (el.id <= v) {
+          if (v === 1 || v === 2) {
+            el.class = "signal-red";
+          } else if (v === 3 || v === 4) {
+            el.class = "signal-yellow";
+          } else if (v === 5) {
+            el.class = "signal-green";
+          } else {
+            el.class = "signal-default";
+          }
+        } else {
+          el.class = "signal-default";
+        }
+      }
+    },
+  },
+};
+</script>
+ 
+<style scoped>
+.signal-box {
+  display: flex;
+  align-items: flex-start;
+  justify-content: center;
+  height: 23px;
+  width: 23px;
+  position: relative;
+  z-index: 3;
+}
+span {
+  font-size: 15px;
+  color: white;
+  margin-left: 3px;
+}
+ul {
+  height: 21px;
+  margin: 0;
+  padding: 0;
+  display: flex;
+  align-items: flex-end;
+}
+
+li {
+  width: 4px;
+  height: 5px;
+  border-radius: 10px;
+  list-style: none;
+  margin: 0 0.5px;
+}
+
+ul li:nth-child(1) {
+  height: 5px;
+}
+
+ul li:nth-child(2) {
+  height: 9px;
+}
+
+ul li:nth-child(3) {
+  height: 13px;
+}
+ul li:nth-child(4) {
+  height: 17px;
+}
+ul li:nth-child(5) {
+  height: 21px;
+}
+
+.signal-default {
+  background: rgba(0, 0, 0, 0.3);
+}
+
+.signal-red {
+  background-color: red;
+}
+
+.signal-yellow {
+  background-color: #e7d055;
+}
+
+.signal-green {
+  background-color: #32cd32;
+}
+</style>