Procházet zdrojové kódy

视频监控组件

Hwf před 11 měsíci
rodič
revize
88d9c95850

+ 1 - 1
.env.development

@@ -19,7 +19,7 @@ VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
 VITE_APP_PORT = 80
 
 # 接口加密功能开关(如需关闭 后端也必须对应关闭)
-VITE_APP_ENCRYPT = true
+VITE_APP_ENCRYPT = false
 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
 VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

+ 1 - 1
.env.production

@@ -22,7 +22,7 @@ VITE_BUILD_COMPRESS = gzip
 VITE_APP_PORT = 80
 
 # 接口加密功能开关(如需关闭 后端也必须对应关闭)
-VITE_APP_ENCRYPT = true
+VITE_APP_ENCRYPT = false
 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
 VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
 # 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换

+ 13 - 0
src/api/routineCommandMap/index.ts

@@ -0,0 +1,13 @@
+import request from '@/utils/request';
+
+// 获取视频地址
+export function getVideoUrlById(id: string) {
+  return request({
+    url: '/api/videoResource/hkvideo/get_video_url_by_id',
+    method: 'get',
+    params: {
+      id: id,
+      protocol: 'wss'
+    }
+  });
+}

+ 215 - 0
src/components/HKVideo/hikvision-h5player.vue

@@ -0,0 +1,215 @@
+<template>
+  <div class="player-box">
+    <div :id="state.id" class="playWnd"></div>
+    <div v-show="state.isLoading" class="loader"></div>
+  </div>
+</template>
+
+<script setup>
+import { reactive, onMounted, onActivated, nextTick, onBeforeUnmount } from 'vue';
+
+const play = async (url) => {
+  state.wsUrl = url;
+  realplay(url);
+};
+
+const stop = async () => {
+  stopPlay();
+};
+
+const playback = async (url, startTime, endTime) => {
+  console.log('playback', url, state.mode, startTime, endTime);
+  startTime += 'Z';
+  endTime += 'Z';
+  state.player &&
+    state.player.JS_Play(url, { playURL: url, mode: 1 }, 0, startTime, endTime).then(
+      () => {
+        console.log('playback success 播放成功');
+        state.player && state.player.JS_Resize();
+        state.isLoading = false;
+        emits('onPlaying');
+      },
+      (e) => {
+        emits('onPlayError');
+        console.error('playerror', e);
+      }
+    );
+};
+
+defineExpose({
+  play,
+  stop,
+  playback
+});
+
+const emits = defineEmits(['onPlaying', 'onPlayError']);
+
+const state = reactive({
+  id: 'playWnd' + Math.random().toString(16).slice(2),
+  player: null,
+  idWidth: 0,
+  idHeight: 0,
+  mode: 0,
+  isLoading: true,
+  wsUrl: ''
+});
+
+onMounted(() => {
+  nextTick(() => {
+    getElementWidth();
+    createPlayer();
+  });
+
+  window.addEventListener('resize', () => {
+    state.player && state.player.JS_Resize();
+  });
+});
+
+onActivated(() => {
+  getElementWidth();
+});
+
+onBeforeUnmount(() => {
+  stopPlay();
+});
+
+// 获取元素宽高
+const getElementWidth = () => {
+  state.idWidth = document.getElementById(state.id)?.offsetWidth;
+  state.idHeight = document.getElementById(state.id)?.offsetHeight;
+};
+
+// 初始化播放器
+const createPlayer = () => {
+  state.player = new window.JSPlugin({
+    szId: state.id,
+    szBasePath: './',
+    iMaxSplit: 1,
+    iCurrentSplit: 1,
+    bSupporDoubleClickFull: false,
+    openDebug: false,
+    oStyle: {
+      borderSelect: '#000'
+    }
+  });
+  // 事件回调绑定
+  state.player.JS_SetWindowControlCallback({
+    windowEventSelect: function (iWndIndex) {
+      //插件选中窗口回调
+      console.log('windowSelect callback: ', iWndIndex);
+    },
+    pluginErrorHandler: function (iWndIndex, iErrorCode, oError) {
+      //插件错误回调
+      console.log('插件错误回调pluginError callback: ', iWndIndex, iErrorCode, oError);
+      setTimeout(() => {
+        realplay(state.wsUrl);
+      }, 100);
+    },
+    windowEventOver: function (iWndIndex) {
+      //鼠标移过回调
+      // console.log('鼠标移过回调', iWndIndex);
+    },
+    windowEventOut: function (iWndIndex) {
+      //鼠标移出回调
+      //console.log(iWndIndex);
+    },
+    windowEventUp: function (iWndIndex) {
+      //鼠标mouseup事件回调
+      //console.log(iWndIndex);
+      // console.log('鼠标mouseup事件回调')
+      // fullScreen(false)
+    },
+    windowFullCcreenChange: function (bFull) {
+      //全屏切换回调
+      console.log('全屏切换回调fullScreen callback: ', bFull);
+      if (!bFull) {
+        console.log('退出全屏');
+      }
+    },
+    firstFrameDisplay: function (iWndIndex, iWidth, iHeight) {
+      //首帧显示回调
+      console.log('firstFrame loaded callback: ', iWndIndex, iWidth, iHeight);
+    },
+    performanceLack: function () {
+      //性能不足回调
+      console.log('performanceLack callback: ');
+      stopPlay();
+    }
+  });
+  state.player.JS_Resize(state.idWidth, state.idHeight).then(
+    () => {
+      console.info('JS_Resize success');
+    },
+    (err) => {
+      console.info('JS_Resize failed');
+    }
+  );
+};
+
+// 开始播放
+const realplay = async (url) => {
+  console.log('realplay', url, state.mode);
+  state.player &&
+    state.player.JS_Play(url, { playURL: url, mode: state.mode }, 0).then(
+      () => {
+        console.log('realplay success 播放成功');
+        state.player && state.player.JS_Resize();
+        state.isLoading = false;
+        emits('onPlaying');
+      },
+      (e) => {
+        emits('onPlayError');
+        console.error('playerror', e);
+      }
+    );
+};
+
+// 停止播放
+const stopPlay = () => {
+  state.player &&
+    state.player.JS_StopRealPlayAll().then(
+      () => {
+        console.log('JS_StopRealPlayAll success 停止播放');
+      },
+      (err) => {
+        console.log('JS_StopRealPlayAll fail', err);
+      }
+    );
+};
+
+// 整体全屏
+const fullScreen = (type) => {
+  state.player &&
+    state.player.JS_FullScreenDisplay(type).then(
+      () => {
+        console.log('JS_FullScreenDisplay success ');
+      },
+      (err) => {
+        console.log('JS_FullScreenDisplay fail', err);
+      }
+    );
+};
+</script>
+
+<style lang="scss">
+.player-box {
+  position: relative;
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+
+  .playWnd {
+    z-index: 10;
+  }
+
+  .loader {
+    position: absolute;
+    z-index: 10;
+    left: calc(1000% - 32px) / 2;
+    top: calc(1000% - 32px) / 2;
+    width: 32px;
+    height: 32px;
+    background: url('@/assets/img/ajax-loader.gif');
+  }
+}
+</style>

+ 195 - 0
src/components/HKVideo/video-dot.vue

@@ -0,0 +1,195 @@
+<template>
+  <div class="dot-box" :style="{ height: height ? height + 'px' : '190px' }">
+    <div class="video-box">
+      <HikvisionPlayer
+        v-if="isPlaying"
+        ref="videoPlayer"
+        style="width: 100%; height: 100%; object-fit: fill"
+        @on-playing="onHkPlaying"
+        @on-play-error="onHKPlayError"
+      ></HikvisionPlayer>
+      <img v-if="posterVisible" class="video-poster" src="@/assets/images/profile.jpg" alt="" @click="play_now" />
+<!--      <img v-if="posterVisible" class="video-poster" :src="dot_data.poster" />-->
+      <div v-if="errBKVisible" class="err_bk">
+        <div class="err_box">
+          <div class="err_inner_box">
+            <div class="err_icon"></div>
+            <div class="err_text">视频解析错误</div>
+            <div class="refresh_btn" @click="play_now">刷新</div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import HikvisionPlayer from './hikvision-h5player.vue';
+import { getVideoUrlById } from '@/api/routineCommandMap';
+
+const props = defineProps({
+  dot_data: Object,
+  height: Number
+});
+
+const play = () => {
+  play_now();
+};
+
+const refresh_data = () => {
+  console.log('refresh_data');
+  // get_video_item(props.dot_data.code).then((res) => {
+  //   props.dot_data.favor = res.favor;
+  // });
+};
+
+defineExpose({
+  play,
+  refresh_data
+});
+
+const emits = defineEmits(['propClick', 'videoPreviewClick', 'favorClick']);
+
+// https://blog.csdn.net/weixin_49826079/article/details/135147184
+const isShowTip = ref(true);
+const contentRef = ref(false);
+
+const wsUrl = ref('');
+const isPlaying = ref(false);
+const errBKVisible = ref(false);
+const posterVisible = ref(true);
+
+const isShowTooltip = async () => {
+  if (contentRef.value.parentNode.offsetWidth > contentRef.value.offsetWidth) {
+    isShowTip.value = true;
+  } else {
+    isShowTip.value = false;
+  }
+};
+
+const onHkPlaying = async () => {
+  console.log('onHkPlaying');
+  errBKVisible.value = false;
+};
+
+const onHKPlayError = async () => {
+  debugger;
+  console.log('onHKPlayError');
+  errBKVisible.value = true;
+  isPlaying.value = false;
+};
+
+
+const videoPlayer = ref(null);
+
+const play_now = async () => {
+  console.log('play_now');
+  if (!isPlaying.value) {
+    posterVisible.value = false;
+    errBKVisible.value = false;
+    isPlaying.value = true;
+    // 视频监控数据
+    getVideoUrlById(props.dot_data.id).then((res) => {
+      wsUrl.value = res.data;
+      videoPlayer.value.play(wsUrl.value);
+    });
+  }
+};
+
+const stop_now = async () => {
+  if (isPlaying.value) {
+    errBKVisible.value = false;
+    videoPlayer.value.stop();
+    isPlaying.value = false;
+  }
+  posterVisible.value = true;
+};
+</script>
+
+<style lang="scss" scoped>
+.dot-box {
+  width: 100%;
+  display: flex;
+  justify-content: space-between;
+}
+
+.video-box {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+
+  position: relative;
+  border-radius: 2px;
+  border: 1px solid #0056cf;
+
+  .video-poster {
+    width: 100%;
+    height: 100%;
+  }
+
+  .err_bk {
+    z-index: 999;
+    left: 0;
+    top: 0;
+    z-index: 99999;
+    background: #000;
+    position: absolute;
+    width: 100%;
+    height: 100%;
+
+    .err_box {
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      width: 100%;
+      height: 100%;
+
+      .err_inner_box {
+        width: 150px;
+        height: 120px;
+
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+
+        display: inline-flex;
+        justify-content: center;
+        align-items: center;
+
+        .err_icon {
+          width: 39px;
+          height: 40px;
+          background: url('@/assets/img/err_video.png');
+          margin-bottom: 10px;
+        }
+
+        .err_text {
+          font-size: 14px;
+          color: #ffffffb3;
+          margin-bottom: 10px;
+        }
+
+        .refresh_btn {
+          width: 120px;
+          height: 30px;
+          border: 1px solid #001b41;
+          background: #006affcc;
+          cursor: pointer;
+
+          color: #ffffff;
+          text-align: center;
+          font-size: 14px;
+          font-style: normal;
+          font-weight: 400;
+          line-height: 30px;
+        }
+      }
+    }
+  }
+}
+
+.img {
+  width: 100%;
+  height: 170px;
+}
+</style>

+ 2 - 0
src/types/components.d.ts

@@ -64,6 +64,7 @@ declare module 'vue' {
     Hamburger: typeof import('./../components/Hamburger/index.vue')['default']
     HeaderSearch: typeof import('./../components/HeaderSearch/index.vue')['default']
     HeaderSection: typeof import('./../components/HeaderSection/index.vue')['default']
+    HikvisionH5player: typeof import('./../components/HKVideo/hikvision-h5player.vue')['default']
     HKVideo: typeof import('./../components/HKVideo/index.vue')['default']
     IconSelect: typeof import('./../components/IconSelect/index.vue')['default']
     IEpUploadFilled: typeof import('~icons/ep/upload-filled')['default']
@@ -92,6 +93,7 @@ declare module 'vue' {
     TopNav: typeof import('./../components/TopNav/index.vue')['default']
     TreeSelect: typeof import('./../components/TreeSelect/index.vue')['default']
     UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
+    VideoDot: typeof import('./../components/HKVideo/video-dot.vue')['default']
     YMap: typeof import('./../components/Map/YMap.vue')['default']
     YMapold: typeof import('./../components/Map/YMapold.vue')['default']
     YztMap: typeof import('./../components/Map/YztMap/index.vue')['default']

+ 7 - 8
src/views/emergencyCommandMap/RightSection.vue

@@ -24,8 +24,7 @@
             <span>{{ item.time }}</span>
           </div>
           <div class="video-content">
-            <video :src="item.url" controls autoplay muted style="width: 100%; height: 100%" />
-<!--            <img class="img" src="@/assets/images/profile.jpg" alt="" />-->
+            <video-dot :dot_data="item" :height="300" />
           </div>
         </div>
       </div>
@@ -55,12 +54,12 @@ const goToHome = () => {
 };
 // 视频监控
 const videoUrl = reactive([
-  { label: '摄像头一', img: '', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-  { label: '摄像头二', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-  { label: '摄像头三', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-  { label: '摄像头四', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-  { label: '摄像头五', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-  { label: '摄像头六', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' }
+  { id: '44098102801327000256', name: '摄像头一', img: '', time: '17:14' },
+  { id: '44098102801327000256', name: '摄像头二', time: '17:14' },
+  { id: '44098102801327000256', name: '摄像头三', time: '17:14' },
+  { id: '44098102801327000256', name: '摄像头四', time: '17:14' },
+  { id: '44098102801327000256', name: '摄像头五', time: '17:14' },
+  { id: '44098102801327000256', name: '摄像头六', time: '17:14' }
 ]);
 // 视频弹窗显隐
 let videoDialogVisible = ref(false);

+ 28 - 3
src/views/login.vue

@@ -1,7 +1,13 @@
 <template>
   <div class="login">
     <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
-      <h3 class="title">RuoYi-Vue-Plus管理系统</h3>
+      <h3 class="title">应急指挥一张图</h3>
+      <el-form-item v-if="tenantEnabled" prop="tenantId">
+        <el-select v-model="loginForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
+          <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
+          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
+        </el-select>
+      </el-form-item>
       <el-form-item prop="username">
         <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
           <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
@@ -21,6 +27,25 @@
         </div>
       </el-form-item>
       <el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">记住密码</el-checkbox>
+      <!--
+      <el-form-item style="float: right">
+        <el-button circle title="微信登录" @click="doSocialLogin('wechat')">
+          <svg-icon icon-class="wechat" />
+        </el-button>
+        <el-button circle title="MaxKey登录" @click="doSocialLogin('maxkey')">
+          <svg-icon icon-class="maxkey" />
+        </el-button>
+        <el-button circle title="TopIam登录" @click="doSocialLogin('topiam')">
+          <svg-icon icon-class="topiam" />
+        </el-button>
+        <el-button circle title="Gitee登录" @click="doSocialLogin('gitee')">
+          <svg-icon icon-class="gitee" />
+        </el-button>
+        <el-button circle title="Github登录" @click="doSocialLogin('github')">
+          <svg-icon icon-class="github" />
+        </el-button>
+      </el-form-item>
+      -->
       <el-form-item style="width: 100%">
         <el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
           <span v-if="!loading">登 录</span>
@@ -33,7 +58,7 @@
     </el-form>
     <!--  底部  -->
     <div class="el-login-footer">
-      <span>Copyright © 2018-2024 疯狂的狮子Li All Rights Reserved.</span>
+      <span v-if="false">Copyright © 2018-2024 疯狂的狮子Li All Rights Reserved.</span>
     </div>
   </div>
 </template>
@@ -64,7 +89,7 @@ const loginRules: ElFormRules = {
 const codeUrl = ref('');
 const loading = ref(false);
 // 验证码开关
-const captchaEnabled = ref(false);
+const captchaEnabled = ref(true);
 // 注册开关
 const register = ref(false);
 const redirect = ref(undefined);

+ 10 - 16
src/views/routineCommandMap/RightSection.vue

@@ -8,13 +8,11 @@
       <div class="card-content video-list">
         <div v-for="(item, index) in videoMonitorState.listData" :key="index" class="video-box">
           <div class="video-header">
-            <span>{{ item.label }}</span>
+            <span>{{ item.name }}</span>
             <span>{{ item.time }}</span>
           </div>
           <div class="video-content">
-            <video :src="item.url" controls autoplay muted style="width: 100%; height: 100%" />
-<!--            <HKVideo :id="item.id" :url="item.url" />-->
-<!--            <img class="img" src="@/assets/images/profile.jpg" alt="" />-->
+            <video-dot :dot_data="item" />
           </div>
         </div>
       </div>
@@ -64,16 +62,12 @@
 </template>
 
 <script lang="ts" setup>
-import Dialog from '@/components/Dialog/index.vue';
-import videoList from '@/views/videoList/index.vue';
-import HKVideo from '@/components/HKVideo/index.vue';
-
 // 视频监控
 const videoMonitorState = reactive({
   listData: [],
   showListDialog: false,
   showDetailDialog: false,
-  detailData: { label: '', url: '' }
+  detailData: { name: '', url: '' }
 });
 // 显示视频列表
 const showVideoMonitorList = () => {
@@ -117,14 +111,14 @@ const showKnowledgeBaseDetail = () => {
 
 // 初始化数据
 const initData = () => {
-  // 视频监控数据
+  // 视频监控
   videoMonitorState.listData = [
-    { id: 1, label: '摄像头一', img: '', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-    { id: 2, label: '摄像头二', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-    { id: 3, label: '摄像头三', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-    { id: 4, label: '摄像头四', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-    { id: 5, label: '摄像头五', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' },
-    { id: 6, label: '摄像头六', url: 'https://vjs.zencdn.net/v/oceans.mp4', time: '17:14' }
+    { id: '44098102801327000256', name: '摄像头一', img: '', time: '17:14' },
+    { id: '44098102801327000256', name: '摄像头二', time: '17:14' },
+    { id: '44098102801327000256', name: '摄像头三', time: '17:14' },
+    { id: '44098102801327000256', name: '摄像头四', time: '17:14' },
+    { id: '44098102801327000256', name: '摄像头五', time: '17:14' },
+    { id: '44098102801327000256', name: '摄像头六', time: '17:14' }
   ];
   // 预案管理数据
   planManageState.listData = [