Parcourir la source

fix🐛: 修改风格

gitboyzcf il y a 3 mois
Parent
commit
114a0d6899

BIN
src/assets/images/video-thumb.png


+ 233 - 0
src/components/PubUpload.vue

@@ -0,0 +1,233 @@
+<template>
+  <div class="pub-upload" w-full h-full>
+    <template v-if="type === 'img'">
+      <n-upload
+        list-type="image-card"
+        v-model:file-list="newFileList"
+        accept=".jpg,.jpeg,.png,.gif"
+        :custom-request="customRequest"
+        :on-remove="handleRemove"
+        @before-upload="imgBeforeUpload"
+      >
+      </n-upload>
+      <div class="u-tip">
+        <div class="u-upload__tip text-red text-12px mt-7px">
+          只能上传.jpg,.jpeg,.png,.gif格式的图片文件
+        </div>
+      </div>
+    </template>
+    <template v-else-if="type === 'video'">
+      <n-upload
+        list-type="image-card"
+        accept="video/*"
+        v-model:file-list="newFileList"
+        :custom-request="customRequest"
+        :on-remove="handleRemove"
+        :create-thumbnail-url="handleThumbnail"
+        :should-use-thumbnail-url="() => true"
+        @before-upload="videoBeforeUpload"
+        @preview="(file) => handlePreview(file, 'video')"
+      >
+      </n-upload>
+      <div class="u-tip">
+        <div class="u-upload__tip text-red text-12px mt-7px">上传视频不能超过1GB, 只能上传mp4</div>
+      </div>
+    </template>
+    <pub-modal v-model:showModal="showModal" title="" class="video-show">
+      <template #modal-slot>
+        <img w-full :src="imgUrl" alt="查看图片" v-if="imgUrl" />
+        <PubPlayer
+          v-else
+          ref="pubPlayerRef"
+          id="dplayerShow"
+          w="100%"
+          h="400px"
+          :url="videoUrl"
+          :autoplay="true"
+        />
+      </template>
+    </pub-modal>
+  </div>
+</template>
+<script setup>
+  import { NUpload } from 'naive-ui'
+  import { msg } from '@/utils'
+  import axios from 'axios'
+  const CancelToken = axios.CancelToken
+  let source = CancelToken.source()
+
+  const { API_CREATE_UPLOAD_POST, API_UPLOAD_PART_PUT, API_COMPLETE_PART_POST } = useRequest()
+  const baseUrl =
+    import.meta.env.VITE_APP_API_BASEURL === '/'
+      ? window.location.origin
+      : import.meta.env.VITE_APP_API_BASEURL
+  const props = defineProps({
+    type: {
+      type: String
+    },
+    fileList: {
+      type: Array,
+      default: () => []
+    },
+    isUpd: {
+      type: Boolean,
+      default: false
+    }
+  })
+  const emits = defineEmits(['update:fileList'])
+  const showModal = ref(false)
+  const videoUrl = ref('')
+  const imgUrl = ref('')
+
+  let newFileList = ref([])
+
+  const imgBeforeUpload = async (data) => {
+    if (!['image/png', 'image/jpg', 'image/gif', 'image/jpeg'].includes(data.file.file?.type)) {
+      msg('warning', '只能上传.jpg,.jpeg,.png,.gif格式的图片文件,请重新上传')
+      return false
+    }
+    return true
+  }
+  // 创建切片数组
+  const createFileChunk = (file, size = 10485760) => {
+    const fileChunkList = []
+    let cur = 0
+    while (cur < file.size) {
+      fileChunkList.push({
+        file: file.slice(cur, cur + size),
+        percentage: 0
+      })
+      cur += size
+    }
+    return fileChunkList
+  }
+
+  // 进度条
+  const createProgressHandler = (item, onProgress, file, list) => {
+    return (e) => {
+      item.percentage = parseInt(String((e.loaded / e.total) * 100))
+      const loaded = list
+        .map((item) => item.file.size * item.percentage)
+        .reduce((acc, cur) => acc + cur)
+      onProgress({ percent: parseInt((loaded / file.size).toFixed(2)) })
+    }
+  }
+
+  // 上传文件切片
+  const uploadFileChunks = async (list, params = {}, onProgress, file, onFinish) => {
+    if (list.length === 0) {
+      await API_COMPLETE_PART_POST({ name: params.FileName, uploadId: params.UploadId })
+      onProgress({ percent: 100 })
+      onFinish()
+      file.file.path = baseUrl + '/data/show/' + params.FileName
+      emits('update:fileList', newFileList.value)
+      return
+    }
+    let pool = [] //并发池
+    let max = 3 //最大并发量
+    let finish = 0 //完成的数量
+    let failList = [] //失败的列表
+    for (let i = 0; i < list.length; i++) {
+      let item = list[i]
+      let task = API_UPLOAD_PART_PUT(
+        {
+          fileName: params.FileName,
+          uploadId: params.UploadId,
+          partNumber: i + 1,
+          size: item.file.size
+        },
+        item.file,
+        {
+          onUploadProgress: createProgressHandler(item, onProgress, file.file, list),
+          cancelToken: source.token // 添加 cancelToken,用于后续取消请求发送
+        }
+      )
+      try {
+        await task
+        let j = pool.findIndex((t) => t === task)
+        pool.splice(j)
+      } catch (err) {
+        failList.push(item)
+      } finally {
+        finish++
+        if (finish === list.length) {
+          uploadFileChunks(failList, params, onProgress, file, onFinish)
+        }
+      }
+      pool.push(task)
+      if (pool.length === max) {
+        //每当并发池跑完一个任务,就再塞入一个任务
+        Promise.all(pool)
+      }
+    }
+  }
+
+  const handleRemove = ({ file }) => {
+    emits('update:fileList', newFileList.value)
+    return true
+  }
+
+  const handlePreview = ({ file }, type) => {
+    videoUrl.value = ''
+    imgUrl.value = ''
+    switch (type) {
+      case 'img':
+        imgUrl.value = file.path
+        showModal.value = true
+        break
+      case 'video':
+        videoUrl.value = file.path || file.url
+        showModal.value = true
+        break
+    }
+  }
+
+  const handleThumbnail = (file, fileInfo) => {
+    file.thumbnailUrl = new URL('../assets/images/video-thumb.png', import.meta.url).href
+    return file.thumbnailUrl
+  }
+
+  const videoBeforeUpload = async (file) => {
+    const maxSize = 1024 * 1024 * 1024 // 1GB,以字节为单位
+
+    if (file.file.file.size > maxSize) {
+      msg('warning', '上传视频不能超过1GB,请重新上传')
+      return false // 阻止上传
+    } else if (!['video/mp4'].includes(file.file.type)) {
+      msg('warning', '上传视频格式错误, 只能上传mp4, 请重新上传')
+      return false
+    }
+    return true // 允许上传
+  }
+
+  const customRequest = async ({
+    file,
+    data,
+    headers,
+    withCredentials,
+    action,
+    onFinish,
+    onError,
+    onProgress
+  }) => {
+    const fileChunkList = createFileChunk(file.file)
+    onProgress({ percent: 1 })
+    const res = await API_CREATE_UPLOAD_POST({ fileName: file.file.name })
+    await uploadFileChunks(fileChunkList, res, onProgress, file, onFinish)
+  }
+
+  onMounted(() => {
+    if (props.isUpd) {
+      newFileList.value = props.fileList.map((item) => {
+        return {
+          ...item,
+          raw: {
+            url: item.path
+          }
+        }
+      })
+      emits('update:fileList', newFileList.value)
+    }
+  })
+</script>
+<style scoped lang="scss"></style>

+ 4 - 4
src/layout/LayoutMain.vue

@@ -1,16 +1,16 @@
 <template>
   <div class="layout-main flex-1 h-0 relative color-#f0f0f0">
     <!-- <div class="layout-main-left absolute left-15px top-70px"> -->
-    <div class="layout-main-left absolute left-15px top-70px">
+    <!-- <div class="layout-main-left absolute left-15px top-15px">
       <slot name="left"></slot>
     </div>
-    <div class="layout-main-right absolute right-15px top-70px">
+    <div class="layout-main-right absolute right-15px top-15px">
       <slot name="right"></slot>
-    </div>
+    </div> -->
     <div class="layout-main-bottom w-50% absolute left-50% translate-x-[-50%] bottom--1066px">
       <slot name="bottom"></slot>
     </div>
-    <div class="layout-main-bottom w-50% absolute left-50% translate-x-[-50%] top-70px">
+    <div class="layout-main-bottom w-50% absolute left-50% translate-x-[-50%] top-15px">
       <slot name="top"></slot>
     </div>
   </div>

+ 6 - 1
src/layout/Three3D/index.vue

@@ -23,11 +23,14 @@
   import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
   import { onMounted, onUnmounted } from 'vue'
   import { useOutsideHomeStore } from '@/stores/modules/home'
+  import { useOutsideSystemStore } from '@/stores/modules/system'
+  import storage from '@/utils/storage'
 
   const emits = defineEmits(['onGetData'])
 
   const { API_MR_CAMERA_GET, API_MR_VIDEO_LIST_GET } = useRequest()
   const useHomeStore = useOutsideHomeStore()
+  const useSystemStore = useOutsideSystemStore()
 
   const percentage = ref(0)
   const percentageFlag = ref(true)
@@ -98,7 +101,9 @@
         const target = intersects[0].object
         translateCamera({ ...camera.position, z: 5 }, target.position, 1).then(() => {
           useHomeStore.temp = 'video'
-          emits('onGetData', target.vData)
+          useSystemStore.deviceInfo = target.vData
+          storage.local.set('d_id', target.vData.id)
+          // emits('onGetData', target.vData)
         })
       }
     })

+ 2 - 2
src/layout/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="layout h-full w-full relative">
     <div class="layout-map w-full h-full absolute left-0 top-0"><slot name="map"></slot></div>
-    <LayoutHeader></LayoutHeader>
+    <!-- <LayoutHeader></LayoutHeader> -->
     <LayoutMain>
       <template #left>
         <slot name="left"></slot>
@@ -32,7 +32,7 @@
       dw: 1920,
       el: 'body',
       resize: true,
-      ignore: ['#map-canvas', '#canvasDom']
+      ignore: ['#map-canvas', '#canvasDom', 'div[class*="v-binder-follower-container"]']
     })
   })
 </script>

+ 459 - 0
src/views/VideoBox/components/TagInfo.vue

@@ -0,0 +1,459 @@
+<template>
+  <n-form
+    class="tag-info"
+    label-placement="top"
+    label-align="left"
+    require-mark-placement="right"
+    size="medium"
+    flex-1
+    :style="style"
+    v-if="formData.length || groupId"
+  >
+    <n-form-item
+      v-for="(item, i) in formData"
+      :key="i"
+      :label="item.label"
+      :path="item.value"
+      :label-placement="
+        ['video', 'video_s', 'img', 'ball_ptz'].includes(item.type) ? 'top' : 'left'
+      "
+      label-width="auto"
+      show-require-mark
+      label-style="color:#fff"
+    >
+      <template v-if="item.type === 'input'">
+        <n-input v-if="isUpd" style="flex: 1" v-model:value="item.value" />
+        <span v-else color="#8ac5ff">{{ item.value }}</span>
+      </template>
+
+      <template v-else-if="item.type === 'img'">
+        <PubUpload
+          v-if="isUpd"
+          v-model:fileList="item.fileList"
+          :type="item.type"
+          :isUpd="isUpd"
+        ></PubUpload>
+        <div class="w-full flex flex-col gap-y-4" v-else>
+          <n-image
+            v-for="(img, i) in item.fileList"
+            class="w-full"
+            :key="i"
+            :img-props="{ className: 'w-full' }"
+            :src="img.url"
+            :render-toolbar="renderToolbar"
+          />
+          <n-empty v-if="!item.fileList.length" size="large" description="暂无图片" />
+        </div>
+      </template>
+      <template v-else-if="item.type === 'video'">
+        <PubUpload
+          v-if="isUpd"
+          v-model:fileList="item.fileList"
+          :type="item.type"
+          :isUpd="isUpd"
+        ></PubUpload>
+
+        <div class="tag-info-video-wrapper" v-else>
+          <PubPlayer
+            ref="pubPlayerRefs"
+            :id="dplayerId + video.id"
+            v-for="video in item.fileList"
+            :key="video.url"
+            :url="video.url"
+            v-bind="$attrs"
+          />
+          <div class="w-full flex justify-center">
+            <n-empty v-if="!item.fileList.length" size="large" description="暂无视频" />
+          </div>
+        </div>
+      </template>
+      <template v-else-if="item.type === 'video_s'">
+        <n-input v-if="isUpd" style="flex: 1" v-model:value="item.value" />
+        <UseFullscreen
+          v-if="!isUpd && resizeViode"
+          class="flex-1"
+          v-slot="{ toggle, isFullscreen }"
+        >
+          <div class="relative flex-1 h-full popover-screen-full-target">
+            <!-- 全屏按钮 -->
+            <div
+              v-if="item.value"
+              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"
+              />
+              <Icon
+                icon="iconamoon:screen-full"
+                color="#8ac5ff"
+                width="25"
+                height="25"
+                @click="toggle"
+                v-else
+              />
+            </div>
+
+            <div
+              v-if="item.value && item.loading"
+              w-full
+              h-full
+              flex
+              flex-justify-center
+              flex-items-center
+              absolute
+            >
+              <Icon icon="loading" width="30px" height="30px" color="#8ac5ff" />
+            </div>
+            <pub-video
+              v-if="item.value"
+              class="h-full"
+              :newClass="[
+                'tag_video' + item.id,
+                videoClass,
+                isFullscreen ? 'h-full w-auto! mx-0 mx-auto' : ''
+              ]"
+            />
+            <div v-else class="w-full flex justify-center">
+              <n-empty v-if="!item.fileList.length" size="large" description="暂无" />
+            </div>
+          </div>
+        </UseFullscreen>
+      </template>
+      <template v-else-if="item.type === 'ball_ptz'">
+        <div class="w-full" v-for="(v, ii) in item.value" :key="ii">
+          <template v-if="isUpd || groupId">
+            <n-input style="flex: 1" v-model:value="v.url" />
+            <n-divider dashed>
+              <span class="text-12px text-#ccc">配置项</span>
+            </n-divider>
+            <n-grid x-gap="12" :cols="2">
+              <n-gi>
+                <n-form-item
+                  show-require-mark
+                  label-style="color:#fff"
+                  label-align="left"
+                  label-placement="left"
+                  label="IP"
+                >
+                  <n-input v-model:value="v.ip" plaeceholder="请输入" />
+                </n-form-item>
+              </n-gi>
+              <n-gi>
+                <n-form-item
+                  show-require-mark
+                  label-style="color:#fff"
+                  label-align="left"
+                  label-placement="left"
+                  label="端口"
+                >
+                  <n-input v-model:value="v.port" plaeceholder="请输入" />
+                </n-form-item>
+              </n-gi>
+              <n-gi>
+                <n-form-item
+                  show-require-mark
+                  label-style="color:#fff"
+                  label-align="left"
+                  label-placement="left"
+                  label="用户名"
+                >
+                  <n-input v-model:value="v.user" plaeceholder="请输入" />
+                </n-form-item>
+              </n-gi>
+              <n-gi>
+                <n-form-item
+                  show-require-mark
+                  label-style="color:#fff"
+                  label-align="left"
+                  label-placement="left"
+                  label="密码"
+                >
+                  <n-input
+                    type="password"
+                    show-password-on="mousedown"
+                    v-model:value="v.password"
+                    plaeceholder="请输入"
+                  />
+                </n-form-item>
+              </n-gi>
+            </n-grid>
+          </template>
+          <template v-else>
+            <UseFullscreen class="flex-1" v-slot="{ toggle, isFullscreen }">
+              <div
+                :class="[
+                  'relative flex-1 h-full popover-screen-full-target overflow-hidden',
+                  isFullscreen ? 'fullscreen' : ''
+                ]"
+                v-if="resizeViode"
+              >
+                <!-- 全屏按钮 -->
+                <div
+                  v-if="item.value"
+                  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"
+                  />
+                  <Icon
+                    icon="iconamoon:screen-full"
+                    color="#8ac5ff"
+                    width="25"
+                    height="25"
+                    @click="toggle"
+                    v-else
+                  />
+                </div>
+
+                <div
+                  v-if="item.value && item.loading"
+                  w-full
+                  h-full
+                  flex
+                  flex-justify-center
+                  flex-items-center
+                  absolute
+                >
+                  <Icon icon="loading" width="30px" height="30px" color="#8ac5ff" />
+                </div>
+                <pub-video
+                  v-if="item.value"
+                  class="h-full"
+                  :newClass="[
+                    'tag_video' + item.id,
+                    videoClass,
+                    isFullscreen ? 'h-full w-auto! mx-0 mx-auto' : ''
+                  ]"
+                />
+                <div v-else class="w-full flex justify-center">
+                  <n-empty v-if="!item.fileList.length" size="large" description="暂无" />
+                </div>
+                <!-- <joystick :KeyId="item.key" class-name="transform-scale-40" :index="ii" /> -->
+              </div>
+            </UseFullscreen>
+          </template>
+        </div>
+      </template>
+    </n-form-item>
+  </n-form>
+  <n-empty v-else description="请选择标签组"></n-empty>
+</template>
+<script setup>
+  // import Joystick from '../pages/Glance/components/Joystick.vue'
+  import { NInput, NForm, NFormItem, NGrid, NGi, NDivider, NEmpty, NImage } from 'naive-ui'
+  import _ from 'lodash-es'
+  import { uuid } from '@/utils'
+  import 'xgplayer/dist/index.min.css'
+  import useWorker from 'omnimatrix-video-player'
+  import { UseFullscreen } from '@vueuse/components'
+  import { useOutsideSystemStore } from '@/stores/modules/system.js'
+
+  const useSystem = useOutsideSystemStore()
+  const pubPlayerRefs = ref([])
+  const { API_GETGROUP_FORMAT_GET } = useRequest()
+  const props = defineProps({
+    tag: {
+      type: Object
+    },
+    content: {
+      type: Array
+    },
+    isUpd: {
+      type: Boolean
+    },
+    groupId: {
+      type: String
+    },
+    dplayerId: {
+      type: String,
+      default: 'myDplayer'
+    },
+    style: {
+      type: Object,
+      default: () => ({
+        maxHeight: '400px',
+        overflow: 'auto'
+      })
+    },
+    videoClass: {
+      type: String
+    }
+  })
+
+  const formData = ref([])
+  const originalFormData = ref([])
+
+  const renderToolbar = (props) => {
+    return h('div', { class: 'flex gap-2' }, [
+      props.nodes.rotateCounterclockwise,
+      props.nodes.rotateClockwise,
+      props.nodes.resizeToOriginalSize,
+      props.nodes.zoomOut,
+      props.nodes.zoomIn,
+      props.nodes.close
+    ])
+  }
+
+  watch(
+    () => props.isUpd,
+    (isUpd) => {
+      if (!isUpd) {
+        update()
+      } else {
+        clearWorker()
+      }
+    }
+  )
+
+  watch(
+    () => props.groupId,
+    (newV) => {
+      API_GETGROUP_FORMAT_GET({ GroupId: newV }).then((res) => {
+        formData.value = res.map((item) => ({
+          ...item,
+          value:
+            item.type === 'ball_ptz'
+              ? [
+                  {
+                    url: '',
+                    ip: '',
+                    port: '8000',
+                    user: '',
+                    password: ''
+                  }
+                ]
+              : '',
+          fileList: []
+        }))
+      })
+    }
+  )
+
+  const resizeViode = ref(true)
+  // 处理数据回显
+  const getGroupFormat = (tagCon) => {
+    if (!props.tag) return
+    API_GETGROUP_FORMAT_GET({ GroupId: props.tag.groupId }).then((res) => {
+      res.map((format) => {
+        tagCon.forEach((item) => {
+          if (format.key === item.key) {
+            Object.assign(format, item)
+          }
+        })
+      })
+      const formatRes = res.map((item) => {
+        let typeWay = ['img', 'video'].includes(item.type)
+        return {
+          ...item,
+          id: uuid(),
+          fileList: typeWay
+            ? item.value.map((url) => ({
+                id: uuid(),
+                name: url.split('/')[url.split('/').length - 1],
+                url,
+                path: url,
+                fullPath: '/' + url.split('/')[url.split('/').length - 1]
+              }))
+            : [],
+          value: !typeWay ? (item.type === 'ball_ptz' ? item.value : item.value.join('')) : '',
+          loading: false
+        }
+      })
+      formData.value = formatRes
+      originalFormData.value = _.cloneDeepWith(formatRes)
+      initVideo(formData.value)
+    })
+  }
+  const workerObj = ref([])
+  provide('workerObj', workerObj)
+  const initVideo = async (arr) => {
+    nextTick(() => {
+      arr.forEach((item) => {
+        if (!item.value) return
+        const isBallPtz = item.type === 'ball_ptz'
+        if (item.type === 'video_s' || isBallPtz) {
+          item.loading = true
+          const url = `wss://${useSystem.ipF}/VideoShow/Rtsp?rtsp=${isBallPtz ? item.value[0].url : item.value}`
+          workerObj.value.push(
+            useWorker(url, '.tag_video' + item.id, () => {
+              item.loading = false
+            })
+          )
+        }
+      })
+    })
+  }
+  const clearWorker = () => {
+    return new Promise((resolve) => {
+      resizeViode.value = false
+      for (let i = 0; i < workerObj.value.length; i++) {
+        workerObj.value[i].worker.terminate()
+        workerObj.value[i].WebSocketWork.terminate()
+      }
+      workerObj.value = []
+      setTimeout(() => {
+        resizeViode.value = true
+        resolve()
+      }, 60)
+    })
+  }
+
+  // 关闭线程
+  // const closeWorker = (index) => {
+  //   if (!workerObj[index]) return
+  //   workerObj[index].terminate()
+  // }
+  const update = () => {
+    getGroupFormat(props.content)
+  }
+  onMounted(() => {
+    update()
+  })
+
+  onUnmounted(() => {
+    clearWorker()
+    pubPlayerRefs.value?.forEach((playerRef) => {
+      playerRef.player.pause()
+    })
+  })
+
+  defineExpose({
+    formData,
+    originalFormData,
+    update,
+    workerObj
+  })
+</script>
+<style scoped lang="scss">
+  .tag-info {
+    width: 100%;
+
+    &-video-wrapper {
+      width: 100%;
+      display: flex;
+      flex-wrap: wrap;
+      gap: 15px;
+      overflow-x: auto;
+    }
+  }
+</style>

+ 218 - 0
src/views/VideoBox/components/addTag.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="add-tag-modal">
+    <div class="add-tag-input">
+      <div class="tag-item-box">
+        <n-form
+          ref="formRef"
+          :model="tagForm"
+          label-placement="left"
+          label-width="auto"
+          require-mark-placement="right-hanging"
+          size="medium"
+          :style="{
+            maxWidth: '640px'
+          }"
+          :rules="rules"
+        >
+          <n-form-item label="标签组" path="downLabel" label-style="color:#fff" show-require-mark>
+            <n-dropdown
+              trigger="click"
+              :options="downOptions"
+              @select="handleSelect"
+              style="max-height: 240px"
+              scrollable
+            >
+              <n-button>{{ downLabel }}</n-button>
+            </n-dropdown>
+          </n-form-item>
+          <n-form-item show-require-mark label="标签名称" path="Name" label-style="color:#fff">
+            <div class="tag-item-box-wrapper">
+              <n-input style="flex: 1" v-model:value="tagForm.Name" placeholder="请输入标签名称" />
+            </div>
+          </n-form-item>
+
+          <n-form-item label="标签内容" path="Content" label-style="color:#fff">
+            <!-- <n-input v-model:value="tagForm.Content" type="textarea" placeholder="请输入标签内容" :autosize="{
+              minRows: 3
+            }" /> -->
+            <!-- <pub-form-item-show :group-id="groupId" ref="tagInfoRef" /> -->
+            <TagInfo ref="tagInfoRef" :group-id="groupId" :is-upd="true"></TagInfo>
+          </n-form-item>
+          <n-grid x-gap="12" :cols="2">
+            <n-gi>
+              <n-form-item label="重点关注" path="Follow" label-style="color:#fff">
+                <div @click="tagForm.Follow = tagForm.Follow ? '0' : '1'">
+                  <Icon
+                    icon="iconamoon:star-fill"
+                    width="25"
+                    height="25"
+                    color="#fcc307"
+                    v-if="+tagForm.Follow"
+                  />
+                  <Icon icon="iconamoon:star-light" width="25" height="25" color="#c3d7df" v-else />
+                </div>
+              </n-form-item>
+            </n-gi>
+            <n-gi>
+              <n-form-item label="样式编辑" path="Color" label-style="color:#fff">
+                <n-color-picker
+                  :swatches="swatches"
+                  :value="tagForm.Color"
+                  :on-update:value="(v) => (tagForm.Color = v)"
+                />
+              </n-form-item>
+            </n-gi>
+          </n-grid>
+        </n-form>
+      </div>
+      <div class="btns" flex flex-justify-end>
+        <n-button @click="addTag">确定</n-button>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup>
+  import { NInput, NButton, NDropdown, NForm, NFormItem, NColorPicker, NGrid, NGi } from 'naive-ui'
+  import { msg, uuid } from '@/utils'
+  import { useOutsideTagStore } from '@/stores/modules/tag'
+  import TagInfo from './TagInfo.vue'
+  const useTagStore = useOutsideTagStore()
+
+  const { API_ADDTAG_GET, API_GETGROUP_GET, API_TAG_DATA_PUT } = useRequest()
+
+  const props = defineProps({
+    baseInfo: Object
+  })
+  const emits = defineEmits(['closeAddTagModal'])
+
+  const tagInfoRef = ref(null)
+  const formRef = ref(null)
+
+  const groupId = ref('')
+  const tagForm = reactive({
+    Id: uuid(),
+    Name: '',
+    X: 0,
+    Y: 0,
+    Content: '',
+    Img: '',
+    Color: 'rgba(32, 128, 240, 0.6)',
+    Follow: 0,
+    isShow: 1
+  })
+  const rules = {
+    Name: {
+      required: true,
+      message: '请输入',
+      trigger: 'blur'
+    }
+  }
+  const swatches = reactive([
+    'rgba(32, 128, 240, 0.6)',
+    'rgba(24, 160, 88, 0.6)',
+    'rgba(255, 255, 255, 0.6)',
+    'rgba(240, 160, 32, 0.6)',
+    'rgba(208, 48, 80, 0.6)'
+  ])
+  const downLabel = ref('点击选择标签组')
+  const downValue = ref('')
+  const downOptions = ref([])
+  const handleSelect = async (key) => {
+    // console.log(key);
+    const group = downOptions.value.filter((item) => item.key === key)[0]
+    downLabel.value = group.label
+    downValue.value = group.key
+    tagForm.Color = group.color
+    groupId.value = key
+  }
+
+  const addTag = (e) => {
+    e.preventDefault()
+    if (downLabel.value === '点击选择标签组') {
+      msg('warning', '点击选择标签组', {
+        duration: 2000
+      })
+      return
+    }
+
+    // 处理标签内容
+    const tagContent = tagInfoRef.value.formData.map((item) => {
+      return {
+        key: item.key,
+        value: item.value
+          ? Array.isArray(item.value)
+            ? item.value.map((v) => ({ ...v, port: +v.port }))
+            : [item.value]
+          : item.fileList.map((v) => v.file.path)
+      }
+    })
+
+    const res = toRaw(tagContent)
+    const flag = res.every((v) => {
+      if (!v.value.length) {
+        return false
+      } else {
+        return Object.values(v.value[0]).every((v) => v)
+      }
+    })
+
+    if (!flag) {
+      return msg('warning', '带*的为必填项,请补充完整', {
+        duration: 2000
+      })
+    }
+    formRef.value?.validate((errors) => {
+      if (!errors) {
+        API_ADDTAG_GET({
+          GroupId: downValue.value,
+          Name: tagForm.Name,
+          X: tagForm.X,
+          Y: tagForm.Y,
+          Color: tagForm.Color,
+          Follow: `${tagForm.Follow}`
+        }).then((res) => {
+          API_TAG_DATA_PUT({ TagUUID: res.TagUUID }, tagContent).then(() => {
+            msg('success', '添加成功')
+            useTagStore.getGroupTag()
+            emits('closeAddTagModal')
+          })
+        })
+      } else {
+        console.error(errors)
+      }
+    })
+  }
+
+  const getGroup = async () => {
+    const res = await API_GETGROUP_GET({ type: 'all' })
+    downOptions.value = res?.map((item) => ({
+      label: item.GroupName,
+      key: item.GroupId,
+      color: item.GroupColor
+    }))
+  }
+
+  const init = () => {
+    Object.assign(tagForm, props.baseInfo)
+    getGroup()
+  }
+  onMounted(() => {
+    init()
+  })
+
+  defineExpose({
+    tagForm
+  })
+</script>
+<style scoped lang="scss">
+  .add-tag-input {
+    .tag-item-box {
+      color: #fff;
+
+      &-wrapper {
+        display: flex;
+        width: 100%;
+      }
+    }
+  }
+</style>

+ 24 - 2
src/views/VideoBox/components/rightBox.vue

@@ -1,5 +1,6 @@
 <template>
   <ul
+    v-if="show"
     :style="{ top: y + 'px', left: x + 'px' }"
     class="absolute c-#fff right-box py-1 flex flex-col items-center justify-center gap-2 bg-#2262acb3"
   >
@@ -13,7 +14,14 @@
   </ul>
 </template>
 <script setup>
-  defineProps({
+  import { useModal } from 'naive-ui'
+  import { useOutsideSystemStore } from '@/stores/modules/system.js'
+  import AddTag from './addTag.vue'
+
+  const show = defineModel()
+  const modal = useModal()
+  const useSystem = useOutsideSystemStore()
+  const props = defineProps({
     x: {
       type: Number,
       default: 0
@@ -27,7 +35,21 @@
     {
       label: '创建标签',
       handler: () => {
-        console.log('创建标签')
+        show.value = false
+        modal.create({
+          title: '新建标签',
+          draggable: true,
+          preset: 'card',
+          maskClosable: false,
+          content: () =>
+            h(AddTag, {
+              baseInfo: {
+                X: props.x / useSystem.ratio.wR,
+                Y: props.y / useSystem.ratio.hR
+              },
+              onCloseAddTagModal: () => modal.destroy()
+            })
+        })
       }
     }
   ]

+ 36 - 40
src/views/VideoBox/index.vue

@@ -21,7 +21,11 @@
     <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.right.prevent="rightClickFn" />
+          <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"
@@ -56,21 +60,24 @@
         v-for="(tag, i) in tagList"
         :key="i"
         :index="i"
-        :top="tag.y"
-        :left="tag.x"
-        :label="tag.name"
+        :top="tag.Y"
+        :left="tag.X"
+        :label="tag.Name"
       ></TagBox>
-      <!-- <RightClick :x="xy.x" :y="xy.y" /> -->
+      <RightClick v-model="isShow" :x="xy.x" :y="xy.y" />
     </div>
   </div>
 </template>
 
 <script setup>
   // import useWorker from '@/assets/js/video-lib/omnimatrix-video-player'
+  import { useWindowSize, watchDebounced } from '@vueuse/core'
   import { omatVideoPlayer } from '@/assets/js/video-lib/flv/omatVideoPlayer'
   import TagBox from './components/tagBox.vue'
   import { useOutsideSystemStore } from '@/stores/modules/system.js'
   import { useOutsideHomeStore } from '@/stores/modules/home'
+  import { useOutsideTagStore } from '@/stores/modules/tag.js'
+
   import { UseFullscreen } from '@vueuse/components'
   import { NSpin } from 'naive-ui'
   // import { useRatio } from '@/hooks/useRatio.js'
@@ -81,44 +88,18 @@
   })
   const useHomeStore = useOutsideHomeStore()
   const useSystem = useOutsideSystemStore()
+  const useTag = useOutsideTagStore()
   const loading = ref(true)
-
-  const tagList = [
-    {
-      name: '标签1',
-      x: 12,
-      y: 12
-    },
-    {
-      name: '标签2',
-      x: 120,
-      y: 120
-    },
-    {
-      name: '标签3',
-      x: 905,
-      y: 100
-    },
-    {
-      name: '标签4',
-      x: 905,
-      y: 500
-    },
-    {
-      name: '标签5',
-      x: 0,
-      y: 500
-    }
-  ]
+  const { width, height } = useWindowSize()
 
   let playerObj = null
 
   const videoList = computed(() => useSystem.videoList)
+  const tagList = ref([])
+  let tagListClone = []
 
   const init = async () => {
-    const url = videoList.value.pano_view.replace('{14}', props.id)
-    console.log(url)
-
+    const url = videoList.value.pano_view.replace('{14}', useSystem.deviceInfo.id)
     loading.value = true
     try {
       // await useSystem.getDeviceInfo()
@@ -128,6 +109,12 @@
         url,
         () => {
           loading.value = false
+          setTimeout(() => {
+            useSystem.setRatio()
+            const [t, tc] = useTag.initTagList()
+            tagList.value = t
+            tagListClone = tc
+          }, 50)
         },
         {
           enableZoom: false,
@@ -147,17 +134,26 @@
 
   const xy = ref({ x: 0, y: 0 })
   const rightClickFn = (event) => {
-    console.log(event.target.offsetWidth)
-    console.log()
-    const scale = document.body.style.transform.split('(')[1].split(')')[0]
-    const [x, y] = [event.offsetX * parseFloat(scale), event.offsetY * parseFloat(scale)]
+    isShow.value = true
 
     xy.value.x = event.offsetX
     xy.value.y = event.offsetY
   }
 
+  const isShow = ref(false)
+
+  // watchDebounced([width, height], useSystem.setRatio, { debounce: 500, maxWait: 1000 })
+  // watch(
+  //   () => useSystem.ratio,
+  //   () => {
+  //     const [t, tc] = useTag.initTagList()
+  //     tagList.value = t
+  //     tagListClone = tc
+  //   }
+  // )
   onMounted(() => {
     init()
+    useTag.getGroupTag()
   })
   onUnmounted(() => {
     playerObj?.destroyed()

+ 1 - 1
src/views/home/leftbox/topBox.vue

@@ -4,7 +4,7 @@
       id="aqsj-echarts"
       ref="echartsRef"
       width="100%"
-      height="275px"
+      height="325px"
       :loading="loading"
       :fullOptions="fullOptions"
     />

+ 1 - 1
src/views/home/leftboxtwo/topBox.vue

@@ -4,7 +4,7 @@
       id="aqsj-echarts2"
       ref="echartsRef"
       width="100%"
-      height="275px"
+      height="326px"
       :loading="loading"
       :fullOptions="fullOptions"
     />

+ 1 - 1
src/views/home/rightbox/bottomBox.vue

@@ -3,7 +3,7 @@
     <ECharts
       id="bjlx-echarts"
       width="100%"
-      height="192px"
+      height="245px"
       :loading="loading"
       :fullOptions="fullOptions"
     />

+ 1 - 1
src/views/home/rightboxtwo/topBox.vue

@@ -20,7 +20,7 @@
         <ECharts
           id="bjzx-echarts2"
           width="100%"
-          height="230px"
+          height="284px"
           :loading="loading"
           :fullOptions="fullOptions"
         />