Ver código fonte

refactor: gateway style video camera

fenghao 3 anos atrás
pai
commit
f78f22e322

BIN
src/assets/map.png


+ 0 - 1
src/views/device/back/components/Video.vue

@@ -11,7 +11,6 @@
       class="o-video__player o-simple-video"
       muted
       autoplay
-      controls
       :poster="poster"
     />
     <div

+ 31 - 11
src/views/device/detail/components/Gateway/Box.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="box">
+    <div class="box__topborder" />
     <div class="box-header l-flex--row center">
       <div class="title">{{ title }}</div>
       <div
@@ -41,21 +42,14 @@ export default {
 
 <style lang="scss" scoped>
 .box {
+  position: relative;
   --mt: 8px;
   height: 100%;
   width: 100%;
   padding: 0 5px;
-  border: 1px solid;
-  border-image: linear-gradient(
-      257deg,
-      rgba(41, 86, 238, 1),
-      rgba(33, 40, 63, 1),
-      rgba(41, 86, 238, 1),
-      rgba(41, 86, 238, 1),
-      rgba(33, 40, 63, 1),
-      rgba(41, 86, 238, 1)
-    )
-    1 1;
+  border-top: 1px solid #2956f0;
+
+  border-bottom: 1px solid #2956f0;
   clip-path: polygon(
     0 var(--mt),
     calc(50% - 80px) var(--mt),
@@ -95,4 +89,30 @@ export default {
     height: calc(100% - 30px);
   }
 }
+.box__topborder {
+  position: absolute;
+  left: 0;
+  right: 0;
+  height: 9px;
+  top: -1px;
+  background: linear-gradient(
+    270deg,
+    rgb(41, 86, 238),
+    rgb(33, 40, 63),
+    transparent calc(50% - 80px),
+    transparent calc(50% + 80px),
+    rgb(33, 40, 63),
+    rgb(41, 86, 238)
+  );
+  clip-path: polygon(
+    0 8px,
+    calc(50% - 80px) 8px,
+    calc(50% - 50px) 0,
+    calc(50% + 50px) 0,
+    calc(50% + 80px) 8px,
+    100% 8px,
+    100% 100%,
+    0 100%
+  );
+}
 </style>

+ 12 - 0
src/views/device/detail/components/Gateway/DeviceAlarm.vue

@@ -255,6 +255,18 @@ export default {
       color: #ffffff;
     }
   }
+  ::v-deep{
+    .el-tag.el-tag--danger{
+      background-color:#F73F3F;
+      color: #fff;
+      border-color: transparent;
+    }
+    .el-tag.el-tag--success{
+            background-color:#00C787;
+      color: #fff;
+      border-color: transparent;
+    }
+  }
 }
 .full{
   .warp{

+ 104 - 6
src/views/device/detail/components/Gateway/DeviceInfo.vue

@@ -14,29 +14,42 @@
       <el-button
         class="o-button"
         icon="el-icon-switch-button"
+        @click="onSwitch(true)"
       >开机</el-button>
       <el-button
         class="o-button"
         icon="el-icon-switch-button"
+        @click="onSwitch(false)"
       >关机</el-button>
       <el-button
+        :loading="rebooting"
         class="o-button"
         icon="el-icon-refresh-right"
+        @click="invoke"
       >重启</el-button>
     </div>
   </div>
 </template>
 
 <script>
+import { publish } from '@/utils/mqtt'
+import { send } from '../../monitor'
 export default {
   props: {
     device: {
       type: Object,
       required: true
+    },
+    online: {
+      type: Boolean,
+      required: true
     }
   },
   data () {
     return {
+      rebooting: false,
+      openFunctionKey: 'bootDevice',
+      closeFunctionKey: 'shutdownDevice',
       items: [
         {
           label: '名称',
@@ -45,11 +58,90 @@ export default {
         {
           label: 'MAC',
           key: 'mac'
+        },
+        {
+          label: '序列号',
+          key: 'serialNumber'
+        },
+        {
+          label: '分辨率',
+          key: 'resolutionRatio'
         }
       ]
     }
   },
+  watch: {
+    online () {
+      this.rebooting = false
+    }
+  },
   methods: {
+    onSwitch (open) {
+      if (!this.online) {
+        this.$message({
+          type: 'warning',
+          message: '设备未上线,请稍后再试'
+        })
+        return
+      }
+      this.$confirm(`立即${open ? '开机' : '关机'}?`, {
+        type: 'warning'
+      }).then(() => {
+        this.sendTopic(open ? this.openFunctionKey : this.closeFunctionKey)
+      })
+    },
+    invoke () {
+      if (this.rebooting) {
+        return
+      }
+      if (!this.online) {
+        this.$message({
+          type: 'warning',
+          message: '设备未上线,请稍后再试'
+        })
+        return
+      }
+      this.$confirm(`重启 ${this.device.name} ?`, { type: 'warning' }).then(
+        () => {
+          send('/restart/ask').then(
+            () => {
+              this.rebooting = true
+            },
+            () => {
+              this.$message({
+                type: 'warning',
+                message: '正在连接,请稍后再试'
+              })
+            }
+          )
+        }
+      )
+    },
+    sendTopic (invoke, inputs = []) {
+      console.log(invoke, inputs)
+      publish(
+        `${this.device.productId}/${this.deviceId}/function/invoke`,
+        JSON.stringify({
+          timestamp: Date.now(),
+          function: invoke,
+          inputs
+        }),
+        true
+      ).then(
+        () => {
+          this.$message({
+            type: 'success',
+            message: '执行成功'
+          })
+        },
+        () => {
+          this.$message({
+            type: 'warning',
+            message: '正在连接,请稍后再试'
+          })
+        }
+      )
+    }
   }
 }
 </script>
@@ -57,30 +149,36 @@ export default {
 <style lang="scss" scoped>
 .wrapper {
   width: 100%;
-  background-color: #232B45;
-   display: flex;
-    align-items: center;
+
+  display: flex;
+  align-items: center;
   .info {
     display: flex;
     align-items: center;
+     background-color: #232b45;
+     line-height: 40px;
+     margin-right: 16px;
     flex: 1;
     .item {
       display: flex;
       flex: 1;
+      align-items: center;
       .label {
         text-align: center;
         flex: 1;
-        color: #9EA9CD;
-
+        color: #9ea9cd;
       }
       .value {
         text-align: center;
         flex: 1;
-        color:#fff
+        color: #fff;
       }
     }
   }
   .buttons {
+    ::v-deep.el-button+.el-button{
+      margin-left: 16px;
+    }
   }
 }
 </style>

+ 7 - 4
src/views/device/detail/components/Gateway/Header.vue

@@ -57,10 +57,12 @@ export default {
   height: 50px;
   align-items: center;
   .left {
-    flex: 0 0 250px;
-    background: url("~@/assets/logo.png") 0 0/100% 100% no-repeat;
-    margin: 0 10px;
+    flex: 0 0 160px;
+    background: url("~@/assets/logo.png");
+    background-size: 142px 36px ;
     height: 100%;
+    background-repeat: no-repeat;
+        background-position: left center
   }
   .mid {
     flex: 1;
@@ -82,15 +84,16 @@ export default {
     color: #fff;
     display: flex;
     align-items: center;
-    margin: 0 10px;
     height: 100%;
     font-size: 18px;
+    justify-content: flex-end;
     .exit {
       margin-left: 20px;
       cursor: pointer;
       color: #2956f0;
       position: relative;
       top: -2px;
+      right: 6px;
     }
   }
 }

+ 69 - 45
src/views/device/detail/components/Gateway/LEDCamera.vue

@@ -1,72 +1,96 @@
 <template>
   <box
     componentkey="LEDCamera"
-    title="LED屏画面监测"
+    title="LED屏画面监测摄像头"
+    :bus="bus"
   >
-    <camera-player
-      :key="item.identifier"
-      :class="{ 'u-pointer': canClick && item.onlineStatus }"
-      :camera="item"
-      @click.native="onClick"
-    >
-      <div
-        class="o-video-footer l-flex--row"
-        @click.stop
-      >
-        <!-- <div class="l-flex__auto c-sibling-item u-ellipsis">
-          {{ item.name }}
-        </div> -->
-      </div> </camera-player></box>
+    <video
+      ref="video"
+      style="width: 100%; height: 100%"
+      :src="src"
+      autoplay
+      muted
+      loop
+      @timeupdate="timeupdate"
+      @canplay="canplay"
+    />
+  </box>
 </template>
 
 <script>
-import CameraPlayer from '@/components/external/camera/CameraPlayer'
-import { getCameras } from '@/api/external'
+import { getAssetUrl } from '@/api/asset'
 import Box from './Box'
+import Bus from './bus'
 export default {
   components: {
-    Box,
-    CameraPlayer
+    Box
   },
   props: {
-    device: {
-      type: Object,
-      required: true
+    full: {
+      type: Boolean,
+      default: false
+    },
+    currentTime: {
+      type: Number,
+      default: 0
     }
   },
   data () {
     return {
-      cameraType: 2,
-      item: {},
-      canClick: false
+      nodata: false,
+      propCurrentTime: 0,
+      bus: {},
+      src:
+        process.env.ENV === 'xuzhou'
+          ? getAssetUrl('material-draft/aff61eb0-3531-409b-80a2-b3f1354b56e4')
+          : 'https://msr.rondochina.com:6443/oss-api/material-draft/aff61eb0-3531-409b-80a2-b3f1354b56e4'
     }
   },
   created () {
-    this.getCameras()
+    if (!this.full) {
+      Bus.$on('getCurrentTime', currentTime => {
+        const video = this.$refs.video
+        video.currentTime = currentTime
+        video.play()
+      })
+    }
+  },
+
+  beforeDestroy () {
+    if (this.full) {
+      const video = this.$refs.video
+      Bus.$emit('getCurrentTime', video.currentTime)
+    }
   },
   methods: {
-    getCameras () {
-      const key = Date.now()
-      this.$key = key
-      getCameras({
-        deviceId: this.device.id,
-        cameraType: this.cameraType
-      })
-        .then(({ data }) => {
-          if (!data.length) { return }
-          if (key === this.$key) {
-            data = data.map(({ thirdPartyDevice }) => thirdPartyDevice)
-          }
-          this.item = data[0]// 取一个
-          console.log(this.item)
-        })
+    pause () {
+      const video = this.$refs.video
+      video.pause()
+    },
+    init () {
+      if (this.$init) {
+        return
+      }
+      console.log('init', this.currentTime)
+      const video = this.$refs.video
+      video.currentTime = this.currentTime
+      this.$init = true
+    },
+    timeupdate () {
+      const video = this.$refs.video
+      if (!video) {
+        return
+      }
+      this.bus = { currentTime: video.currentTime }
     },
-    onClick () {
-      if (!this.canClick) { return }
-      console.log('click')
+    canplay () {
+      console.log('canplay', this.full)
+      if (this.full) {
+        this.init()
+      }
     }
   }
 }
 </script>
 
-<style></style>
+<style lang="scss" scoped></style>

+ 72 - 0
src/views/device/detail/components/Gateway/LEDCameraReally.vue

@@ -0,0 +1,72 @@
+<template>
+  <box
+    componentkey="LEDCamera"
+    title="LED屏画面监测摄像头"
+  >
+    <camera-player
+      :key="item.identifier"
+      :class="{ 'u-pointer': canClick && item.onlineStatus }"
+      :camera="item"
+      @click.native="onClick"
+    >
+      <div
+        class="o-video-footer l-flex--row"
+        @click.stop
+      >
+        <!-- <div class="l-flex__auto c-sibling-item u-ellipsis">
+          {{ item.name }}
+        </div> -->
+      </div> </camera-player></box>
+</template>
+
+<script>
+import CameraPlayer from '@/components/external/camera/CameraPlayer'
+import { getCameras } from '@/api/external'
+import Box from './Box'
+export default {
+  components: {
+    Box,
+    CameraPlayer
+  },
+  props: {
+    device: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      cameraType: 2,
+      item: {},
+      canClick: false
+    }
+  },
+  created () {
+    this.getCameras()
+  },
+  methods: {
+    getCameras () {
+      const key = Date.now()
+      this.$key = key
+      getCameras({
+        deviceId: this.device.id,
+        cameraType: this.cameraType
+      })
+        .then(({ data }) => {
+          if (!data.length) { return }
+          if (key === this.$key) {
+            data = data.map(({ thirdPartyDevice }) => thirdPartyDevice)
+          }
+          this.item = data[0]// 取一个
+          console.log('first')
+        })
+    },
+    onClick () {
+      if (!this.canClick) { return }
+      console.log('click')
+    }
+  }
+}
+</script>
+
+<style></style>

+ 23 - 5
src/views/device/detail/components/Gateway/LinkState.vue

@@ -33,6 +33,18 @@
             {{ item.label }}
           </div>
         </div>
+        <div class="co led">
+          <video
+            ref="player"
+            class="o-video__player o-simple-video"
+            style="width:100%;height:100%"
+            muted
+            autoplay
+            controls
+            :poster="poster"
+          />
+          <div class="ctext">led大屏</div>
+        </div>
         <div
           v-for="line in lines"
           :key="line.key"
@@ -101,15 +113,17 @@
 </template>
 
 <script>
+import videoPoster from '@/assets/image_no_program.svg'
 import Box from './Box'
 import { getBoundThirdPartyDevices } from '@/api/external'
 import { ScreenshotCache } from '@/utils/cache'
+import videoback from './mixin/videoback'
 export default {
   name: 'LinkState',
   components: {
     Box
   },
-
+  mixins: [videoback],
   props: {
     device: {
       type: Object,
@@ -122,6 +136,7 @@ export default {
   },
   data () {
     return {
+      poster: videoPoster,
       scale: 1,
       items: [
         {
@@ -151,11 +166,11 @@ export default {
         {
           label: '接收卡',
           key: 'receive_card'
-        },
-        {
-          label: 'LED大屏',
-          key: 'led'
         }
+        // {
+        //   label: 'LED大屏',
+        //   key: 'led'
+        // }
       ],
       lines: Array.from({ length: 8 }, (v, index) => {
         // M4,0 L4,95
@@ -760,6 +775,9 @@ export default {
     }
   }
 }
+video::-webkit-media-controls {
+  display:none !important;
+}
 </style>
 <style lang="scss">
 $linkLengthArr: 22, 119, 92, 95, 122, 123, 150, 137, 450, 449, 494,78;

+ 36 - 4
src/views/device/detail/components/Gateway/Map.vue

@@ -1,8 +1,15 @@
 <template>
   <box
     componentkey="Map"
-    title="地图"
-  >??????</box>
+    title="地图位置"
+  >
+    <div class="wrapper">
+      <div
+        :class="{ full }"
+        class="map"
+      />
+    </div>
+  </box>
 </template>
 
 <script>
@@ -10,10 +17,35 @@ import Box from './Box'
 export default {
   components: {
     Box
+  },
+  props: {
+    full: {
+      type: Boolean,
+      default: false
+    }
   }
 }
 </script>
 
-<style>
-
+<style lang="scss" scoped>
+.wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+.map {
+  width: 100%;
+  height: 100%;
+  background-image: url("~@/assets/map.png");
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+}
+.full.map {
+  width: 984px;
+  height: 556px;
+}
 </style>

+ 1 - 0
src/views/device/detail/components/Gateway/Sensor.vue

@@ -110,6 +110,7 @@ export default {
   },
   methods: {
     onUpdate (list) {
+      list = list.map(item => { return { ...item, time: item.time.substring(0, 16) } })
       this.list = list.concat(list.slice()).concat(list.slice())
     },
     getSensors () {

+ 1 - 0
src/views/device/detail/components/Gateway/Timeline.vue

@@ -695,6 +695,7 @@ export default {
 }
 .c-timeline__main {
   border: none;
+  flex:1
 }
 .c-timeline__time {
   color: #fff;

+ 4 - 1
src/views/device/detail/components/Gateway/TrafficCamera.vue

@@ -282,6 +282,9 @@ export default {
       //     this.getStarttime(this.datevalue, this.active)
       //   )
       // }
+      if (this.datevalue.getTime() > Date.now()) {
+        this.datevalue = Date.now()
+      }
       this.initEchart()
     },
     getStarttime (time, type) {
@@ -343,7 +346,7 @@ export default {
   .datePicker {
     width: 170px;
     ::v-deep>.el-input__inner{
-      background-color: #2E3756;
+      background-color: transparent;
       color: #fff;
 
     }

+ 19 - 3
src/views/device/detail/components/Gateway/fullDialog.vue

@@ -1,10 +1,14 @@
 <template>
-  <div :class="key">
+  <div
+    :class="key"
+    class="dialog"
+  >
     <el-dialog
       v-if="showDetail"
       :visible.sync="showDetail"
       custom-class="d-preview"
       :show-close="false"
+      :modal-append-to-body="false"
     >
       <slot />
     </el-dialog>
@@ -32,6 +36,12 @@ export default {
 }
 </script>
 <style lang="scss" scoped>
+.dialog{
+::v-deep.v-modal{
+  opacity:0.85
+}
+}
+
 ::v-deep .d-preview {
   display: flex;
   flex-direction: column;
@@ -62,8 +72,8 @@ export default {
 }
 .LEDCamera {
   ::v-deep .d-preview {
-       max-width: 909px;
-    max-height: 580.7px;
+       min-width: 909px;
+    min-height: 580.7px;
   }
 }
 .Timeline{
@@ -78,6 +88,12 @@ export default {
     height: 560px;
   }
 }
+.Map{
+    ::v-deep .d-preview {
+    min-width: 1000px;
+    min-height: 600px;
+  }
+}
 // ::v-deep.c-preview ::v-deep.el-dialog__body {
 //     width: 100%;
 //     height: 100%;

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

@@ -2,7 +2,10 @@
   <Frame v-if="!loading">
     <Header class="header" />
     <div class="DeviceInfo">
-      <DeviceInfo :device="device" />
+      <DeviceInfo
+        :device="device"
+        :online="isOnline"
+      />
     </div>
     <div class="modules">
       <div class="left">
@@ -54,7 +57,12 @@
       </div>
       <div class="right">
         <div class="line">
-          <div class="LEDCamera"><LEDCamera :device="device" /></div>
+          <div
+            class="LEDCamera"
+          ><LEDCamera
+            ref="LEDCamera"
+            :device="device"
+          /></div>
         </div>
         <div class="line">
           <div class="Timeline"><Timeline :device="device" /></div>
@@ -139,6 +147,9 @@ export default {
       this.fullComponent = map[v]
       this.bus = bus
       this.$refs.fullDialog.show(v)
+      if (v === 'LEDCamera') {
+        this.$refs.LEDCamera.pause()
+      }
     })
   },
   destroyed () {
@@ -155,6 +166,8 @@ export default {
         .then(({ data }) => {
           if (data) {
             this.device = data
+            this.isActivated = data.activate === 2
+            this.isOnline = this.isActivated && data.onlineStatus === 1
             start(this.device)
             addListener('online', this.onUpdate)
             startSensor()

+ 80 - 0
src/views/device/detail/components/Gateway/mixin/videoback.js

@@ -0,0 +1,80 @@
+import flvjs from 'flv.js'
+import { authCode } from '@/api/camera'
+const CAMERA_URL = `${location.protocol}//${process.env.VUE_APP_GATEWAY || location.host}`
+export default {
+  data () {
+    return {
+      maskShow: false,
+      refreshShow: false,
+      first: false
+    }
+  },
+  created () {
+    this.$timer = -1
+    this.getAuthCode()
+  },
+  methods: {
+    getAuthCode () {
+      authCode({ deviceId: this.device.id }).then(({ data }) => {
+        this.createPlayer(data)
+      })
+    },
+    createPlayer ({ timestamp, token, expire }) {
+      if (flvjs.isSupported()) {
+        // 创建一个flvjs实例
+        const url = `${CAMERA_URL}/live/${this.device.id}.flv?authorization=${token}&timestamp=${timestamp}&expire=${expire}`
+        this.$player = flvjs.createPlayer({
+          type: 'flv',
+          isLive: true,
+          hasAudio: false,
+          url
+        })
+        this.$player.on('error', () => {
+          this.refreshShow = true
+          this.destroyPlay()
+          this.$message({
+            type: 'warning',
+            message: '设备视频流出错'
+          })
+        })
+        let decodedFrames = -1
+        this.$player.on('statistics_info', res => {
+          decodedFrames = res.decodedFrames
+        })
+        this.$timer = setTimeout(() => {
+          if (decodedFrames === 0) {
+            this.destroyPlay()
+            this.$message({
+              type: 'warning',
+              message: `${this.device.name}设备没有视频流`
+            })
+            this.refreshShow = true
+          }
+        }, 10000)
+        // 将实例挂载到video元素上面
+        this.$player.attachMediaElement(this.$refs.player)
+
+        try {
+          // 开始运行加载 只要流地址正常 就可以在h5页面中播放出画面了
+          this.$player.load()
+          this.$player.play()
+        } catch (error) {
+          console.log('连接websocker异常', error)
+        }
+      }
+    },
+    destroyPlay () {
+      this.$player.pause()
+      this.$player.unload()
+      this.$player.detachMediaElement()
+      this.$player.destroy()
+      this.$player = null
+    }
+  },
+  beforeDestroy () {
+    if (this.$player) {
+      clearTimeout(this.$timer)
+      this.destroyPlay()
+    }
+  }
+}