Browse Source

feat: 添加高级联动、智能巡检、一点即视、放大镜、截图、录像等

gitboyzcf 1 month ago
parent
commit
8980877f72

+ 14 - 20
src/views/VideoBox/components/TagBox.vue

@@ -1,12 +1,12 @@
 <template>
   <div
     :class="['tag-box' + index, 'absolute color-white']"
-    :style="{ top: `${top}px`, left: `${left}px` }"
+    :style="{ top: `${tag.Y}px`, left: `${tag.X}px` }"
   >
     <div
       class="'tag-count tag-box-wrapper p-1 w-144px absolute left-18px top--50px text-12px cursor-pointer"
       @click="toggle"
-      ><div class="tag-label text-center">{{ label }}</div></div
+      ><div class="tag-label text-center">{{ tag.Name }}</div></div
     >
     <img
       class="tag-count absolute bottom-0 left-0 w-162px pointer-events-none"
@@ -14,19 +14,22 @@
       alt=""
     />
     <Teleport defer to=".video-box-wrapper">
-      <div class="z-9 absolute" ref="floating" :style="{ top: top + 'px', left: left + 'px' }">
+      <div class="z-9 absolute" ref="floating" :style="{ top: tag.Y + 'px', left: tag.X + 'px' }">
         <transition name="scaleHide">
           <div
             v-if="isShow"
-            class="overflow-hidden origin-top-left h-200px w-300px bg-[rgba(11,21,44,1)] absolute left-10px top--0px b-solid b-1 b-#2BABF2"
+            class="overflow-hidden origin-top-left w-300px bg-[rgba(11,21,44,1)] absolute left-10px top--0px b-solid b-1 b-#2BABF2"
           >
             <div class="flex justify-between p-1">
-              <span>{{ label }}</span>
+              <span>{{ tag.Name }}</span>
               <span
                 class="i-material-symbols-close text-coolgray cursor-pointer"
                 @click.stop="isShow = false"
               ></span>
             </div>
+            <n-scrollbar style="max-height: 500px">
+              <tag-info :content="tag.Data" :is-upd="false" dplayerId="tagList" :tag="tag" />
+            </n-scrollbar>
           </div>
         </transition>
       </div>
@@ -35,6 +38,8 @@
 </template>
 
 <script setup>
+  import TagInfo from './TagInfo.vue'
+  import { NScrollbar } from 'naive-ui'
   defineOptions({ name: 'TagBox' })
   const floating = ref(null)
   const props = defineProps({
@@ -42,18 +47,7 @@
       type: Number,
       default: 0
     },
-    label: {
-      type: String,
-      default: '标签'
-    },
-    top: {
-      type: Number,
-      default: 0
-    },
-    left: {
-      type: Number,
-      default: 0
-    }
+    tag: Object
   })
   const isShow = ref(false)
 
@@ -68,14 +62,14 @@
     const { width, height } = videoCount.getBoundingClientRect()
     const [w, h] = [width / 2, height / 2]
 
-    if (props.left > w) {
+    if (props.tag.X > w) {
       tagBox.style.transform = 'rotateY(180deg)'
       tagLabel.style.transform = 'rotateY(180deg)'
-      if (props.top < h) {
+      if (props.tag.Y < h) {
         tagBox.style.transform = 'rotate(180deg)'
         tagLabel.style.transform = 'rotate(180deg)'
       }
-    } else if (props.top < h) {
+    } else if (props.tag.Y < h) {
       tagBox.style.transform = 'rotateX(180deg)'
       tagLabel.style.transform = 'rotateX(180deg)'
     }

+ 6 - 6
src/views/VideoBox/components/TagInfo.vue

@@ -18,11 +18,11 @@
         ['video', 'video_s', 'img', 'ball_ptz'].includes(item.type) ? 'top' : 'left'
       "
       label-width="auto"
-      show-require-mark
+      :show-require-mark="isUpd"
       label-style="color:#fff"
     >
       <template v-if="item.type === 'input'">
-        <n-input v-if="isUpd" style="flex: 1" v-model:value="item.value" />
+        <n-input v-if="isUpd" style="flex: 1" v-model:value="item.value" placeholder="请输入" />
         <span v-else color="#8ac5ff">{{ item.value }}</span>
       </template>
 
@@ -139,7 +139,7 @@
             <n-grid x-gap="12" :cols="2">
               <n-gi>
                 <n-form-item
-                  show-require-mark
+                  :show-require-mark="isUpd"
                   label-style="color:#fff"
                   label-align="left"
                   label-placement="left"
@@ -150,7 +150,7 @@
               </n-gi>
               <n-gi>
                 <n-form-item
-                  show-require-mark
+                  :show-require-mark="isUpd"
                   label-style="color:#fff"
                   label-align="left"
                   label-placement="left"
@@ -161,7 +161,7 @@
               </n-gi>
               <n-gi>
                 <n-form-item
-                  show-require-mark
+                  :show-require-mark="isUpd"
                   label-style="color:#fff"
                   label-align="left"
                   label-placement="left"
@@ -172,7 +172,7 @@
               </n-gi>
               <n-gi>
                 <n-form-item
-                  show-require-mark
+                  :show-require-mark="isUpd"
                   label-style="color:#fff"
                   label-align="left"
                   label-placement="left"

+ 1 - 1
src/views/VideoBox/components/rightBox.vue

@@ -47,7 +47,7 @@
                 X: props.x / useSystem.ratio.wR,
                 Y: props.y / useSystem.ratio.hR
               },
-              onCloseAddTagModal: () => modal.destroy()
+              onCloseAddTagModal: () => modal.destroyAll()
             })
         })
       }

+ 142 - 53
src/views/VideoBox/index.vue

@@ -1,51 +1,61 @@
 <template>
   <div class="video-box w-100%">
-    <div class="cursor-pointer w-30px h-35px" @click="toMap">
-      <svg
-        t="1731580007592"
-        class="icon"
-        viewBox="0 0 1024 1024"
-        version="1.1"
-        xmlns="http://www.w3.org/2000/svg"
-        p-id="4534"
-        width="100%"
-        height="100%"
-      >
-        <path
-          d="M658.261333 170.624H292.565333v-170.666667L0 255.957333l292.565333 256v-170.666666h365.696c124.330667 0 219.434667 110.933333 219.434667 256s-95.061333 256-219.434667 256H292.565333v170.666666h365.696c197.504 0 365.738667-187.733333 365.738667-426.666666s-160.938667-426.666667-365.738667-426.666667z"
-          fill="#2A79D0"
-          p-id="4535"
-        ></path>
-      </svg>
+    <div class="cursor-pointer" @click="toMap">
+      <span class="block w-30px h-30px color-#8ac5ff i-bx:arrow-to-left"></span>
     </div>
     <div class="video-box-wrapper mt-10px relative bg-#00000080">
       <UseFullscreen v-slot="{ toggle, isFullscreen }">
-        <div class="relative w-full h-full flex items-center">
-          <pub-video-new
-            class="aspect-ratio-video"
-            @click="isShow = false"
-            @click.right.prevent="rightClickFn"
-          />
-          <!-- 全屏按钮 -->
-          <div
-            class="all-screen popover-screen-full-trigger cursor-pointer z-99 absolute right-8px top-8px"
+        <div class="relative w-full h-full flex">
+          <vue-photo-zoom-pro
+            ref="zoomRef"
+            class="w-full h-full"
+            :scale="3"
+            :width="200"
+            :disabled-event="isZoom"
           >
-            <Icon
-              icon="icon-park-outline:off-screen"
-              color="#8ac5ff"
-              width="25"
-              height="25"
+            <div class="h-full flex items-center">
+              <pub-video-new
+                class="aspect-ratio-video"
+                @click="isShow = false"
+                @click.right.prevent="rightClickFn"
+              />
+            </div>
+            <template v-slot:zoomer>
+              <pub-video-new newClass="zoom-video" class="h-full" :key="resetKey" />
+            </template>
+          </vue-photo-zoom-pro>
+          <div class="flex items-center z-99 absolute right-8px top-8px gap-2">
+            <ul class="flex flex-justify-around flex-items-center gap-2">
+              <li
+                v-for="item in listComputed"
+                :key="item.iCom"
+                @click.stop="toolsHandler(item.type)"
+              >
+                <Icon
+                  :class="[item.class]"
+                  :icon="item.iCom"
+                  :color="item.color"
+                  :width="item.w"
+                  :height="item.h"
+                  :style="{ marginBottom: item.type === 'lx' ? '-5px' : '' }"
+                />
+              </li>
+            </ul>
+            <span
+              class="size-26px color-#8ac5ff cursor-pointer i-ph:magnifying-glass-plus"
+              @click="magnifying(!magnifyingFlag)"
+            ></span>
+            <!-- 全屏按钮 -->
+            <span
               v-if="isFullscreen"
+              class="block size-25px color-#8ac5ff cursor-pointer i-icon-park-outline:off-screen"
               @click="toggle"
-            />
-            <Icon
-              icon="iconamoon:screen-full"
-              color="#8ac5ff"
-              width="25"
-              height="25"
-              @click="toggle"
+            ></span>
+            <span
               v-else
-            />
+              class="block size-25px color-#8ac5ff cursor-pointer i-iconamoon:screen-full"
+              @click="toggle"
+            ></span>
           </div>
           <div
             v-if="loading"
@@ -56,14 +66,7 @@
         </div>
       </UseFullscreen>
 
-      <TagBox
-        v-for="(tag, i) in tagList"
-        :key="i"
-        :index="i"
-        :top="tag.Y"
-        :left="tag.X"
-        :label="tag.Name"
-      ></TagBox>
+      <TagBox v-for="(tag, i) in tagList" :key="i" :index="i" :tag="tag"></TagBox>
       <RightClick v-model="isShow" :x="xy.x" :y="xy.y" />
     </div>
   </div>
@@ -80,8 +83,10 @@
 
   import { UseFullscreen } from '@vueuse/components'
   import { NSpin } from 'naive-ui'
-  // import { useRatio } from '@/hooks/useRatio.js'
+  import { $mitt, uuid } from '@/utils'
   import RightClick from './components/rightBox.vue'
+  import VuePhotoZoomPro from 'vue-photo-zoom-pro'
+  import 'vue-photo-zoom-pro/dist/style/vue-photo-zoom-pro.css'
 
   const props = defineProps({
     id: Number
@@ -91,8 +96,8 @@
   const useTag = useOutsideTagStore()
   const loading = ref(true)
   const { width, height } = useWindowSize()
-
   let playerObj = null
+  let magnifyingWorkerObj = null
 
   const videoList = computed(() => useSystem.videoList)
   const tagList = ref([])
@@ -111,9 +116,7 @@
           loading.value = false
           setTimeout(() => {
             useSystem.setRatio()
-            const [t, tc] = useTag.initTagList()
-            tagList.value = t
-            tagListClone = tc
+            useTag.getGroupTag()
           }, 50)
         },
         {
@@ -151,11 +154,97 @@
   //     tagListClone = tc
   //   }
   // )
+  const isZoom = ref(true)
+  const zoomRef = ref(null)
+  const resetKey = ref(uuid())
+  const magnifyingFlag = ref(false)
+  const magnifying = (flag) => {
+    zoomRef.value?.mouseLeave()
+    magnifyingFlag.value = flag
+    if (flag) {
+      if (magnifyingWorkerObj) return
+      const url = videoList.value.pano_view.replace('{14}', useSystem.deviceInfo.id)
+      magnifyingWorkerObj = omatVideoPlayer('.zoom-video', url, () => {
+        isZoom.value = false
+        zoomRef.value.mouseEnter()
+      })
+    } else {
+      isZoom.value = true
+      zoomRef.value.mouseLeave()
+      magnifyingWorkerObj?.destroyed()
+      resetKey.value = uuid()
+      magnifyingWorkerObj = null
+    }
+  }
+  useTag.$subscribe(() => {
+    const [t, tc] = useTag.initTagList()
+    tagList.value = t
+    tagListClone = tc
+  })
+  const keyupHedler = (e) => {
+    if (e.code === 'Escape') {
+      if (magnifyingFlag.value) {
+        magnifying(false)
+      }
+    }
+  }
+
+  const listComputed = computed(() => {
+    return tools.value.map((item) => {
+      if (item.type === 'lx') {
+        if (state.value) {
+          item.iCom = 'fad:armrecording'
+          item.color = 'red'
+          item.class = 'showhide-anim'
+        } else {
+          item.iCom = 'uil:video'
+          item.color = '#8ac5ff'
+          item.class = ''
+        }
+      }
+      return item
+    })
+  })
+  const tools = ref([
+    {
+      iCom: 'iconoir:screenshot',
+      label: '截图',
+      color: '#8ac5ff',
+      type: 'jt',
+      w: 25,
+      h: 25
+    },
+    {
+      iCom: 'uil:video',
+      label: '录像',
+      type: 'lx',
+      color: '#8ac5ff',
+      class: '',
+      w: 30,
+      h: 30
+    }
+  ])
+  const state = ref(false)
+  const toolsHandler = (type) => {
+    switch (type) {
+      case 'lx':
+        // switchState()
+        state.value = !state.value
+        playerObj.recording(state.value)
+        break
+      case 'jt':
+        playerObj.screenshot()
+        break
+    }
+  }
+
   onMounted(() => {
     init()
-    useTag.getGroupTag()
+    document.body.addEventListener('keyup', keyupHedler)
   })
   onUnmounted(() => {
     playerObj?.destroyed()
+    keyupHedler({ code: 'Escape' })
+    document.body.removeEventListener('keyup', keyupHedler)
   })
 </script>

+ 39 - 18
src/views/home/middleBottomBox/components/advancedLinkage.vue

@@ -3,25 +3,35 @@
     <UseFullscreen v-slot="{ toggle, isFullscreen }">
       <div class="relative w-full h-full flex items-center">
         <pub-video-new :newClass="['al-video' + currentActive]" class="aspect-ratio-video" />
-        <!-- 全屏按钮 -->
-        <div
-          class="all-screen popover-screen-full-trigger cursor-pointer z-99 absolute right-8px top-8px"
-        >
-          <Icon
-            icon="icon-park-outline:off-screen"
-            color="#8ac5ff"
-            width="25"
-            height="25"
-            v-if="isFullscreen"
-            @click="toggle"
-          />
+        <div class="absolute top-10px right-10px flex flex-row-reverse gap-1 z-999">
+          <!-- 全屏按钮 -->
+          <div class="all-screen popover-screen-full-trigger cursor-pointer">
+            <Icon
+              icon="icon-park-outline:off-screen"
+              color="#8ac5ff"
+              width="25"
+              height="25"
+              v-if="isFullscreen"
+              @click="toggle"
+            />
+            <Icon
+              icon="iconamoon:screen-full"
+              color="#8ac5ff"
+              width="25"
+              height="25"
+              @click="toggle"
+              v-else
+            />
+          </div>
           <Icon
-            icon="iconamoon:screen-full"
+            icon="grommet-icons:power-shutdown"
+            v-if="!isFullscreen"
             color="#8ac5ff"
             width="25"
             height="25"
-            @click="toggle"
-            v-else
+            cursor-pointer
+            alt="关闭画面"
+            @click="closeWorker"
           />
         </div>
         <div
@@ -48,6 +58,7 @@
   })
   const useSystem = useOutsideSystemStore()
   const loading = ref(false)
+  const clearCom = inject('clearCom')
 
   let playerObj = null
 
@@ -68,8 +79,11 @@
           loading.value = false
         },
         {
-          enableZoom: false,
-          enableDrag: false
+          enableZoom: true, // 启用缩放
+          enableDrag: true, // 启用拖动
+          minScale: 1, // 最小缩放比例
+          maxScale: 6, // 最大缩放比例
+          dragSpeed: 1.9 // 拖动速度
         }
       )
     } catch (error) {
@@ -77,9 +91,16 @@
     }
   }
 
+  // 关闭线程
+  const closeWorker = () => {
+    if (!playerObj) return
+    playerObj?.destroyed()
+    playerObj = null
+    clearCom()
+  }
+
   onMounted(() => {
     init()
-    console.log(props.data)
   })
 
   onUnmounted(() => {

+ 192 - 0
src/views/home/middleBottomBox/components/glance.vue

@@ -0,0 +1,192 @@
+<!-- 一点即视 -->
+<template>
+  <div class="glance relative">
+    <div class="glance-right">
+      <UseFullscreen class="flex items-center" v-slot="{ toggle, isFullscreen }">
+        <div class="glance-right-wrapper screen-full-target">
+          <pub-video-new :newClass="'glance-ps-video' + currentActive" />
+          <joystick :id="data.id" />
+          <div
+            v-if="isVideo"
+            absolute
+            top-0
+            right-0
+            left-0
+            bottom-0
+            flex
+            flex-justify-center
+            flex-items-center
+          >
+            <Icon icon="loading" color="#007bff" width="40" height="40" />
+          </div>
+          <div
+            v-if="!isVideo && !useSystem.videoList.ball_camera"
+            absolute
+            top-0
+            right-0
+            left-0
+            bottom-0
+            flex
+            flex-justify-center
+            flex-items-center
+          >
+            <n-empty size="large" description="球机网络故障,请检查网络后重连设备!">
+              <template #extra>
+                <n-button size="small" @click="reconnect"> 重连 </n-button>
+              </template>
+            </n-empty>
+          </div>
+
+          <!-- 操作按钮 -->
+          <div
+            class="panoramic-situation-main-btn-box absolute top-10px right-10px flex flex-row-reverse"
+          >
+            <!-- 全屏按钮 -->
+            <div
+              class="panoramic-situation-main-all-screen screen-full-trigger cursor-pointer ml-1"
+              alt="全屏按钮"
+            >
+              <Icon
+                icon="icon-park-outline:off-screen"
+                color="#8ac5ff"
+                width="25"
+                height="25"
+                v-if="isFullscreen"
+                @click="toggle"
+              />
+              <Icon
+                icon="iconamoon:screen-full"
+                color="#8ac5ff"
+                width="25"
+                height="25"
+                @click="toggle"
+                v-else
+              />
+            </div>
+            <!-- 关闭按钮 -->
+            <Icon
+              icon="grommet-icons:power-shutdown"
+              v-if="!isFullscreen"
+              mr-1
+              color="#8ac5ff"
+              width="25"
+              height="25"
+              cursor-pointer
+              alt="关闭画面"
+              @click="closeWorker"
+            />
+          </div>
+        </div>
+      </UseFullscreen>
+    </div>
+  </div>
+</template>
+<script setup>
+  import Joystick from './joystick.vue'
+  import { NEmpty, NButton } from 'naive-ui'
+  import { uuid } from '@/utils'
+  import { omatVideoPlayer } from '@/assets/js/video-lib/flv/omatVideoPlayer'
+  import { useOutsideSystemStore } from '@/stores/modules/system.js'
+  import { UseFullscreen } from '@vueuse/components'
+  defineOptions({ name: 'GlanceBox' })
+
+  const clearCom = inject('clearCom')
+  const useSystem = useOutsideSystemStore()
+  const { API_CLICKTOSEE_POST, API_LY_B_RELOGIN_POST } = useRequest()
+
+  const isVideo = ref(true)
+
+  const props = defineProps({
+    currentActive: Number,
+    data: {
+      type: Object,
+      default: () => {}
+    }
+  })
+
+  watch(
+    () => useSystem.partObj,
+    async (newV) => {
+      // if (useSystem.winActive === props.currentActive) {
+
+      // }
+      await API_CLICKTOSEE_POST(
+        { CameraId: props.data.id },
+        {
+          X: Number(newV.PartCenterX),
+          Y: Number(newV.PartCenterY)
+        }
+      )
+    },
+    { deep: true }
+  )
+  // 球机重连
+  const reconnect = async () => {
+    isVideo.value = true
+    try {
+      await API_LY_B_RELOGIN_POST({ BallCameraId: props.data.id })
+      getWss()
+    } catch (e) {
+      if ([7].includes(e.code)) {
+        isVideo.value = false
+      }
+    }
+  }
+
+  const playerObj = shallowRef(null)
+  provide('playerObj', playerObj)
+
+  const getWss = () => {
+    nextTick(() => {
+      isVideo.value = true
+      const url = useSystem.videoList.ball_camera
+        .replace('{4}', uuid())
+        .replace('{12}', props.data.id)
+
+      playerObj.value = omatVideoPlayer(
+        '.glance-ps-video' + props.currentActive,
+        url,
+        () => {
+          isVideo.value = false
+        },
+        { enableZoom: false, enableDrag: false }
+      )
+    })
+  }
+
+  // 关闭线程
+  const closeWorker = () => {
+    playerObj.value?.destroyed()
+    clearCom()
+  }
+  onMounted(() => {
+    getWss()
+  })
+  onUnmounted(() => {
+    playerObj.value?.destroyed()
+  })
+</script>
+<style scoped lang="scss">
+  .glance {
+    display: flex;
+    height: 100%;
+    &-left {
+      width: 400px;
+      height: 100%;
+      border-right: 1px solid rgba(0, 128, 255, 0.4);
+    }
+    &-right {
+      flex: 1;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      &-wrapper {
+        width: 100%;
+        position: relative;
+        overflow: hidden;
+        transition: all 0.5s;
+        aspect-ratio: 16/9;
+      }
+    }
+  }
+</style>

+ 335 - 0
src/views/home/middleBottomBox/components/joystick.vue

@@ -0,0 +1,335 @@
+<template>
+  <div
+    ref="joystickRef"
+    :class="[
+      'joystick absolute bottom-0 right-0 origin-bottom-right transition-all',
+      planShow ? 'w-[300px]' : 'w-0'
+    ]"
+  >
+    <div class="joystick-openorclose absolute left--40px bottom-0" @click="showSwitch">
+      <Icon
+        icon="mingcute:left-fill"
+        color="#409fff"
+        width="40"
+        height="40"
+        :class="['cursor-pointer transition-all', planShow ? 'rotate-180' : 'rotate-0']"
+      />
+    </div>
+    <div class="flex">
+      <div class="joystick-wrapper">
+        <div class="buttons grid grid-cols-3 grid-rows-3 gap-1 w-38 h-38">
+          <div
+            class="direction-button bg-#093f9d75 b-0 overflow-hidden p-2 transition duration-200 ease-in-out active:bg-blue-500 focus:outline-none"
+            v-for="direction in directions"
+            :style="{
+              backgroundColor: direction === 'hr' && isHR ? 'rgb(59 130 246)' : ''
+            }"
+            :key="direction"
+            v-on:[downEvent].stop="setPZTmove(direction, 'down')"
+            v-on:[upEvent].stop="setPZTmove(direction, 'up')"
+          >
+            <img class="w-full" :src="getStaticResource(`assets/images/${direction}.png`).href" />
+          </div>
+        </div>
+      </div>
+      <div class="slider-box flex-1 ml-1 w-140px">
+        <div class="slider-box-control h-full grid grid-cols-1 grid-rows-3 gap-1">
+          <div
+            class="h-full text-20px line-height-45px text-center bg-#093f9d75 flex flex-justify-around flex-items-center"
+          >
+            <img
+              src="@/assets/images/fd.png"
+              class="w-28px transition duration-200 ease-in-out active:bg-blue-500 focus:outline-none"
+              alt=""
+              v-on:[downEvent]="setPZTmove('fd', 'down')"
+              v-on:[upEvent]="setPZTmove('fd', 'up')"
+            />
+            <img
+              src="@/assets/images/sx.png"
+              class="w-28px transition duration-200 ease-in-out active:bg-blue-500 focus:outline-none"
+              alt=""
+              v-on:[downEvent]="setPZTmove('sx', 'down')"
+              v-on:[upEvent]="setPZTmove('sx', 'up')"
+            />
+          </div>
+          <div class="h-full text-20px line-height-45px text-center bg-#093f9d75">
+            <!-- <img
+              src="@/assets/images/jia.png"
+              class="w-32px transition duration-200 ease-in-out active:bg-blue-500 focus:outline-none"
+              alt=""
+              v-on:[downEvent]="setPZTmove('jia', 'down')"
+              v-on:[upEvent]="setPZTmove('jia', 'up')"
+            />
+            <img
+              src="@/assets/images/jian.png"
+              class="w-32px transition duration-200 ease-in-out active:bg-blue-500 focus:outline-none"
+              alt=""
+              v-on:[downEvent]="setPZTmove('jian', 'down')"
+              v-on:[upEvent]="setPZTmove('jian', 'up')"
+            /> -->
+            <ul class="h-full flex flex-justify-around flex-items-center">
+              <li
+                v-for="item in listComputed"
+                :key="item.iCom"
+                @click.stop="toolsHandler(item.type)"
+              >
+                <Icon
+                  :class="[item.class]"
+                  :icon="item.iCom"
+                  :color="item.color"
+                  :width="item.w"
+                  :height="item.h"
+                  :style="{ marginBottom: item.type === 'lx' ? '-5px' : '' }"
+                />
+              </li>
+            </ul>
+          </div>
+          <div class="px-4 place-content-center flex flex-col flex-items-center bg-#093f9d75">
+            <n-slider
+              v-model:value="sliderValue"
+              style="
+                --n-fill-color: #093f9d;
+                --n-fill-color-hover: #093f9d;
+                --n-rail-color: #093f9d;
+                --n-rail-color-hover: #093f9d;
+                --n-handle-color: #8ac5ff;
+              "
+              :min="1"
+              :max="7"
+              :tooltip="false"
+            />
+            <div class="text-12px color-#8ac5ff text-nowrap">
+              步长 <span>{{ sliderValue }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { NSlider } from 'naive-ui'
+  import { getStaticResource } from '@/utils'
+
+  const { API_PTZ_MOVE_START_POST, API_PTZ_MOVE_STOP_POST, API_LY_B_RELOGIN_POST } = useRequest()
+  const directions = ['lt', 't', 'tr', 'l', 'hr', 'r', 'lb', 'b', 'br']
+  const sliderValue = ref(7)
+
+  const downEvent = 'mousedown'
+  const upEvent = 'mouseup'
+
+  const planShow = ref(false)
+  const props = defineProps({
+    id: String
+  })
+  // 球机重连
+  const reconnect = (err, data, api) => {
+    if ([7].includes(err.code)) {
+      // dialogFn(
+      //   'warning',
+      //   '警告',
+      //   '球机网络故障,请检查网络后重连设备!',
+      //   async () => {
+      //     const res = await API_LY_B_RELOGIN_POST()
+      //     reconnect(res)
+      //   },
+      //   () => {},
+      //   '重连',
+      //   '取消'
+      // )
+    } else {
+      api(data)
+    }
+  }
+
+  const isHR = ref(false)
+  const setPZTmove = (direction, isKey) => {
+    let data = { id: 0 }
+    let isDownOrUp = 'down'
+    switch (direction) {
+      case 't':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 3 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 3 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'tr':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 6 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 6 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'r':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 2 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 2 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'br':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 8 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { speed: sliderValue.value, direction: 8 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'b':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 4 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 4 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'lb':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 7 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 7 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'l':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 1 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 1 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'hr':
+        if (isKey === 'down') {
+          isHR.value = !isHR.value
+          if (isHR.value) {
+            Object.assign(data, { speed: sliderValue.value, direction: 1 })
+            isDownOrUp = 'down'
+          } else {
+            Object.assign(data, { direction: 1 })
+            isDownOrUp = 'up'
+          }
+        }
+        break
+      case 'lt':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 5 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 5 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'fd':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 9 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 9 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'sx':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 10 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 10 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'jia':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 11 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 11 })
+          isDownOrUp = 'up'
+        }
+        break
+      case 'jian':
+        if (isKey === 'down') {
+          Object.assign(data, { speed: sliderValue.value, direction: 12 })
+          isDownOrUp = 'down'
+        } else {
+          Object.assign(data, { direction: 12 })
+          isDownOrUp = 'up'
+        }
+        break
+    }
+    if (!data.direction) return
+    if (isDownOrUp === 'down') {
+      API_PTZ_MOVE_START_POST({ CameraId: props.id }, data)
+    } else {
+      API_PTZ_MOVE_STOP_POST({ CameraId: props.id }, data)
+    }
+  }
+
+  const showSwitch = () => {
+    planShow.value = !planShow.value
+  }
+
+  const playerObj = inject('playerObj')
+
+  const tools = ref([
+    {
+      iCom: 'iconoir:screenshot',
+      label: '截图',
+      color: '#8ac5ff',
+      type: 'jt',
+      w: 25,
+      h: 25
+    },
+    {
+      iCom: 'uil:video',
+      label: '录像',
+      type: 'lx',
+      color: '#8ac5ff',
+      class: '',
+      w: 30,
+      h: 30
+    }
+  ])
+  const state = ref(false)
+  const toolsHandler = (type) => {
+    switch (type) {
+      case 'lx':
+        // switchState()
+        state.value = !state.value
+        playerObj.value.recording(state.value)
+        break
+      case 'jt':
+        playerObj.value.screenshot()
+        break
+    }
+  }
+
+  const listComputed = computed(() => {
+    return tools.value.map((item) => {
+      if (item.type === 'lx') {
+        if (state.value) {
+          item.iCom = 'fad:armrecording'
+          item.color = 'red'
+          item.class = 'showhide-anim'
+        } else {
+          item.iCom = 'uil:video'
+          item.color = '#8ac5ff'
+          item.class = ''
+        }
+      }
+      return item
+    })
+  })
+</script>

+ 189 - 0
src/views/home/middleBottomBox/components/partBox.vue

@@ -0,0 +1,189 @@
+<template>
+  <div class="part-box h-full">
+    <div class="part-box-wrapper h-full">
+      <UseFullscreen v-slot="{ toggle, isFullscreen }">
+        <div class="tour-video-box screen-full-target h-full flex justify-center items-center">
+          <!-- <template v-if="tourId"> -->
+          <!-- <div class="w-full h-full flex flex-justify-center flex-items-center">
+            <pub-video
+              :newClass="'tour-video' + currentActive"
+              :style="{ maxWidth: '100%', maxHeight: '100%', aspectRatio: 16 / 9 }"
+            />
+          </div> -->
+          <pub-video-new :newClass="'t-video' + currentActive" />
+          <!-- 工具面板 -->
+          <!-- <operation-panel
+          :index="currentActive"
+          z-98
+          :op-class="'tour-video' + currentActive"
+          uuid="uuid"
+        /> -->
+          <!-- </template> -->
+          <!-- 操作按钮 -->
+          <div class="tour-main-btn-box" absolute top-10px right-10px flex flex-row-reverse z-999>
+            <!-- 全屏按钮 -->
+            <div
+              class="tour-main-all-screen screen-full-trigger"
+              cursor-pointer
+              ml-1
+              alt="全屏按钮"
+            >
+              <Icon
+                icon="icon-park-outline:off-screen"
+                color="#8ac5ff"
+                width="25"
+                height="25"
+                v-if="isFullscreen"
+                @click="toggle"
+              />
+              <Icon
+                icon="iconamoon:screen-full"
+                color="#8ac5ff"
+                width="25"
+                height="25"
+                @click="toggle"
+                v-else
+              />
+            </div>
+            <Icon
+              icon="grommet-icons:power-shutdown"
+              v-if="!isFullscreen"
+              color="#8ac5ff"
+              width="25"
+              height="25"
+              cursor-pointer
+              alt="关闭画面"
+              @click="closeVideo(currentActive)"
+            />
+          </div>
+          <div v-if="!playerObj" class="flex flex-justify-center flex-items-center h-full">
+            <span style="animation: fadeIn 0.8s alternate infinite">请双击全景画面</span>
+          </div>
+          <div
+            v-if="loading"
+            class="absolute top-50% left-50% transform-translate--50% flex justify-center items-center"
+          >
+            <NSpin />
+          </div>
+        </div>
+      </UseFullscreen>
+    </div>
+  </div>
+</template>
+<script setup>
+  import { NSpin } from 'naive-ui'
+  import { useOutsideSystemStore } from '@/stores/modules/system.js'
+  import { uuid } from '@/utils'
+  import { omatVideoPlayer } from '@/assets/js/video-lib/flv/omatVideoPlayer'
+  import { UseFullscreen } from '@vueuse/components'
+
+  const useSystem = useOutsideSystemStore()
+
+  const props = defineProps({
+    currentActive: Number,
+    data: Object
+  })
+  const tourId = ref('')
+  const clearCom = inject('clearCom')
+  let playerObj = null
+  const videoList = computed(() => useSystem.videoList)
+
+  const tempArr = ref([
+    // {
+    //   x: 158,
+    //   y: 259,
+    //   show: true,
+    //   key: uuid()
+    // },
+    // {
+    //   x: 731,
+    //   y: 374,
+    //   show: false,
+    //   key: uuid()
+    // },
+    // {
+    //   x: 1358,
+    //   y: 444,
+    //   show: false,
+    //   key: uuid()
+    // },
+    // {
+    //   x: 2201,
+    //   y: 642,
+    //   show: false,
+    //   key: uuid()
+    // }
+  ])
+  let tempArrI = 0
+  let tempArrT = null
+  const loading = ref(false)
+  const videoClick = async (item, index) => {
+    if (playerObj) return
+    if (item.id) {
+      closeWorker(index)
+    }
+    tourId.value = props.data.id
+
+    tempArr.value =
+      props.data?.route.map((path) => ({
+        ...path,
+        z: 0,
+        show: false,
+        key: uuid()
+      })) || []
+    nextTick(() => {
+      const url = videoList.value.pano_view.replace('{14}', useSystem.deviceInfo.id)
+      loading.value = true
+
+      try {
+        // await useSystem.getDeviceInfo()
+        console.log('websocket请求')
+        playerObj = omatVideoPlayer(
+          '.t-video' + props.currentActive,
+          url,
+          () => {
+            const videoDom = document.querySelector('.t-video' + props.currentActive)
+            console.log(videoDom.style.transform)
+
+            videoDom.style.transform = `scale(3) translate(${-tempArr.value[tempArrI].x}px, ${-tempArr.value[tempArrI].y}px)`
+            loading.value = false
+            tempArrT = setInterval(() => {
+              if (tempArrI < tempArr.value.length - 1) {
+                ++tempArrI
+              } else {
+                tempArrI = 0
+              }
+              videoDom.style.transform = `scale(3) translate(${-tempArr.value[tempArrI].x}px, ${-tempArr.value[tempArrI].y}px)`
+            }, tempArr.value[tempArrI].duration * 1000)
+          },
+          {
+            enableZoom: false,
+            enableDrag: false
+          }
+        )
+      } catch (error) {
+        console.log(error)
+      }
+    })
+  }
+
+  const closeVideo = () => {
+    closeWorker()
+  }
+  // 关闭线程
+  const closeWorker = (index) => {
+    if (!playerObj) return
+    clearInterval(tempArrT)
+    playerObj?.destroyed()
+    playerObj = null
+  }
+
+  onMounted(() => {
+    videoClick(props.data, props.currentActive)
+  })
+
+  onBeforeUnmount(() => {
+    closeWorker()
+  })
+</script>
+<style scoped lang="scss"></style>

+ 187 - 0
src/views/home/middleBottomBox/components/tour.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="tour-box h-full">
+    <div class="tour-box-wrapper h-full">
+      <UseFullscreen v-slot="{ toggle, isFullscreen }">
+        <div class="tour-video-box screen-full-target h-full flex justify-center items-center">
+          <!-- <template v-if="tourId"> -->
+          <!-- <div class="w-full h-full flex flex-justify-center flex-items-center">
+            <pub-video
+              :newClass="'tour-video' + currentActive"
+              :style="{ maxWidth: '100%', maxHeight: '100%', aspectRatio: 16 / 9 }"
+            />
+          </div> -->
+          <pub-video-new :newClass="'t-video' + currentActive" />
+          <!-- 工具面板 -->
+          <!-- <operation-panel
+          :index="currentActive"
+          z-98
+          :op-class="'tour-video' + currentActive"
+          uuid="uuid"
+        /> -->
+          <!-- </template> -->
+          <!-- 操作按钮 -->
+          <div
+            class="tour-main-btn-box absolute top-10px right-10px flex flex-row-reverse z-999 gap-1"
+          >
+            <!-- 全屏按钮 -->
+            <div class="tour-main-all-screen screen-full-trigger cursor-pointer" alt="全屏按钮">
+              <Icon
+                icon="icon-park-outline:off-screen"
+                color="#8ac5ff"
+                width="25"
+                height="25"
+                v-if="isFullscreen"
+                @click="toggle"
+              />
+              <Icon
+                icon="iconamoon:screen-full"
+                color="#8ac5ff"
+                width="25"
+                height="25"
+                @click="toggle"
+                v-else
+              />
+            </div>
+            <Icon
+              icon="grommet-icons:power-shutdown"
+              v-if="!isFullscreen"
+              color="#8ac5ff"
+              width="25"
+              height="25"
+              cursor-pointer
+              alt="关闭画面"
+              @click="closeVideo(currentActive)"
+            />
+          </div>
+          <div
+            v-if="loading"
+            class="absolute top-50% left-50% transform-translate--50% flex justify-center items-center"
+          >
+            <NSpin />
+          </div>
+        </div>
+      </UseFullscreen>
+    </div>
+  </div>
+</template>
+<script setup>
+  import { NSpin } from 'naive-ui'
+  import { useOutsideSystemStore } from '@/stores/modules/system.js'
+  import { uuid } from '@/utils'
+  import { omatVideoPlayer } from '@/assets/js/video-lib/flv/omatVideoPlayer'
+  import { UseFullscreen } from '@vueuse/components'
+
+  const useSystem = useOutsideSystemStore()
+
+  const props = defineProps({
+    currentActive: Number,
+    data: Object
+  })
+  const tourId = ref('')
+  const clearCom = inject('clearCom')
+  let playerObj = null
+  const videoList = computed(() => useSystem.videoList)
+
+  const tempArr = ref([
+    // {
+    //   x: 158,
+    //   y: 259,
+    //   show: true,
+    //   key: uuid()
+    // },
+    // {
+    //   x: 731,
+    //   y: 374,
+    //   show: false,
+    //   key: uuid()
+    // },
+    // {
+    //   x: 1358,
+    //   y: 444,
+    //   show: false,
+    //   key: uuid()
+    // },
+    // {
+    //   x: 2201,
+    //   y: 642,
+    //   show: false,
+    //   key: uuid()
+    // }
+  ])
+  let tempArrI = 0
+  let tempArrT = null
+  const loading = ref(true)
+  const videoClick = async (item, index) => {
+    if (playerObj) return
+    if (item.id) {
+      closeWorker(index)
+    }
+    tourId.value = props.data.id
+
+    tempArr.value =
+      props.data?.route.map((path) => ({
+        ...path,
+        z: 0,
+        show: false,
+        key: uuid()
+      })) || []
+    nextTick(() => {
+      const params1 = tempArr.value[0]
+      params1.z = 99
+      params1.show = true
+      const url = videoList.value.pano_view.replace('{14}', useSystem.deviceInfo.id)
+      loading.value = true
+
+      try {
+        // await useSystem.getDeviceInfo()
+        console.log('websocket请求')
+        playerObj = omatVideoPlayer(
+          '.t-video' + props.currentActive,
+          url,
+          () => {
+            const videoDom = document.querySelector('.t-video' + props.currentActive)
+            console.log(videoDom.style.transform)
+
+            videoDom.style.transform = `scale(3) translate(${-tempArr.value[tempArrI].x}px, ${-tempArr.value[tempArrI].y}px)`
+            loading.value = false
+            tempArrT = setInterval(() => {
+              if (tempArrI < tempArr.value.length - 1) {
+                ++tempArrI
+              } else {
+                tempArrI = 0
+              }
+              videoDom.style.transform = `scale(3) translate(${-tempArr.value[tempArrI].x}px, ${-tempArr.value[tempArrI].y}px)`
+            }, tempArr.value[tempArrI].duration * 1000)
+          },
+          {
+            enableZoom: false,
+            enableDrag: false
+          }
+        )
+      } catch (error) {
+        console.log(error)
+      }
+    })
+  }
+
+  const closeVideo = () => {
+    closeWorker()
+    clearCom()
+  }
+  // 关闭线程
+  const closeWorker = (index) => {
+    if (!playerObj) return
+    clearInterval(tempArrT)
+    playerObj?.destroyed()
+    playerObj = null
+  }
+
+  onMounted(() => {
+    videoClick(props.data, props.currentActive)
+  })
+
+  onBeforeUnmount(() => {
+    closeWorker()
+  })
+</script>
+<style scoped lang="scss"></style>