Parcourir la source

Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	src/types/components.d.ts
yangyuxuan il y a 2 mois
Parent
commit
e78f80cb33

+ 39 - 0
src/api/viedoTag/viedoManagement.ts

@@ -0,0 +1,39 @@
+import request from '@/utils/request';
+// 查询
+export function getTagList(params?: any) {
+  return request({
+    url: '/api/videoResource/tag/list',
+    method: 'get',
+    params: params
+  });
+}
+// 详情
+export function getVideoTagInfo(dictCode) {
+  return request({
+    url: '/api/videoResource/tag/info/' + dictCode,
+    method: 'get'
+  });
+}
+// 新增
+export function createVideoTag(data) {
+  return request({
+    url: '/api/videoResource/tag/add',
+    method: 'post',
+    data: data
+  });
+}
+// 修改
+export function updateVideoTag(data, dictCode) {
+  return request({
+    url: '/api/videoResource/tag/update/' + dictCode,
+    method: 'put',
+    data: data
+  });
+}
+// 删除
+export function deleteVideoTag(dictCode: string) {
+  return request({
+    url: '/api/videoResource/tag/delete/' + dictCode,
+    method: 'delete'
+  });
+}

BIN
src/assets/images/map/tag2.png


BIN
src/assets/images/videoTagEdit/box1.png


BIN
src/assets/images/videoTagEdit/btn.png


BIN
src/assets/images/videoTagEdit/close.png


BIN
src/assets/images/videoTagEdit/industry.png


BIN
src/assets/images/videoTagEdit/line1.png


BIN
src/assets/images/videoTagEdit/line2.png


BIN
src/assets/images/videoTagEdit/type.png


+ 25 - 14
src/components/Dialog/index.vue

@@ -6,12 +6,12 @@
     :style="{ width: computedWidth, height: computedHeight, zIndex: zIndex }"
   >
     <div :class="type === 'xs' || headerType === 'header2' ? 'dialog-header2' : 'dialog-header'">
-      <div v-if="!hideTitle" class="dialog-title">
+      <div v-if="!hideTitle" class="dialog-title" :title="title ? title : '弹窗'">
         {{ title ? title : '弹窗' }}
       </div>
       <div v-if="!!getTagId" class="tags">
         <div v-for="(item, index) in tags" :key="index" class="tag">{{ item.dict_label }}</div>
-        <div class="add-tag" @click="handleShowAddTag">+添加标签</div>
+        <i :class="tags && tags.length > 0 ? 'collectFill' : 'collect'" @click="handleShowAddTag" />
       </div>
       <i class="decoration" />
       <i class="dialog-close" @click="closeDialog" />
@@ -30,8 +30,8 @@
     <i class="triangle2" />
     <i class="triangle3" />
     <i class="triangle4" />
-    <VideoTagEdit v-model="showAddTag" :tags="tags" :id="getTagId" @updateVideoTag="getData" />
   </div>
+  <VideoTagEdit v-if="showAddTag" v-model="showAddTag" :tags="tags" :id="getTagId" @updateVideoTag="getData(true)" />
 </template>
 
 <script lang="ts" setup name="Dialog">
@@ -39,7 +39,8 @@ import { getVideoTagInfo } from '@/api/videoMonitor';
 import useAppStore from '@/store/modules/app';
 
 interface Tag {
-  name: string;
+  dict_label: string;
+  dict_value: string;
 }
 // type: xs、sm、md、lg、xl
 interface Props {
@@ -67,7 +68,7 @@ const props = withDefaults(defineProps<Props>(), {
   confirmText: '确定',
   hideFooter: false
 });
-const emit = defineEmits(['update:modelValue', 'close', 'confirm']);
+const emit = defineEmits(['update:modelValue', 'close', 'confirm', 'changeTagsData']);
 const computedWidth = computed(() => {
   if (!!props.width) {
     return props.width;
@@ -134,9 +135,12 @@ let showAddTag = ref(false);
 const handleShowAddTag = () => {
   showAddTag.value = true;
 };
-const getData = () => {
+const getData = (needUpdate?: boolean) => {
   getVideoTagInfo({ video_code: props.getTagId }).then((res) => {
     tags.value = res.data;
+    if (!!needUpdate) {
+      emit('changeTagsData', res.data);
+    }
   });
 };
 onMounted(() => {
@@ -180,6 +184,9 @@ onMounted(() => {
       font-family: 'YouSheBiaoTiHei';
       font-size: 24px;
       padding-left: 25px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
     }
     .dialog-close {
       position: absolute;
@@ -248,16 +255,20 @@ onMounted(() => {
         justify-content: center;
         align-items: center;
       }
-      .add-tag {
-        width: 104px;
-        height: 29px;
-        background: url('@/assets/images/map/rightMenu/btn.png') no-repeat;
+      .collect {
+        background: url('@/assets/images/video/collect.png') no-repeat;
+      }
+      .collectFill {
+        background: url('@/assets/images/video/collectFill.png') no-repeat;
+      }
+      .collect,
+      .collectFill {
+        width: 20px;
+        height: 20px;
+        cursor: pointer;
         background-size: 100% 100%;
-        margin-left: 12px;
-        display: flex;
-        align-items: center;
-        justify-content: center;
         cursor: pointer;
+        margin-left: 6px;
       }
     }
     &::before {

+ 9 - 3
src/components/HKVideo/hikvision-h5player.vue

@@ -2,7 +2,7 @@
   <div class="player-box">
     <div :id="state.id" class="playWnd"></div>
     <div v-show="state.isLoading" class="loader"></div>
-    <img v-if="!state.isLoading" class="video-enlarge" src="@/assets/images/video/enlarge.png" alt="" @click="fullScreen" />
+    <img v-if="!state.isLoading && !hideEnlarge" class="video-enlarge" src="@/assets/images/video/enlarge.png" alt="" @click="fullScreen" />
   </div>
 </template>
 
@@ -10,6 +10,10 @@
 import { reactive, onMounted, onActivated, nextTick, onBeforeUnmount } from 'vue';
 import { parseTime } from '@/utils/ruoyi';
 
+const props = defineProps({
+  hideEnlarge: Boolean
+});
+
 const play = async (url) => {
   state.wsUrl = url;
   realplay(url);
@@ -216,6 +220,7 @@ defineExpose({
   stop,
   playback,
   handleScreenshot,
+  fullScreen,
   getPlayer
 });
 </script>
@@ -244,8 +249,9 @@ defineExpose({
   .loader {
     position: absolute;
     z-index: 10;
-    left: calc(1000% - 32px) / 2;
-    top: calc(1000% - 32px) / 2;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
     width: 32px;
     height: 32px;
     background: url('@/assets/images/video/ajax-loader.gif');

+ 9 - 7
src/components/HKVideo/index.vue

@@ -38,8 +38,11 @@ const play = () => {
 };
 
 const refresh_data = () => {
-  console.log('refresh_data');
-  play_now();
+  reload.value = true;
+  nextTick(() => {
+    reload.value = false;
+    play_now();
+  });
 };
 
 const emits = defineEmits(['propClick', 'videoPreviewClick', 'favorClick']);
@@ -98,10 +101,6 @@ watch(
 );
 const play_now = async (check?: boolean) => {
   posterVisible.value = false;
-  // if (check === true && props.dot_data.status === '离线') {
-  //   errBKVisible.value = true;
-  //   isPlaying.value = false;
-  // } else {
   errBKVisible.value = false;
   isPlaying.value = true;
   // 视频监控数据
@@ -109,7 +108,6 @@ const play_now = async (check?: boolean) => {
     wsUrl.value = res.data;
     videoPlayer.value.play(wsUrl.value);
   });
-  // }
   console.log('play_now');
 };
 
@@ -121,6 +119,9 @@ const stop_now = async () => {
   }
   posterVisible.value = true;
 };
+const handleFullScreen = (name) => {
+  videoPlayer.value.fullScreen(name);
+};
 const handleScreenshot = (name) => {
   videoPlayer.value.handleScreenshot(name);
 };
@@ -131,6 +132,7 @@ defineExpose({
   play,
   refresh_data,
   handleScreenshot,
+  handleFullScreen,
   getPlayer
 });
 onMounted(() => {

+ 1 - 1
src/components/HKVideo/index2.vue

@@ -39,7 +39,7 @@
     <!--收藏弹窗-->
     <VideoTagEdit v-if="showCollectDialog" :id="dot_data.id" v-model="showCollectDialog" :tags="tags" @update-video-tag="getData2" />
     <!--点击全屏弹窗-->
-    <video-dialog v-if="showFullScreenDialog" v-model="showFullScreenDialog" :videoMonitorData="dot_data" @changeTagsData="getData" />
+    <video-dialog v-if="showFullScreenDialog" v-model="showFullScreenDialog" height="680px" :videoMonitorData="dot_data" @changeTagsData="getData" />
   </div>
 </template>
 

+ 16 - 4
src/components/Map/YztMap/index.vue

@@ -81,10 +81,22 @@ watch(
   }
 );
 const mapList = reactive({
-  satellite2: ['YZT1712111943104', 'YZT1695608158269'],
-  satellite3: ['YZT1708679726700', 'YZT1695608158269'],
-  imageMap: ['YZT1640925052482', 'YZT1695608158269'],
-  imageMap2: ['YZT1640925052482', 'YZT1695608158269']
+  satellite2: [
+    { layer: 'map', code: 'YZT1712111943104', zIndex: '-99', visible: true },
+    { layer: 'annotation', code: 'YZT1695608158269', zIndex: '-98', visible: true }
+  ],
+  satellite3: [
+    { layer: 'map', code: 'YZT1708679726700', zIndex: '-99', visible: true },
+    { layer: 'annotation', code: 'YZT1695608158269', zIndex: '-98', visible: true }
+  ],
+  imageMap: [
+    { layer: 'map', code: 'YZT1640925052482', zIndex: '-99' },
+    { layer: 'annotation', code: 'YZT1695608158269', zIndex: '-98', visible: true }
+  ],
+  imageMap2: [
+    { layer: 'map', code: 'YZT1640925052482', zIndex: '-99', visible: true },
+    { layer: 'annotation', code: 'YZT1695608158269', zIndex: '-98', visible: true }
+  ]
 });
 // 监听地图类型变化
 watch(

+ 1 - 1
src/components/Map/data.ts

@@ -102,7 +102,7 @@ export const titleList = {
   '30': '路网视频',
   '31': '江湖河库',
   '32': '防溺水',
-  '33': '森林防火',
+  '33': '森防视频',
   '34': '防灾救援',
   '41': '救援队伍',
   '42': '交通局视频',

+ 110 - 256
src/components/VideoTagEdit/index.vue

@@ -1,288 +1,142 @@
 <template>
-  <div v-show="modelValue" class="edit-container">
-    <div class="edit-header">
-      <div class="title">视频标签</div>
-      <i class="close" @click="handleClose" />
+  <Dialog v-model="show" title="视频标签" height="auto" @close="handleClose" @confirm="handleAdd">
+    <div class="title-box">
+      <div class="title">当前标签</div>
     </div>
-    <div class="edit-content">
-      <div class="title-box">
-        <div class="title">当前标签</div>
-      </div>
-      <div class="tags">
-        <div v-for="(item, index) in tags" :key="index" class="tag">{{ item.dict_label }}</div>
-      </div>
-      <div class="title-box">
-        <div class="title">新增标签</div>
-      </div>
-      <div class="tags">
-        <el-select
-          v-model="addTag2"
-          :teleported="false"
-          class="custom-select"
-          popper-class="custom-select-popper"
-          style="width: 300px; margin-left: 10px"
-        >
-          <el-option v-for="item in video_type" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-        <div class="common-btn-primary" @click="handleAdd3">添加</div>
-      </div>
-      <div class="title-box">
-        <div class="title">新建标签</div>
-      </div>
-      <div class="tags">
-        <div class="tag">{{ addTag }}</div>
-        <el-input v-model="addTag" class="custom-input" placeholder="不超过20个字,回车添加" @keyup.enter="handleAdd"></el-input>
-      </div>
-      <div class="title-box2">
-        <div class="title">最近标签</div>
-      </div>
-      <div class="tags">
-        <div v-for="(item, index) in recentTags" :key="index" class="tag" @click="handleAdd2(item)">{{ item.dict_label }}</div>
-      </div>
-      <div class="title-box3">
-        <i class="icon-type" />
-        <div class="title">类型</div>
-        <el-select
-          v-model="type"
-          :teleported="false"
-          class="custom-select"
-          popper-class="custom-select-popper"
-          style="width: 140px; margin-bottom: 10px"
-        >
-          <el-option label="全部" value="lx" />
-          <el-option v-for="item in video_tag_type" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-      </div>
-      <div class="tags">
-        <div v-for="(item, index) in typeTags" :key="index" class="tag" @click="handleAdd2(item)">{{ item.dict_label }}</div>
-      </div>
-      <div class="title-box3">
-        <i class="icon-industry" />
-        <div class="title">行业</div>
-        <el-select
-          v-model="industry"
-          :teleported="false"
-          class="custom-select"
-          popper-class="custom-select-popper"
-          style="width: 140px; margin-bottom: 10px"
-        >
-          <el-option label="全部" value="hy" />
-          <el-option v-for="item in video_tag_industry" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-      </div>
-      <div class="tags">
-        <div v-for="(item, index) in industryTags" :key="index" class="tag" @click="handleAdd2(item)">{{ item.dict_label }}</div>
+    <div v-if="selectTags && selectTags.length > 0" class="tags">
+      <div v-for="(item, index) in selectTags" :key="index" class="tagActive">{{ item.label }}</div>
+    </div>
+    <div v-else class="empty-text">暂无标签</div>
+    <div class="title-box" style="margin-top: 20px">
+      <div class="title">添加标签</div>
+    </div>
+    <div class="tags">
+      <div v-for="item in videoType" :key="item.value" :class="!!item.checked ? 'tagActive' : 'tag'" @click="handleSelect(item)">
+        {{ item.label }}
       </div>
     </div>
-  </div>
+  </Dialog>
 </template>
 
 <script lang="ts" setup name="VideoTagEdit">
-import { addVideoTag, addVideoTagLabel, getLxHyVideoTagInfo, getRecentlyVideoTagInfo } from '@/api/videoMonitor';
-import { showSuccessMsg } from '@/utils/notification';
+import { addVideoTag } from '@/api/videoMonitor';
+import { getDicts } from '@/api/system/dict/data';
 
-const props = defineProps({
-  modelValue: Boolean,
-  tags: [],
-  id: String
-});
-const emits = defineEmits(['update:modelValue', 'updateVideoTag']);
-const proxy = getCurrentInstance()?.proxy;
-const { video_type, video_tag_type, video_tag_industry } = toRefs<any>(proxy?.useDict('video_type', 'video_tag_type', 'video_tag_industry'));
-let addTag = ref('');
-let addTag2 = ref('');
-let recentTags = ref([]);
-let type = ref('lx');
-let typeTags = ref([]);
-let industry = ref('hy');
-let industryTags = ref([]);
+interface Tag {
+  dict_label: string;
+  dict_value: string;
+}
+interface Props {
+  modelValue: boolean;
+  tags: Tag[];
+  id: string;
+}
 
-const getTypeList = () => {
-  getLxHyVideoTagInfo({ dict_value: type.value }).then((res) => {
-    typeTags.value = res.data;
-  });
-};
-const getIndustryList = () => {
-  getLxHyVideoTagInfo({ dict_value: industry.value }).then((res) => {
-    industryTags.value = res.data;
-  });
-};
-watch(
-  () => props.id,
-  () => {
-    if (!!props.id) {
-      getRecentlyVideoTagInfo().then((res) => {
-        recentTags.value = res.data;
-      });
-      getTypeList();
-      getIndustryList();
-    } else {
-      addTag.value = '';
-      recentTags.value = [];
-      typeTags.value = [];
-      industryTags.value = [];
-    }
+const props = defineProps<Props>();
+const emits = defineEmits(['update:modelValue', 'updateVideoTag']);
+let videoType = ref([]);
+const show = computed({
+  get() {
+    return props.modelValue;
   },
-  {
-    immediate: true
+  set(newValue) {
+    emits('update:modelValue', newValue);
   }
-);
+});
+let selectTags = ref([]);
+
 //关闭
 const handleClose = () => {
   emits('update:modelValue', false);
 };
+// 选择
+const handleSelect = (item) => {
+  item.checked = !item.checked;
+  const index = selectTags.value.findIndex((item2) => item2.value === item.value);
+  if (!!item.checked) {
+    if (index === -1) {
+      selectTags.value.push(item);
+    }
+  } else {
+    if (index >= 0) {
+      selectTags.value.splice(index, 1);
+    }
+  }
+};
 const handleAdd = () => {
-  addVideoTagLabel({
-    video_code: props.id,
-    dict_label: addTag.value,
-    dict_type: 'video_type'
-  }).then(() => {
-    proxy.$modal.msgSuccess('新增成功');
-    emits('updateVideoTag');
+  const tagsId = [];
+  selectTags.value.forEach((item) => {
+    tagsId.push(item.value);
   });
-  addTag.value = '';
-};
-const handleAdd2 = (item) => {
-  addVideoTag({ video_code: props.id, dict_value: item.dict_value, dict_type: 'video_type' }).then(() => {
-    proxy.$modal.msgSuccess('新增成功');
+  addVideoTag({ video_code: props.id, dict_value: tagsId, dict_type: 'video_type' }).then(() => {
+    showSuccessMsg('新增成功');
     emits('updateVideoTag');
+    handleClose();
   });
 };
-const handleAdd3 = () => {
-  addVideoTag({ video_code: props.id, dict_value: addTag2.value, dict_type: 'video_type' }).then(() => {
-    proxy.$modal.msgSuccess('新增成功');
-    emits('updateVideoTag');
+
+const getTagList = () => {
+  getDicts('video_type').then((res) => {
+    let data = [];
+    let data2 = [];
+    res.data.forEach((item) => {
+      const checked = !!props.tags && props.tags.findIndex((item2) => item2.dict_value === item.dictValue) > -1;
+      const obj = { label: item.dictLabel, value: item.dictValue, checked: checked };
+      if (!!checked) {
+        data2.push(obj);
+      }
+      data.push(obj);
+    });
+    videoType.value = data;
+    selectTags.value = data2;
   });
 };
+onMounted(() => {
+  getTagList();
+});
 </script>
 
 <style lang="scss" scoped>
-.edit-container {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  z-index: 100;
-  width: 407px;
-  height: 602px;
-  background: url('@/assets/images/videoTagEdit/box1.png') no-repeat;
+.title-box {
+  width: 379px;
+  height: 39px;
+  background: url('@/assets/images/map/titleBox3.png') no-repeat;
   background-size: 100% 100%;
-  .edit-header {
-    height: 40px;
+  padding-left: 22px;
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+    padding-top: 6px;
+    padding-left: 3px;
+  }
+}
+.tags {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  margin-top: -5px;
+  .tag {
+    background: url('@/assets/images/map/tag2.png') no-repeat;
+  }
+  .tagActive {
+    background: url('@/assets/images/map/tag.png') no-repeat;
+  }
+  .tag,
+  .tagActive {
+    min-width: 146px;
+    height: 30px;
+    background-size: 100% 100%;
     display: flex;
-    justify-content: space-between;
     align-items: center;
-    padding: 0 20px;
-    position: relative;
-    .title {
-      color: transparent;
-      background-image: linear-gradient(to bottom, #ffffff 30%, #edf7fe 50%, #5cc4fa 70%, #40a2e7 100%);
-      -webkit-background-clip: text;
-      background-clip: text;
-      display: inline-block;
-      font-family: 'YouSheBiaoTiHei';
-      font-size: 24px;
-    }
-    .close {
-      width: 16px;
-      height: 16px;
-      background: url('@/assets/images/videoTagEdit/close.png') no-repeat;
-      background-size: 100% 100%;
-      cursor: pointer;
-    }
-    &::before {
-      content: '';
-      width: 43px;
-      height: 18px;
-      background: url('@/assets/images/videoTagEdit/line1.png') no-repeat;
-      background-size: 100% 100%;
-      position: absolute;
-      bottom: -8px;
-      left: 0;
-    }
-    &::after {
-      content: '';
-      width: calc(100% - 83px);
-      height: 2px;
-      background: url('@/assets/images/videoTagEdit/line2.png') no-repeat;
-      background-size: 100% 100%;
-      position: absolute;
-      bottom: 0;
-      left: 47px;
-    }
+    justify-content: center;
+    margin-top: 6px;
+    padding: 0 6px 0 5px;
+    cursor: pointer;
   }
-  .edit-content {
-    margin-top: 10px;
-    height: 500px;
-    overflow-y: auto;
-    .title-box {
-      width: 379px;
-      height: 39px;
-      background: url('@/assets/images/map/titleBox3.png') no-repeat;
-      background-size: 100% 100%;
-      padding-left: 30px;
-      margin-top: 10px;
-      .title {
-        font-size: 16px;
-      }
-    }
-    .title-box2 {
-      width: 135px;
-      height: 24px;
-      background: url('@/assets/images/map/rightMenu/titleBox2.png') no-repeat bottom left;
-      background-size: 135px 20px;
-      padding-left: 30px;
-      margin-top: 10px;
-      .title {
-        font-size: 16px;
-        color: #96aac1;
-      }
-    }
-    .title-box3 {
-      display: flex;
-      align-items: center;
-      margin-top: 10px;
-      .title {
-        font-size: 16px;
-        color: #96aac1;
-        margin: 0 6px;
-      }
-      .icon-type {
-        width: 28px;
-        height: 26px;
-        background: url('@/assets/images/videoTagEdit/type.png') no-repeat;
-        background-size: 100% 100%;
-      }
-      .icon-industry {
-        width: 26px;
-        height: 27px;
-        background: url('@/assets/images/videoTagEdit/industry.png') no-repeat;
-        background-size: 100% 100%;
-      }
-    }
-    .tags {
-      display: flex;
-      flex-wrap: wrap;
-      align-items: center;
-      margin-top: -5px;
-      .tag {
-        min-width: 126px;
-        height: 26px;
-        background: url('@/assets/images/map/tag.png') no-repeat;
-        background-size: 100% 100%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        margin-top: 10px;
-        padding: 0 20px 0 15px;
-        cursor: pointer;
-      }
-      .custom-input {
-        width: 230px;
-        margin-left: 20px;
-        margin-top: 10px;
-      }
-    }
+  .custom-input {
+    width: 222px;
+    margin-left: 6px;
+    margin-top: 6px;
   }
 }
 </style>

+ 12 - 2
src/hooks/AMap/useAMap.ts

@@ -115,6 +115,13 @@ export function useAMap(options) {
   const addSearchMarker = (item) => {
     map.setZoom(18);
     map.setCenter(item.lnglat);
+    // 获取到上一次的搜索标记并移除
+    const index = addPoints.findIndex((m) => {
+      return m.dataType === 'search';
+    });
+    if (index > -1) {
+      addPoints.splice(index, 1);
+    }
     addMarker([item], true);
     clickMarker = item;
     options.onMarkerClick(item);
@@ -122,8 +129,11 @@ export function useAMap(options) {
   // 添加多个点
   const addMarker = (points, notClean?: boolean) => {
     if (!notClean) {
-      clearMarker('point');
+      addPoints = points;
+    } else {
+      addPoints.push(...points);
     }
+    clearMarker('point');
     addPoints = points;
     const count = points.length;
     const _renderClusterMarker = function (context) {
@@ -191,7 +201,7 @@ export function useAMap(options) {
     };
     cluster = new AMap.MarkerCluster(
       map, //地图实例
-      points, //海量点数据,数据中需包含经纬度信息字段 lnglat
+      addPoints, //海量点数据,数据中需包含经纬度信息字段 lnglat
       {
         gridSize: 30, //数据聚合计算时网格的像素大小
         renderClusterMarker: _renderClusterMarker, //上述步骤的自定义聚合点样式

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

@@ -27,13 +27,9 @@ declare module 'vue' {
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
-    ElCard: typeof import('element-plus/es')['ElCard']
-    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
-    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
-    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -44,35 +40,19 @@ declare module 'vue' {
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
-    ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
-    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
-    ElLink: typeof import('element-plus/es')['ElLink']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
-    ElOption: typeof import('element-plus/es')['ElOption']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
-    ElRadio: typeof import('element-plus/es')['ElRadio']
-    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
-    ElSegmented: typeof import('element-plus/es')['ElSegmented']
-    ElSelect: typeof import('element-plus/es')['ElSelect']
-    ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
-    ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
-    ElSlider: typeof import('element-plus/es')['ElSlider']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
-    ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
-    ElTimeline: typeof import('element-plus/es')['ElTimeline']
-    ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
-    ElTree: typeof import('element-plus/es')['ElTree']
-    ElUpload: typeof import('element-plus/es')['ElUpload']
     ExcelEditor: typeof import('./../components/ExcelEditor/index.vue')['default']
     FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
     FooterSection: typeof import('./../components/FooterSection/index.vue')['default']

+ 31 - 46
src/utils/olMap/olMap.ts

@@ -1,7 +1,7 @@
 // 引入OpenLayers的主模块
 import Map from 'ol/Map';
 import View from 'ol/View';
-import TileState from 'ol/TileState'
+import TileState from 'ol/TileState';
 import Feature from 'ol/Feature';
 import Point from 'ol/geom/Point';
 import VectorLayer from 'ol/layer/Vector';
@@ -191,17 +191,12 @@ export class olMap {
     // 添加新的图层
     if (Array.isArray(options.id)) {
       for (const layer of options.id) {
-        if (typeof layer === 'string') {
-          await this.formatXml(layer); // 等待当前 layer 处理完成
-        } else {
-          await this.formatXml(layer.id, layer.minZoom, layer.maxZoom, layer.zIndex, layer.visible); // 等待当前 layer 处理完成
-        }
+        await this.formatXml(layer);
       }
     } else if (options.id === 'tianditu') {
       await this.formatXml2();
     } else if (options.id) {
-      // 如果 options.id 不是数组,但确实是一个图层,则直接处理
-      await this.formatXml(options.id, options.minZoom, options.maxZoom, options.zIndex, options.visible);
+      await this.formatXml(options);
     }
     // 创建Vector层并添加到地图上
     this.vectorLayer = new VectorLayer({
@@ -238,18 +233,18 @@ export class olMap {
     }
     return `${baseUrl}${dataType}_${projType}/wmts?${paramsStr.slice(0, -1)}`;
   }
-  formatXml(code: string, minZoom?: number, maxZoom?: number, zIndex?: number, visible?: boolean) {
+  formatXml(options) {
     const xml = new WMTSCapabilities();
-    return this.getCapabilities(code).then((lists) => {
-      const geojson = xml.read(lists.data);
-      const data = geojson.Contents.Layer[0];
+    return this.getCapabilities(options.code).then((res) => {
+      const geoJson = xml.read(res.data);
+      const data = geoJson.Contents.Layer[0];
       const layerParam = {
         layerName: data.Abstract,
         styleName: data.Identifier,
         tilematrixset: data.TileMatrixSetLink[0].TileMatrixSet,
         format: data.Format[0]
       };
-      this.createWmsLayer(code, layerParam, minZoom, maxZoom, zIndex, visible);
+      this.createWmsLayer(options, layerParam);
     });
   }
 
@@ -261,9 +256,9 @@ export class olMap {
   }
 
   // 请求地图图片加载图层
-  createWmsLayer(code, layerParam, minZoom = 0, maxZoom, zIndex = -1, visible = true) {
+  createWmsLayer(options, layerParam) {
     const source = new WMTS({
-      url: commonUrl + code,
+      url: commonUrl + options.code,
       crossOrigin: 'Anonymous',
       layer: layerParam.layerName,
       style: layerParam.styleName,
@@ -275,13 +270,12 @@ export class olMap {
         resolutions: resolutions,
         matrixIds: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21']
       }),
-      // 关键修改:自定义瓦片加载函数
       tileLoadFunction: function (tile, src) {
         const xhr = new XMLHttpRequest();
         xhr.open('GET', src);
 
         // 添加自定义 Headers
-        const headers = globalHeaders()
+        const headers = globalHeaders();
         xhr.setRequestHeader('Authorization', headers.Authorization);
         xhr.setRequestHeader('clientid', headers.clientid);
 
@@ -303,15 +297,12 @@ export class olMap {
       }
     });
     const layer = new TileLayer({
-      name: code,
       source: source,
-      zIndex: zIndex,
-      minZoom: minZoom,
-      maxZoom,
-      visible: visible
+      zIndex: options.zIndex,
+      visible: options.visible
     });
-    layer.set('layerName', code);
-
+    layer.set('layerName', options.layer);
+    layer.set('id', options.code);
     this.map.addLayer(layer);
   }
 
@@ -582,32 +573,26 @@ export class olMap {
   // 切换图层
   async replaceLayers(newLayers, loadendFunc) {
     // 遍历当前的所有图层并移除它们
-    this.map.getLayers().forEach((layer) => {
-      this.map.removeLayer(layer);
+    const layers = this.map.getLayers();
+    const layerArray = layers.getArray().slice();
+    layerArray.forEach((layer) => {
+      // 标注现在都是用同一个暂不移除'annotation'
+      if (!!layer && ['map'].includes(layer.get('layerName'))) {
+        layer.getSource().clear();
+        layer.dispose();
+        this.map.removeLayer(layer);
+      }
     });
 
     if (Array.isArray(newLayers)) {
       for (const layer of newLayers) {
-        if (typeof layer === 'string') {
-          await this.formatXml(layer); // 等待当前 layer 处理完成
-        } else {
-          await this.formatXml(layer.id, layer.minZoom, layer.maxZoom, layer.zIndex, layer.visible); // 等待当前 layer 处理完成
-        }
+        await this.formatXml(layer);
       }
+    } else if (newLayers.id === 'tianditu') {
+      await this.formatXml2();
     } else {
-      // 如果 options.id 不是数组,但确实是一个图层,则直接处理
-      await this.formatXml(newLayers.id, newLayers.minZoom, newLayers.maxZoom, newLayers.zIndex, newLayers.visible);
+      await this.formatXml(newLayers);
     }
-    // 创建Vector层并添加到地图上
-    this.vectorLayer = new VectorLayer({
-      source: new VectorSource({
-        features: []
-      })
-    });
-    this.map.addLayer(this.vectorLayer);
-    const point = JSON.parse(JSON.stringify(this.markers));
-    this.markers = [];
-    this.addMarker(point);
     if (loadendFunc) {
       loadendFunc();
     }
@@ -734,7 +719,7 @@ export class olMap {
     this.map.addOverlay(this.infoWindow);
   }
 
-  hideInfo(flag) {
+  hideInfo(flag?: boolean) {
     this.map.removeOverlay(this.infoWindow);
     this.infoWindow = null;
     if (!!flag && this.select) {
@@ -743,7 +728,7 @@ export class olMap {
   }
   /**
    *
-   * @param {Geojon} chaozhou 根据geojson对象创建Featrue对象
+   * @param {Geojon} chaozhou 根据geoJson对象创建Featrue对象
    * @returns VectorLayer
    */
   createVecByJson(json, options) {
@@ -807,7 +792,7 @@ export class olMap {
             width: 2
           })
         }),
-        zIndex: options.zIndex ? options.zIndex : 99
+        zIndex: options.zIndex ? options.zIndex : -97
       });
       // // 合并区边界
       // const format = new GeoJSON();

+ 5 - 31
src/views/emergencyCommandMap/LeftSection/VideoMonitor.vue

@@ -4,10 +4,7 @@
     <div class="more-btn" @click="showVideoMonitorList">{{ '查看更多>>' }}</div>
     <div class="card-content video-list">
       <div v-for="(item, index) in listData" :key="index" class="video-box">
-        <HKVideo :dot_data="item" autoplay />
-        <div class="video-label">
-          <span class="label">{{ item.name }}</span>
-        </div>
+        <HKVideo :dot_data="item" autoplay @change="(data, index) => updateData(data, index)" />
       </div>
     </div>
   </div>
@@ -17,6 +14,7 @@
 <script lang="ts" setup name="VideoMonitor">
 import { getVideoListByUser } from '@/api/videoMonitor';
 import VideoMonitorEdit from './VideoMonitorEdit.vue';
+import HKVideo from '@/components/HKVideo/index2.vue';
 
 const props = defineProps({
   longitude: Number,
@@ -40,6 +38,9 @@ const initData = () => {
     listData.value = res.rows;
   });
 };
+const updateData = (data, index) => {
+  console.log(data, index, 'sdakfhasdkjfhskjdfhdskjh');
+}
 initData();
 
 const showVideoMonitorList = () => {
@@ -81,33 +82,6 @@ const showVideoMonitorList = () => {
       &:nth-child(4) {
         margin-right: 0;
       }
-      .video-label {
-        position: absolute;
-        bottom: 5px;
-        right: 3px;
-        z-index: 901;
-        display: flex;
-        .label {
-          width: 186px;
-          height: 22px;
-          line-height: 22px;
-          font-size: 14px;
-          white-space: nowrap;
-          overflow: hidden;
-          text-overflow: ellipsis;
-          padding: 0 5px 0 10px;
-          color: #fff;
-          background-color: rgba(18, 107, 248, 0.4);
-          text-align: right;
-        }
-        &::before {
-          content: '';
-          width: 0;
-          height: 0;
-          border-bottom: 22px solid rgba(18, 107, 248, 0.4);
-          border-left: 22px solid transparent;
-        }
-      }
     }
     :deep(.err_box) {
       align-items: flex-start;

+ 114 - 33
src/views/emergencyCommandMap/LeftSection/VideoMonitorEdit.vue

@@ -2,27 +2,62 @@
   <Dialog custom-show type="xl" title="视频监控" class="dialog" hide-footer draggable @close="reset">
     <div class="search-box">
       <div class="box-left">
-        <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="70px" label-position="left">
-          <el-form-item prop="name">
-            <el-input v-model="queryParams.name" class="custom-input2" placeholder="请输入摄像头名称" style="width: 500px" />
+        <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-position="left">
+          <el-form-item label="区划" prop="area" label-width="30px">
+            <el-select
+              v-model="treeData.area"
+              class="custom-select"
+              popper-class="custom-select-popper"
+              :teleported="false"
+              style="width: 150px; margin-left: 10px"
+              @change="handleAreaChange"
+            >
+              <el-option v-for="item in treeData.areaList" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
+            <el-select
+              v-model="treeData.town"
+              class="custom-select"
+              popper-class="custom-select-popper"
+              :teleported="false"
+              style="width: 150px; margin-left: 10px"
+              @change="handleTownChange"
+            >
+              <el-option v-for="item in treeData.townList" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
+            <el-select
+              v-model="treeData.village"
+              class="custom-select"
+              popper-class="custom-select-popper"
+              :teleported="false"
+              style="width: 150px; margin-left: 10px"
+              @change="handleVillageChange"
+            >
+              <el-option v-for="item in treeData.villageList" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
           </el-form-item>
-          <el-form-item label="实景视频" prop="dict_value">
+          <el-form-item label="实景视频" prop="dict_value" label-width="70px">
             <el-select
               v-model="queryParams.dict_value"
               class="custom-select"
               popper-class="custom-select-popper"
               :teleported="false"
+              style="width: 120px"
             >
               <el-option v-for="item in video_type" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
           </el-form-item>
-          <el-form-item>
+          <el-form-item prop="name">
+            <el-input v-model="queryParams.name" class="custom-input2" placeholder="请输入摄像头名称" style="width: 313px" />
+          </el-form-item>
+          <el-form-item style="margin-right: 0">
             <div class="common-btn-primary" @click="handleQuery">搜索</div>
             <div class="common-btn" @click="resetQuery">重置</div>
           </el-form-item>
         </el-form>
       </div>
-      <div v-show="!editVideo" class="common-btn-primary2 edit-icon" style="margin-top: -20px" @click="activeEdit">编辑首页视频</div>
+    </div>
+    <div style="display: flex; justify-content: flex-end; margin-bottom: 12px; margin-top: -20px">
+      <div v-show="!editVideo" class="common-btn-primary2 edit-icon" @click="activeEdit">编辑首页视频</div>
       <div v-show="editVideo" class="edit-box">
         <div class="flex">
           <div v-for="(item, index) in editData" :key="index" class="box-item">
@@ -33,7 +68,7 @@
           </div>
         </div>
         <div class="flex" style="flex-direction: column; align-items: center">
-          <div class="common-btn-primary3" @click="handleSave">保存</div>
+          <div class="common-btn-primary3" style="margin-top: -25px" @click="handleSave">保存</div>
           <div class="common-btn-danger2" @click="handleCancel">取消</div>
         </div>
       </div>
@@ -41,16 +76,16 @@
     <div class="border"></div>
     <div class="video-list2">
       <div v-for="(item, index) in dialogListData" :key="index" class="video-box" @click="selectItem(item)">
-        <div class="video-label">
-          <span class="label">{{ item.name }}</span>
-        </div>
         <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center">
           <div v-if="editVideo">
-            <div v-if="item.sort" class="active-tag"></div>
-            <div :class="item.sort ? 'common-checked-active' : 'common-checked'"></div>
+            <div v-if="getCheckStatus(item.video_code)" class="active-tag"></div>
+            <div :class="getCheckStatus(item.video_code) ? 'common-checked-active' : 'common-checked'"></div>
             <div class="img"></div>
+            <div class="video-label">
+              <span class="label">{{ item.name }}</span>
+            </div>
           </div>
-          <HKVideo v-else :dot_data="item" autoplay />
+          <HKVideo v-else :dot_data="item" autoplay :is-index="item.sort" />
         </div>
       </div>
     </div>
@@ -63,7 +98,7 @@
         layout="total, prev, pager, next"
         @pagination="getList"
       />
-      <div v-if="total === 0" style="width: 100%; text-align: center; font-size: 18px; font-weight: bold">暂无数据</div>
+      <div v-if="total === 0" style="width: 100%; text-align: center; font-size: 38px; font-weight: bold">暂无数据</div>
     </div>
     <div id="container" style="display: none"></div>
   </Dialog>
@@ -75,6 +110,9 @@ import { getEmergencyVideoCata, getUserVideoPoints, getVideoList, updateUserVide
 import { deepClone } from '@/utils';
 import useAppStore from '@/store/modules/app';
 import useMapStore from '@/store/modules/map';
+import HKVideo from '@/components/HKVideo/index2.vue';
+import { getRegionalTree } from '@/api/PreventionResponsible';
+
 interface LngLat {
   longitude: number;
   latitude: number;
@@ -97,11 +135,20 @@ const { video_type } = toRefs<any>(proxy?.useDict('video_type'));
 
 //查看更多数据
 const queryFormRef = ref();
+const treeData = reactive({
+  area: '',
+  town: '',
+  village: '',
+  areaList: [],
+  townList: [],
+  villageList: []
+});
 const queryParams = reactive({
   current: 1,
   size: 8,
   dict_value: '',
-  name: ''
+  name: '',
+  area: ''
 });
 let total = ref(0);
 let editVideo = ref(false);
@@ -122,7 +169,7 @@ watch(
   { immediate: true }
 );
 
-const getList = () => {
+const getList = (flag?: boolean) => {
   let newParams = {
     latitude: props.lngLat.latitude,
     longitude: props.lngLat.longitude,
@@ -138,9 +185,10 @@ const getList = () => {
   getEmergencyVideoCata(newParams).then((res) => {
     dialogListData.value = res.rows;
     total.value = res.total;
-    if (editVideo.value && editData.value) {
-      filterData(editData.value);
-    }
+    getUserVideoPoints().then((res2) => {
+      filterData(res2.data.videoInfos);
+      editData.value = deepClone(res2.data.videoInfos);
+    });
   });
 };
 const getVideoInfoList = () => {
@@ -165,9 +213,6 @@ const deleteItem = (index) => {
 };
 /** 表单重置 */
 const reset = () => {
-  queryParams.current = 1;
-  queryParams.dict_value = '';
-  queryParams.name = '';
   emits('update:modelValue', false);
 };
 
@@ -181,16 +226,18 @@ const handleQuery = () => {
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields();
+  treeData.area = '';
+  treeData.townList = [];
+  treeData.town = '';
+  treeData.villageList = [];
+  treeData.village = '';
+  queryParams.current = 1;
   handleQuery();
 };
 
 // 开启编辑视频
 const activeEdit = () => {
-  getUserVideoPoints().then((res) => {
-    filterData(res.data.videoInfos);
-    editData.value = deepClone(res.data.videoInfos);
-    editVideo.value = true;
-  });
+  editVideo.value = true;
 };
 const filterData = (data) => {
   data.forEach((item) => {
@@ -213,13 +260,46 @@ const handleSave = () => {
     data.push(item.video_code_int);
   });
   updateUserVideoPoints(data).then(() => {
-    getList();
+    getList(true);
     handleCancel();
   });
 };
-
+// 区变化
+const handleAreaChange = () => {
+  getAreaTreeList('townList', treeData.area);
+  treeData.town = '';
+  treeData.village = '';
+  queryParams.area = treeData.area;
+};
+// 镇变化
+const handleTownChange = () => {
+  getAreaTreeList('villageList', treeData.town);
+  treeData.village = '';
+  queryParams.area = treeData.town;
+};
+// 村变化
+const handleVillageChange = () => {
+  queryParams.area = treeData.village;
+};
+// 获取区划树
+const getAreaTreeList = (datasource, id) => {
+  getRegionalTree(id).then((res) => {
+    treeData[datasource] = res.data;
+  });
+};
+const getCheckStatus = (id) => {
+  let flag = false;
+  for (let i = 0; i < editData.value.length; i++) {
+    if (editData.value[i].video_code_int === id) {
+      flag = true;
+      break;
+    }
+  }
+  return flag;
+};
 onMounted(() => {
   getList();
+  getAreaTreeList('areaList', 2);
   // getVideoInfoList();
 });
 </script>
@@ -230,6 +310,7 @@ onMounted(() => {
   width: 100%;
   justify-content: space-between;
   align-items: center;
+  margin-top: -10px;
   .el-form--inline .el-form-item {
     margin-right: 15px;
   }
@@ -312,9 +393,9 @@ onMounted(() => {
   height: 83px;
   background: url('@/assets/images/video/editBg.png') no-repeat;
   background-size: 100% 100%;
-  position: absolute;
-  right: 20px;
-  top: 27px;
+  //position: absolute;
+  //right: 20px;
+  //top: 27px;
 }
 .box-item {
   position: relative;
@@ -396,7 +477,7 @@ onMounted(() => {
   background-color: #15428d;
   width: 100%;
   height: 1px;
-  margin-bottom: 30px;
+  margin-bottom: 15px;
 }
 .edit-icon {
   display: flex;

+ 12 - 17
src/views/globalMap/LeftMenu.vue

@@ -13,7 +13,7 @@
           </div>
           <div class="search-list">
             <div class="scroll">
-              <div v-for="(item, index) in searchState.resultList" :key="index" class="list-item" @click="selectSearchMarker(item)">
+              <div v-for="(item, index) in searchState.resultList" :key="index" class="list-item" @click="selectSearchMarker(item, 'gd')">
                 <i class="icon1" />
                 <div class="text1" :title="item.name">{{ item.name }}</div>
                 <!--              <div class="text2">{{ item.address }}</div>-->
@@ -112,20 +112,7 @@
         </div>
       </Transition>
     </div>
-    <Dialog
-      v-if="showDialog"
-      v-model="showDialog"
-      type="lg"
-      header-type="header2"
-      title="视频"
-      height="720px"
-      :get-tag-id="videoMonitorData.video_code"
-      hide-footer
-    >
-      <div style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center">
-        <HKVideo :dot_data="videoMonitorData" />
-      </div>
-    </Dialog>
+    <video-dialog v-if="showDialog" v-model="showDialog" :videoMonitorData="videoMonitorData" height="680px" />
   </div>
 </template>
 
@@ -133,7 +120,9 @@
 import { getPointInfoComprehensiveSearch } from '@/api/globalMap';
 import { listMenu2 } from '@/api/system/menu';
 import { deepClone } from '@/utils';
+import VideoDialog from '@/components/HKVideo/video-dialog.vue';
 import { onClickOutside } from '@vueuse/core';
+import gcoord from 'gcoord';
 
 const emits = defineEmits(['switchMap', 'clickMenu', 'selectSearchMarker']);
 const getPlaceSearch = inject('getPlaceSearch');
@@ -186,9 +175,15 @@ const changeSearchText = () => {
 };
 
 // 点击搜索结果,添加标注
-const selectSearchMarker = (item) => {
+const selectSearchMarker = (item, type) => {
+  const data = deepClone(item);
+  if (type === 'gd') {
+    const lnglat = gcoord.transform([data.longitude, data.latitude], gcoord.GCJ02, gcoord.WGS84);
+    data.longitude = lnglat[0];
+    data.latitude = lnglat[1];
+  }
   searchState.showList = false;
-  emits('selectSearchMarker', item);
+  emits('selectSearchMarker', data);
 };
 
 const menuState = reactive({

+ 3 - 10
src/views/globalMap/index.vue

@@ -114,14 +114,6 @@ const addMarkers = (item) => {
       item.checked2 = true;
       pointType.value.push(item);
     }
-    const path = [];
-    pointType.value.forEach((item) => {
-      path.push(item.component);
-    });
-    if (!path.includes('43') && addMarkersTimer) {
-      clearInterval(addMarkersTimer);
-      addMarkersTimer = null;
-    }
     addMarkersMethod();
   }
 };
@@ -161,7 +153,7 @@ const addMarkersMethod = () => {
     markerList.value = adjustPoint(data);
     dom?.addMarker(data);
   });
-  if (addMarkersTimer) {
+  if (!path.includes('43') && addMarkersTimer) {
     clearInterval(addMarkersTimer);
     addMarkersTimer = null;
   }
@@ -200,7 +192,8 @@ const clickMenu = (item, dataList) => {
         '路网视频',
         '江湖河库',
         '防溺水',
-        '森林防火',
+        // '森林防火',
+        '森防视频',
         '防灾救援',
         '救援队伍',
         '交通局视频',

+ 99 - 61
src/views/routineCommandMap/PositionMap.vue

@@ -1,65 +1,88 @@
 <template>
-  <Dialog type="md" title="请选择事发地点" height="780px" customShow @close="handleClose" @confirm="submit">
-    <el-form ref="queryFormRef" :model="form" :rules="rules">
-      <div class="form">
-        <div class="line">
-          <div class="form-item" style="margin-right: 10px">
-            <div class="text">详细地址</div>
-            <el-input v-model="form.address" class="custom-input" placeholder="请输入" />
-            <div v-if="searchPop" class="scroll_box">
-              <div style="height: 30px; line-height: 30px">
-                <span style="font-weight: bold">搜索结果列表</span>
-                <i class="el-icon-close" style="float: right; font-size: 12px; cursor: pointer" @click="closeSearchList()" />
-              </div>
+  <Dialog type="md" title="请选择事发地点" draggable height="780px" customShow @close="handleClose" @confirm="submit">
+    <div class="custom-dialog">
+      <el-form ref="queryFormRef" :model="form" :rules="rules">
+        <div class="form">
+          <div class="line">
+            <div class="form-item" style="margin-right: 20px">
+              <div class="text">灾害事件</div>
+              <el-input v-model="form.event_title" class="custom-input" placeholder="请输入" />
+            </div>
+          </div>
+          <div class="line">
+            <div class="form-item" style="margin-right: 20px">
+              <div class="text">灾害级别</div>
+              <el-select
+                v-model="form.event_level"
+                placeholder="请选择"
+                class="custom-select"
+                popper-class="custom-select-popper"
+                :teleported="false"
+                clearable
+              >
+                <el-option v-for="item in mm_event_level" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+            </div>
+          </div>
+          <div class="line">
+            <div class="form-item" style="margin-right: 10px">
+              <div class="text">详细地址</div>
+              <el-input v-model="form.address" class="custom-input" placeholder="请输入" />
+              <div v-if="searchPop" class="scroll_box">
+                <div style="height: 30px; line-height: 30px">
+                  <span style="font-weight: bold">搜索结果列表</span>
+                  <i class="el-icon-close" style="float: right; font-size: 12px; cursor: pointer" @click="closeSearchList()" />
+                </div>
 
-              <el-scrollbar class="scroll">
-                <div v-for="(item, index) in searchList" v-show="searchList.length" :key="index" class="item" @click="handlePanTo(index)">
-                  <el-image class="img" :src="item.img" :alt="item.name" lazy>
-                    <template #error>
-                      <div class="image-slot">
-                        <i class="el-icon-picture-outline"></i>
-                      </div>
-                    </template>
-                  </el-image>
-                  <div>
-                    <div class="text">{{ item.name }}</div>
-                    <div>{{ item.address }}</div>
+                <el-scrollbar class="scroll">
+                  <div v-for="(item, index) in searchList" v-show="searchList.length" :key="index" class="item" @click="handlePanTo(index)">
+                    <el-image class="img" :src="item.img" :alt="item.name" lazy>
+                      <template #error>
+                        <div class="image-slot">
+                          <i class="el-icon-picture-outline"></i>
+                        </div>
+                      </template>
+                    </el-image>
+                    <div>
+                      <div class="text">{{ item.name }}</div>
+                      <div>{{ item.address }}</div>
+                    </div>
                   </div>
-                </div>
-                <div v-show="!searchList.length" class="empty" style="text-align: center">没有搜索到内容</div>
-              </el-scrollbar>
+                  <div v-show="!searchList.length" class="empty" style="text-align: center">没有搜索到内容</div>
+                </el-scrollbar>
 
-              <el-pagination
-                background
-                small
-                :hide-on-single-page="true"
-                layout="prev, pager, next"
-                :total="total"
-                :page-size="pageSize"
-                :current-page="pageNum"
-                style="margin-top: 10px"
-                @current-change="handleChangePage"
-              >
-              </el-pagination>
+                <el-pagination
+                  background
+                  small
+                  :hide-on-single-page="true"
+                  layout="prev, pager, next"
+                  :total="total"
+                  :page-size="pageSize"
+                  :current-page="pageNum"
+                  style="margin-top: 10px"
+                  @current-change="handleChangePage"
+                >
+                </el-pagination>
+              </div>
             </div>
+            <div class="common-btn-primary" @click="handleInput(0)">搜索</div>
           </div>
-          <div class="common-btn-primary" @click="handleInput(0)">搜索</div>
-        </div>
-        <div class="line">
-          <div class="form-item">
-            <div class="text">经度</div>
-            <el-input v-model="form.longitude" class="custom-input" placeholder="请输入" />
-          </div>
-          <div class="form-item" style="margin-left: 80px">
-            <div class="text">详细地址</div>
-            <el-input v-model="form.latitude" class="custom-input" placeholder="请输入" />
+          <div class="line">
+            <div class="form-item">
+              <div class="text" style="width: 72px; text-align: right">经度</div>
+              <el-input v-model="form.longitude" class="custom-input" placeholder="请输入" />
+            </div>
+            <div class="form-item">
+              <div class="text" style="width: 72px; text-align: right">纬度</div>
+              <el-input v-model="form.latitude" class="custom-input" placeholder="请输入" />
+            </div>
           </div>
         </div>
-      </div>
-    </el-form>
-    <div ref="containerRef" class="map_box">
-      <div id="positionMap">
-        <div id="map" class="map" :style="{ width: width, height: height }"></div>
+      </el-form>
+      <div ref="containerRef" class="map_box">
+        <div id="positionMap">
+          <div id="map" class="map" :style="{ width: width, height: height }"></div>
+        </div>
       </div>
     </div>
   </Dialog>
@@ -78,6 +101,8 @@ const props = defineProps({
     }
   }
 });
+const proxy = getCurrentInstance()?.proxy;
+const { mm_event_level } = toRefs(proxy?.useDict('mm_event_level'));
 const router = useRouter();
 const emits = defineEmits(['update:visible']);
 // 地图对象
@@ -86,11 +111,6 @@ let amap = {};
 let marker = null; //地图上的点标记
 let contextMenu = null;
 let lnglatPosition = ref([]); //选中的新坐标
-let rules = reactive({
-  address: [{ required: true, message: '详细地址不能为空', trigger: 'blur' }],
-  longitude: [{ required: true, message: '经度不能为空', trigger: 'blur' }],
-  latitude: [{ required: true, message: '纬度不能为空', trigger: 'blur' }]
-});
 let pageNum = ref(1);
 let pageSize = ref(10);
 let total = ref(0);
@@ -300,6 +320,7 @@ function handleClose() {
 let queryFormRef = ref();
 let containerRef = ref();
 function handleResize() {
+  debugger
   const containerWidth = containerRef.value.clientWidth * (document.body.clientWidth / 1920);
   const containerHeight = containerRef.value.clientHeight * (document.body.clientHeight / 1080);
   width.value = containerWidth + 'px';
@@ -307,6 +328,22 @@ function handleResize() {
   map.resize();
 }
 function submit() {
+  if (!form.address) {
+    proxy('详细地址不能为空');
+  } else if (!form.longitude) {
+    showErrorMsg('经度不能为空');
+  } else if (!form.latitude) {
+    showErrorMsg('纬度不能为空');
+  } else {
+    addEvent(form).then((res) => {
+      router.push({
+        path: '/emergencyCommandMap',
+        query: {
+          event_id: res.data
+        }
+      });
+    });
+  }
   queryFormRef.value.validate((valid) => {
     if (valid) {
       console.log('提交数据', form);
@@ -447,8 +484,8 @@ function submit() {
 .empty {
   margin: 20px 0;
 }
-:deep(.dialog-content) {
-  flex: 1;
+.custom-dialog {
+  height: 100%;
   padding: 10px 0;
   display: flex;
   flex-direction: column;
@@ -456,6 +493,7 @@ function submit() {
     overflow: hidden;
   }
 }
+
 .form {
   .line {
     width: 100%;

+ 96 - 0
src/views/videoTag/videoAdd.vue

@@ -0,0 +1,96 @@
+<template>
+  <div class="common-dialog">
+    <div class="common-dialog-content">
+      <div class="common-dialog-title-box">
+        <i class="common-dialog-title-icon" />
+        <div>{{ props.id ? '修改视频标签' : '新增视频标签' }}</div>
+      </div>
+      <div class="common-dialog-box">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+          <el-form-item label="标签名称:" prop="dict_label">
+            <el-input v-model="form.dict_label" placeholder="请输入标签名称" style="width: 468px !important" />
+          </el-form-item>
+        </el-form>
+        <div class="common-dialog-footer">
+          <el-button @click="closeDialog">取消</el-button>
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm(formRef)">确定</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, reactive, toRefs } from 'vue';
+import { createVideoTag, getVideoTagInfo, updateVideoTag } from '@/api/viedoTag/viedoManagement';
+const emits = defineEmits(['close']);
+const props = defineProps({
+  id: String
+});
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { show_status, material_category_level } = toRefs<any>(proxy?.useDict('show_status', 'material_category_level'));
+const formRef = ref();
+const buttonLoading = ref(false);
+
+// 表单数据
+const data = reactive({
+  form: {
+    dict_label: ''
+  },
+  rules: {
+    dict_label: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }]
+  }
+});
+
+const { form, rules } = toRefs(data);
+
+const closeDialog = () => {
+  emits('close');
+};
+const submitForm = async (formEl) => {
+  if (!formEl) return;
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      if (props.id) {
+        updateVideoTag(form.value, props.id).then(() => {
+          proxy.$modal.msgSuccess('修改成功');
+          emits('close', true);
+        });
+      } else {
+        createVideoTag(form.value).then(() => {
+          proxy.$modal.msgSuccess('新增成功');
+          emits('close', true);
+        });
+      }
+    } else {
+      nextTick(() => {
+        let isError = document.getElementsByClassName('is-error');
+        isError[0].scrollIntoView({
+          // 滚动到指定节点
+          // 值有start,center,end,nearest,当前显示在视图区域中间
+          block: 'center',
+          // 值有auto、instant,smooth,缓动动画(当前是慢速的)
+          behavior: 'smooth'
+        });
+      });
+      proxy.$modal.msgError('表单校验失败');
+      return false;
+    }
+  });
+};
+
+onMounted(() => {
+  if (!props.id) return;
+  getVideoTagInfo(props.id).then((res) => {
+    form.value = res.data;
+  });
+});
+</script>
+
+<style lang="scss" scoped>
+.flex {
+  display: flex;
+  span {
+    white-space: nowrap;
+  }
+}
+</style>

+ 113 - 0
src/views/videoTag/viedoManagement.vue

@@ -0,0 +1,113 @@
+<template>
+  <div>
+    <div v-show="!videoAddState.show" class="app-container">
+      <div>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" icon="Plus" @click="handleAdd">新建</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete(selectedRow)"> 删除 </el-button>
+          </el-col>
+        </el-row>
+        <!-- 表格组件 -->
+        <el-table ref="multipleTable" v-loading="loading" :data="tableData" @selection-change="handleSelectionChange">
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column label="视频标签" align="center" prop="dictLabel" />
+          <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+            <template #default="scope">
+              <el-text class="common-btn-text-primary" @click="handleUpdate(scope.row)">编辑</el-text>
+              <el-text class="common-btn-text-danger" @click="handleDelete(scope.row)">删除</el-text>
+            </template>
+          </el-table-column>
+        </el-table>
+        <pagination
+          v-show="total > 0"
+          v-model:page="queryParams.page"
+          v-model:limit="queryParams.pageSize"
+          :total="total"
+          @pagination="fetchWorkrData"
+        />
+      </div>
+    </div>
+    <VideoAdd v-if="videoAddState.show" :event-id="videoAddState.eventId" @close="handleCancel" @refresh="fetchWorkrData" />
+  </div>
+</template>
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue';
+import { to } from 'await-to-js';
+import VideoAdd from '@/views/videoTag/videoAdd.vue';
+import { getTagList } from '@/api/viedoTag/viedoManagement';
+const loading = ref(true);
+const showSearch = ref(true);
+const multiple = ref(true);
+const ids = ref<Array<number | string>>([]);
+const single = ref(true);
+const total = ref(0);
+const tableData = ref([]);
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const initFormData = reactive({
+  dictcode: '',
+  dictLabel: ''
+});
+const data = reactive({
+  form: { ...initFormData },
+  queryParams: {
+    page: 1,
+    pageSize: 10,
+    dictLabel: ''
+  }
+});
+const { queryParams, form } = toRefs(data);
+let videoAddState = reactive({
+  show: false,
+  eventId: ''
+});
+const handleCancel = () => {
+  videoAddState.show = false;
+};
+
+const handleAdd = () => {
+  videoAddState.show = true;
+};
+const handleUpdate = (row) => {
+  if (row) {
+    videoAddState.eventId = row.dictcode + '';
+    videoAddState.show = true;
+  }
+};
+const handleDelete = async (row) => {
+  let id = [];
+  if (row) {
+    id = [row.dictcode];
+  } else {
+    id = ids.value;
+  }
+  const [err] = await to(proxy?.$modal.confirm('此标签已被摄像头使用,是否确认删除?') as any);
+  if (!err) {
+    await workDelete(id);
+    proxy.$modal.msgSuccess('删除成功');
+    fetchWorkrData();
+  }
+};
+const fetchWorkrData = () => {
+  loading.value = true;
+  getTagList(queryParams.value)
+    .then((res) => {
+      tableData.value = res.data;
+      total.value = res.total;
+    })
+    .finally(() => {
+      loading.value = false;
+    });
+};
+const handleSelectionChange = (selection) => {
+  ids.value = selection.map((item) => item.dictcode);
+  selectedRow.value = selection.length === 1 ? selection[0] : null;
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+};
+onMounted(() => {
+  fetchWorkrData();
+});
+</script>