Browse Source

Signed-off-by: caner <5658514@qq.com>

caner 3 years ago
parent
commit
aa41a49999

+ 1 - 0
package.json

@@ -9,6 +9,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "echarts": "^5.4.0",
     "vue": "^3.2.37"
   },
   "devDependencies": {

+ 414 - 1
src/App.vue

@@ -1,11 +1,424 @@
 <template>
-  <div>test</div>
+  <div
+    v-if="isLogin"
+    class="box"
+  >
+    <video
+      id="v2"
+      autoplay
+      playsinline
+      muted
+    />
+    <div class="maker">
+      <!-- 信号 -->
+      <Signal :signal="signal" />
+      <!-- 手柄 -->
+      <div class="contrl">
+        &#xe72a;
+      </div>
+      <!-- 音频 -->
+      <div class="audio">
+        &#xe6c1;
+      </div>
+      <!-- 喇叭 -->
+      <div class="arcode">
+        &#xe600;
+      </div>
+      <!-- 电量 -->
+      <Battery :quantity="60" />
+    </div>
+    <div class="gauge">
+      <Gauge
+        :value="gauge.value"
+        :gears="gauge.gears"
+      />
+    </div>
+    <Loading v-if="showLoading" />
+  </div>
+  <Login
+    v-else
+    :err="err"
+    @loginBack="login"
+  />
 </template>
 
 <script setup lang='ts'>
+import {
+  onMounted,
+  onUnmounted, provide, reactive, ref, watch
+} from 'vue'
+import Login from '@/components/login.vue'
+import Loading from '@/components/loading.vue'
+import Gauge from '@/components/gauge.vue'
+import Signal from '@/components/signal.vue'
+import Battery from '@/components/battery.vue'
 
+const isLogin = ref(true)
+const err = ref('')
+const showLoading = ref(false)
+const gauge = reactive({
+  value: 40,
+  gears: 1 // 1低速档 | 2 高速档
+})
+const signal = ref(2)
+const audio = reactive({
+  state: false,
+  muted: false,
+  warn: false,
+  Recorder: null as any,
+  chunks: [] as any
+})
+const webRtc: any = {
+  Peer: null,
+  remoteVideo: null,
+  remoteAudioTrak: null,
+  iceServers: [
+    {
+      urls: [ 'stun:caner.top:3478' ]
+    },
+    {
+      urls: 'turn:caner.top:3478',
+      username: 'admin',
+      credential: '123456'
+    }
+  ]
+}
+const conctrl = {
+  state: false,
+  gear: 1,
+  conNumber: 0
+}
+const HOST = 'ws://127.0.0.1:49800'
+let socket = null as any
+const sleep = (ms: number) => new Promise((res) => setTimeout(res, ms))
+let num = 0
+
+// 挡位
+const Gear = (gear: number, speed: number) => {
+  // 低速档
+  if (gear === 1) {
+    if (speed < 116) {
+      // 前
+      speed = 120
+    } else if (speed >= 120 && speed <= 131) {
+      speed = 128
+    } else if (speed > 140) {
+      // 后
+      speed = 140
+    }
+  }
+  // 高速档
+  if (gear === 2) {
+    if (speed < 96) {
+      speed = 96
+    } else if (speed >= 120 && speed <= 131) {
+      speed = 128
+    } else if (speed > 160) {
+      speed = 160
+    }
+  }
+  return speed
+}
+
+// 手柄数据
+const ControlData = () => {
+  const data = navigator.getGamepads()
+  const db = data[0]
+  if (!db) return
+  // 挡位选择AB
+  if (db.buttons[1].touched) conctrl.gear = 2
+  if (db.buttons[0].touched) conctrl.gear = 1
+  // 语音按键R2
+  audio.state = db.buttons[7].touched
+  // 静音X3
+  audio.muted = db.buttons[3].touched
+  // 播放警笛Y2
+  audio.warn = db.buttons[2].touched
+  // console.log(db.buttons);
+  const params = {
+    v0: Math.floor(db.axes[0] * 128 + 128),
+    v1: Math.floor(db.axes[1] * 128 + 128),
+    v2: Math.floor(db.axes[2] * 128 + 128),
+    v3: Gear(conctrl.gear, Math.floor(db.axes[3] * 128 + 128))
+  }
+
+  if (socket) socket.send({ type: 'conctrl', conctrl: params })
+  conctrl.conNumber = requestAnimationFrame(ControlData)
+}
+
+// 手柄连接
+const conControl = () => {
+  conctrl.state = true
+  ControlData()
+}
+
+// 手柄断开连接
+const disControl = () => {
+  conctrl.state = false
+  cancelAnimationFrame(conctrl.conNumber)
+}
+
+// 关闭
+const close = (error: string) => {
+  if (webRtc.Peer) webRtc.Peer.close()
+  if (webRtc.remoteVideo) webRtc.remoteVideo.srcObject = null
+  socket.close()
+  isLogin.value = false
+  showLoading.value = false
+  err.value = error || ''
+  socket = null
+  webRtc.Peer = null
+  window.removeEventListener('gamepadconnected', conControl)
+  window.removeEventListener('gamepaddisconnected', disControl)
+}
+
+// socket连接
+const onOpen = () => {
+  console.log('连接成功!')
+  try {
+    isLogin.value = true
+    // init webrtc
+    webRtc.Peer = new RTCPeerConnection({
+      iceServers: webRtc.iceServers,
+      bundlePolicy: 'max-bundle'
+    })
+
+    // listen state
+    webRtc.Peer.onicegatheringstatechange = () => {
+      console.log('GatheringState: ', webRtc.Peer.iceGatheringState)
+      if (webRtc.Peer.iceGatheringState === 'complete') {
+        const answer = webRtc.Peer.localDescription
+        socket.send(answer)
+      }
+    }
+    // listen track
+    webRtc.Peer.ontrack = async (evt: any) => {
+      console.log('track', evt)
+      webRtc.remoteVideo = document.getElementById('v2')
+      webRtc.remoteVideo.srcObject = { ...evt.streams[0] }
+      if (evt.track.kind === 'audio') webRtc.remoteAudioTrak = { ...evt.streams[0] }
+    }
+    // listen changestate
+    webRtc.Peer.oniceconnectionstatechange = async () => {
+      const state = webRtc.Peer.iceConnectionState
+      console.log('ICE状态', state)
+      if (
+        state === 'failed'
+        || state === 'disconnected'
+        || state === 'closed'
+      ) {
+        close('P2P通信失败')
+      }
+
+      // ICE连接成功|初始化摇杆
+      if (state === 'connected') {
+        // init Control
+        window.addEventListener('gamepadconnected', conControl)
+        window.addEventListener('gamepaddisconnected', disControl)
+        await sleep(3000)
+        showLoading.value = false
+      }
+    }
+  } catch (error) {
+    socket.close()
+    isLogin.value = false
+    err.value = 'webrtc初始化错误'
+  }
+}
+
+// socket信息
+const onMsg = (event: { data: string }) => {
+  console.log(`收到消息啦:${event.data}`)
+}
+
+// socket 连接错误
+const onErr = async () => {
+  isLogin.value = false
+  err.value = '连接错误!'
+  await sleep(3000)
+  if (!socket.userInfo) return
+  err.value = '重连中...'
+  const { roomID, name } = socket.userInfo
+  socket = new WebSocket(`${HOST}/${roomID}/${name}`)
+  socket.onmessage = onMsg
+  socket.onopen = onOpen
+  socket.userInfo = { roomID, name }
+}
+
+// 登录
+const login = (params: { name: string; roomID: string }) => {
+  err.value = '连接中...'
+  if (socket) { socket.close(); socket = null; return }
+  socket = new WebSocket(`${HOST}/${params.roomID}/${params.name}`)
+  socket.onmessage = onMsg
+  socket.onopen = onOpen
+  socket.onerror = onErr
+  socket.userInfo = params
+}
+
+// 发送音频
+const sendAudio = (blob: Blob) => {
+  console.log('发送', blob)
+  if (!socket && !socket.connected) return
+  socket.send({
+    type: 'Meadia',
+    Meadia: blob
+  })
+}
+
+// 初始化声音
+const initRecorder = async () => {
+  try {
+    const stream = await navigator.mediaDevices.getUserMedia({
+      audio: true
+    })
+    audio.Recorder = new MediaRecorder(stream)
+
+    // 事件监听
+    audio.Recorder.ondataavailable = (e: { data: any }) => {
+      audio.chunks.push(e.data)
+    }
+    audio.Recorder.onstart = () => {
+      audio.chunks = []
+    }
+    audio.Recorder.onstop = () => {
+      const blob = new Blob(audio.chunks, {
+        type: 'audio/webm;codecs=opus'
+      })
+      sendAudio(blob)
+    }
+  } catch (error) {
+    err.value = '不支持webrtc音频'
+  }
+}
+
+provide('err', err)
+
+// 静音
+watch(() => audio.muted, (v) => {
+  if (v) {
+    num++
+    const state = !(num % 2)
+    audio.state = state
+    if (socket && socket.connected) socket.send(JSON.stringify({ type: 'contrlAudio', contrlAudio: state }))
+  }
+})
+
+// 鸣笛
+watch(() => audio.warn, (v) => {
+  if (v && socket && socket.connected) socket.send(JSON.stringify({ type: 'warnAudio', warnAudio: v }))
+})
+
+// 发送音频
+watch(() => audio.state, (v) => {
+  if (!audio.Recorder) return
+  if (v) {
+    audio.Recorder.start()
+  } else {
+    audio.Recorder.stop()
+  }
+})
+
+onMounted(() => {
+  initRecorder()
+})
+
+onUnmounted(() => {
+  if (socket) socket.close()
+  if (audio.Recorder) audio.Recorder = null
+  audio.chunks = []
+  socket = null
+})
 </script>
+<style lang="less" scoped>
+.box {
+  video {
+    width: 100%;
+    height: 100%;
+    background: none;
+    object-fit: fill;
+    font-size: 0;
+  }
 
+  .maker {
+    position: fixed;
+    top: 0;
+    left: 50%;
+    width: 555px;
+    height: 30px;
+    transform: translate(-50%, 0);
+    z-index: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    &::before {
+      position: absolute;
+      z-index: 0;
+      content: "";
+      width: 100%;
+      height: 0;
+      border-top: 30px solid rgba(0, 0, 0, 0.25);
+      border-left: 15px solid transparent;
+      border-right: 15px solid transparent;
+    }
+
+    &>div {
+      margin: 0 3px;
+    }
+
+    .audio,
+    .arcode {
+      font-size: 24px;
+      color: rgba(0, 0, 0, 0.3);
+    }
+
+    .contrl {
+      font-size: 32px;
+      color: rgba(0, 0, 0, 0.3);
+    }
+  }
+
+  .gauge {
+    position: fixed;
+    bottom: 0;
+    left: 50%;
+    transform: translate(-50%, 0);
+    width: 500px;
+    height: 185px;
+    z-index: 9;
+  }
+}
+</style>
 <style>
+html,
+body,
+#app {
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  user-select: none;
+  min-width: 1000px;
+  min-height: 900px;
+  /* background: black; */
+}
+
+#app {
+  font-family: "fonts";
+  font-style: normal;
+}
+
+/* 隐藏滚动条 */
+::-webkit-scrollbar {
+  width: 0 !important;
+  display: none;
+}
 
+@font-face {
+  font-family: "fonts";
+  src: url("./assets/iconfont.woff2") format("woff2"),
+    url("./assets/iconfont.woff") format("woff"),
+    url("./assets/iconfont.ttf") format("truetype");
+}
 </style>

BIN
src/assets/iconfont.ttf


BIN
src/assets/iconfont.woff


BIN
src/assets/iconfont.woff2


+ 27 - 24
src/components/battery.vue

@@ -1,33 +1,36 @@
 <template>
-  <div class="electric-panel" :class="bgClass">
+  <div
+    class="electric-panel"
+    :class="bgClass"
+  >
     <div class="panel">
-      <div class="remainder" :style="{ width: quantity + '%' }" />
+      <div
+        class="remainder"
+        :style="{ width: quantity + '%' }"
+      />
+    </div>
+    <div class="berText">
+      50%
     </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 lang="ts" setup>
+import { computed } from 'vue'
+
+const props = defineProps<{
+  quantity:number
+}>()
+const bgClass = computed(() => {
+  if (props.quantity >= 50) {
+    return 'success'
+  } if (props.quantity >= 20) {
+    return 'warning'
+  } if (props.quantity >= 1) {
+    return 'danger'
+  }
+  return 'danger'
+})
 </script>
 
 <style scoped>

+ 93 - 100
src/components/gauge.vue

@@ -1,107 +1,100 @@
 <template>
-  <div id="charts"></div>
+  <div id="charts" />
 </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: {
-              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: [0],
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import { nextTick, onBeforeUnmount, watch } from 'vue'
+
+const props = defineProps<{
+  gears: number,
+  value: number
+}>()
+
+let chart = null as any
+const option = {
+  series: {
+    name: 'Pressure',
+    type: 'gauge',
+    itemStyle: {
+      color: '#FFFFFF'
+    },
+    startAngle: 180,
+    max: 60,
+    endAngle: 0,
+    axisLine: {
+      lineStyle: {
+        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: (v:number) => `{value|${v.toFixed(0)}}{unit|km/h}\n{num|${props.gears}}`,
+      rich: {
+        value: {
+          fontSize: 20,
+          fontWeight: 'bolder',
+          color: '#FFFFFF'
+        },
+        unit: {
+          fontSize: 20,
+          color: '#FFFFFF',
+          padding: [ 0, 0, 0, 10 ]
         },
-      },
-    };
-  },
-  mounted() {
-    this.mychart = echarts.init(document.getElementById("charts"));
-    this.mychart.setOption(this.option);
-  },
-  watch: {
-    value(v) {
-      if (!this.mychart) return;
-      if (v >= 60) v = 60;
-      if (v <= 0) v = 0;
-      this.option.series.data[0] = v;
-      this.mychart.setOption(this.option);
+        num: {
+          fontSize: 20,
+          color: '#FFFFFF',
+          padding: [ 0, 0, 0, 10 ]
+        }
+      }
     },
-    gears(v){
-      if (!this.mychart) return;
-      this.mychart.setOption(this.option);
-    }
-  },
-};
+    pointer: {
+      show: false
+    },
+    data: [ props.value ]
+  }
+}
+
+nextTick(() => {
+  const dom = document.getElementById('charts') as HTMLElement
+  chart = echarts.init(dom)
+  chart.setOption(option)
+})
+
+watch(props, (v) => {
+  if (!chart) return
+  option.series.data[0] = v.value
+  chart.setOption(option)
+}, { deep: true })
+
+onBeforeUnmount(() => {
+  if (!chart) return
+  chart.dispose()
+  chart = null
+})
 </script>
 <style scoped>
 #charts {

+ 5 - 5
src/components/loading.vue

@@ -1,11 +1,11 @@
 <template>
   <div class="loading">
     <figure>
-      <div class="dot white"></div>
-      <div class="dot"></div>
-      <div class="dot"></div>
-      <div class="dot"></div>
-      <div class="dot"></div>
+      <div class="dot white" />
+      <div class="dot" />
+      <div class="dot" />
+      <div class="dot" />
+      <div class="dot" />
     </figure>
   </div>
 </template>

+ 90 - 81
src/components/login.vue

@@ -1,45 +1,49 @@
 <template>
   <div class="login">
     <div>
-      <div class="logo">登陆</div>
-      <input type="text" placeholder="房间" maxlength="20" v-model="roomID" />
-      <input type="text" placeholder="昵称" maxlength="20" v-model="name" />
-      <div class="err">{{ err || error }}</div>
-      <button @click="login">加入</button>
+      <div class="logo">
+        Lgoin
+      </div>
+      <input
+        v-model="ID"
+        type="text"
+        placeholder="房间"
+        maxlength="20"
+      >
+      <input
+        v-model="name"
+        type="text"
+        placeholder="昵称"
+        maxlength="20"
+      >
+      <div class="err">
+        {{ err || error }}
+      </div>
+      <button @click="verification">
+        加入
+      </button>
     </div>
   </div>
 </template>
-<script>
-export default {
-  props: {
-    err: {
-      type: String,
-      default: () => {
-        return "";
-      },
-    },
-  },
-  data() {
-    return {
-      name: "",
-      roomID: "",
-      error: ""
-    };
-  },
-  methods: {
-    async login() {
-      if(!this.roomID){
-        this.error = "请输入房间号";
-      }else if(!this.name){
-        this.error = "请输入昵称";
-      }else{
-        this.$emit("loginBack", { name: this.name, roomID: this.roomID });
-      }
-    },
-  },
-};
+<script setup lang='ts'>
+import { inject, ref } from 'vue'
+const err = inject('err')
+const ID = ref('')
+const name = ref('')
+const error = ref()
+const callBack = defineEmits([ 'loginBack' ])
+
+const verification = () => {
+  if (!ID.value) {
+    error.value = '请输入房间号'
+  } else if (!name.value) {
+    error.value = '请输入昵称'
+  } else {
+    callBack('loginBack', { name: name.value, roomID: ID.value })
+  }
+}
 </script>
-<style scoped>
+<style scoped lang="less">
 .login {
   width: 100%;
   height: 100%;
@@ -47,52 +51,57 @@ export default {
   display: flex;
   justify-content: center;
   align-items: center;
-}
-.logo {
-  width: 100px;
-  height: 50px;
-  margin: 0 auto;
-  text-align: center;
-}
-input {
-  display: block;
-  border: 0;
-  border-bottom: solid 1px #ccc;
-  text-indent: 10px;
-  margin: 0 auto;
-  margin-bottom: 20px;
-  outline: none;
-  border-radius: 0;
-  background: none;
-}
 
-button {
-  display: block;
-  width: 130px;
-  height: 30px;
-  background: #79b8fa;
-  border: none;
-  color: white;
-  font-size: 17px;
-  font-weight: 500;
-  border-radius: 5px;
-  cursor: pointer;
-  margin: 0 auto;
-  margin-top: 20px;
-  line-height: 30px;
-}
-button:hover {
-  background: #2d8cf0;
-}
-.err {
-  width: 175px;
-  margin: 0 auto;
-  font-size: 12px;
-  color: red;
-  text-align: left;
-  text-indent: 10px;
-}
-input:nth-child(3) {
-  margin-bottom: 5px;
+  .logo {
+    width: 100px;
+    height: 50px;
+    margin: 0 auto;
+    text-align: center;
+  }
+
+  input {
+    display: block;
+    border: 0;
+    border-bottom: solid 1px #ccc;
+    text-indent: 10px;
+    margin: 0 auto;
+    margin-bottom: 20px;
+    outline: none;
+    border-radius: 0;
+    background: none;
+  }
+
+  button {
+    display: block;
+    width: 130px;
+    height: 30px;
+    background: #79b8fa;
+    border: none;
+    color: white;
+    font-size: 17px;
+    font-weight: 500;
+    border-radius: 5px;
+    cursor: pointer;
+    margin: 0 auto;
+    margin-top: 20px;
+    line-height: 30px;
+  }
+
+  button:hover {
+    background: #2d8cf0;
+  }
+
+  .err {
+    width: 175px;
+    margin: 0 auto;
+    font-size: 12px;
+    color: red;
+    text-align: left;
+    text-indent: 10px;
+  }
+
+  input:nth-child(3) {
+    margin-bottom: 5px;
+  }
 }
 </style>

+ 0 - 90
src/components/record.vue

@@ -1,90 +0,0 @@
-<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 {
-  props: ["audioState"],
-  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 = () => {
-          const blob = new Blob(this.chunks, {
-            type: "audio/webm;codecs=opus",
-          });
-          this.$emit("callBack", blob);
-        };
-      } catch (error) {
-        this.show = false;
-        this.mediaRecorder = null;
-        let txt = "不支持的音频";
-        if (error.toString().includes("getUserMedia")) {
-          txt = "不支持webrtc音频";
-        } else {
-          txt = "未获取到音频设备";
-        }
-        Message.error({
-          content: txt,
-        });
-      }
-    },
-
-    // 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);
-    },
-  },
-  watch: {
-    audioState(v) {
-      if (!this.mediaRecorder) return;
-      if (v) {
-        this.mediaRecorder.start();
-      } else {
-        this.mediaRecorder.stop();
-      }
-      this.show = v;
-    },
-  },
-  mounted() {
-    this.initRecorder();
-  },
-};
-</script>

+ 55 - 54
src/components/signal.vue

@@ -1,65 +1,62 @@
 <template>
   <div class="signal-box">
-    <ul v-if="signalValue">
-      <li v-for="(item, idex) in list" :key="idex" :class="item.class"></li>
+    <ul>
+      <li
+        v-for="(item, idex) in list"
+        :key="idex"
+        :class="item.class"
+      />
     </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",
-        },
-      ],
-    };
+
+<script lang="ts" setup>import { reactive, watch } from 'vue'
+
+const props = defineProps<{
+  signal: number
+}>()
+const list: Array<{ class: string; id: number }> = reactive([
+  {
+    id: 1,
+    class: 'signal-default'
   },
-  watch: {
-    signalValue(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";
-        }
-      }
-    },
+  {
+    id: 2,
+    class: 'signal-default'
   },
-};
+  {
+    id: 3,
+    class: 'signal-default'
+  },
+  {
+    id: 4,
+    class: 'signal-default'
+  },
+  {
+    id: 5,
+    class: 'signal-default'
+  }
+])
+watch(props, (v) => {
+  for (let j = 0; j < list.length; j++) {
+    const el = list[j]
+    if (el.id <= v.signal) {
+      if (v.signal === 1 || v.signal === 2) {
+        el.class = 'signal-red'
+      } else if (v.signal === 3 || v.signal === 4) {
+        el.class = 'signal-yellow'
+      } else if (v.signal === 5) {
+        el.class = 'signal-green'
+      } else {
+        el.class = 'signal-default'
+      }
+    } else {
+      el.class = 'signal-default'
+    }
+  }
+}, { deep: true, immediate: true })
 </script>
- 
+
 <style scoped>
 .signal-box {
   display: flex;
@@ -70,11 +67,13 @@ export default {
   position: relative;
   z-index: 3;
 }
+
 span {
   font-size: 15px;
   color: white;
   margin-left: 3px;
 }
+
 ul {
   height: 21px;
   margin: 0;
@@ -102,9 +101,11 @@ ul li:nth-child(2) {
 ul li:nth-child(3) {
   height: 13px;
 }
+
 ul li:nth-child(4) {
   height: 17px;
 }
+
 ul li:nth-child(5) {
   height: 21px;
 }