chauncey 1 год назад
Родитель
Сommit
b29ddc78a3

+ 1 - 1
src/api/camera/camera-preview.ts

@@ -360,7 +360,7 @@ export const deletePresetApi = (data: { presetToken: string; cameraId: number })
   });
 };
 
-interface PresetDetailItem {
+export interface PresetDetailItem {
   name: string;
   token: string;
   ptzposition: {

Разница между файлами не показана из-за своего большого размера
+ 11 - 0
src/assets/icons/delete-preset-position.svg


Разница между файлами не показана из-за своего большого размера
+ 14 - 0
src/assets/icons/edit-preset-position-focus.svg


Разница между файлами не показана из-за своего большого размера
+ 14 - 0
src/assets/icons/edit-preset-position.svg


+ 8 - 0
src/assets/icons/fullscreen.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745213618167"
+    class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3201" width="24"
+    height="24" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <path
+        d="M598.016 214.016l212.010667 0 0 212.010667-84.010667 0 0-128-128 0 0-84.010667zM726.016 726.016l0-128 84.010667 0 0 212.010667-212.010667 0 0-84.010667 128 0zM214.016 425.984l0-212.010667 212.010667 0 0 84.010667-128 0 0 128-84.010667 0zM297.984 598.016l0 128 128 0 0 84.010667-212.010667 0 0-212.010667 84.010667 0z"
+        fill="#000000" p-id="3202"></path>
+</svg>

Разница между файлами не показана из-за своего большого размера
+ 35 - 0
src/assets/icons/preset-placeholder-img.svg


+ 6 - 1
src/modules/algo-params-setting-base/components/CameraDirectionControl/CameraDirectionControl.vue

@@ -19,22 +19,27 @@
   const cameraDetailStore = useCameraDetailStore();
   const { cameraId } = storeToRefs(cameraDetailStore);
   const presetListStore = usePresetListStore();
+  const emits = defineEmits(['update:ControlPerspective']);
 
-  const STEP = 0.05;
+  const STEP = 0.025;
 
   const handleMoveTop = () => {
+    emits('update:ControlPerspective');
     cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: 0, y: STEP });
     presetListStore.currentPresetToken = '';
   };
   const handleMoveRight = () => {
+    emits('update:ControlPerspective');
     cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: STEP, y: 0 });
     presetListStore.currentPresetToken = '';
   };
   const handleMoveBottom = () => {
+    emits('update:ControlPerspective');
     cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: 0, y: -STEP });
     presetListStore.currentPresetToken = '';
   };
   const handleMoveLeft = () => {
+    emits('update:ControlPerspective');
     cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: -STEP, y: 0 });
     presetListStore.currentPresetToken = '';
   };

+ 4 - 1
src/modules/algo-params-setting-base/components/CameraViewSetting/CameraViewScale.vue

@@ -21,9 +21,12 @@
   const cameraDetailStore = useCameraDetailStore();
   const { cameraId } = storeToRefs(cameraDetailStore);
 
-  const zoomStep = 0.05;
+  const zoomStep = 0.025;
+
+  const emits = defineEmits(['update:ControlPerspective']);
 
   const handleEnlarge = () => {
+    emits('update:ControlPerspective');
     cameraMoveApi({ cameraId: cameraId.value, zoom: zoomStep, x: 0, y: 0 });
     // presetListStore.currentPresetToken = '';
   };

+ 2 - 1
src/modules/algo-params-setting-base/components/CameraViewSetting/CameraViewSetting.vue

@@ -31,10 +31,11 @@
       <div> <AlgoCanSelect :selected-ids="cameraAlgoIds" @select="handleApplyAlgo" /></div>
     </div>
 
+    <!-- 取消这个功能 全部移植到设备管理-相机设备-相机预览功能里 -->
     <div
       class="presetAddWrapper"
       :class="{ hidePresetControlCls: showFenceTool }"
-      v-if="!!cameraDetailStore.detail?.isPtz"
+      v-if="!!cameraDetailStore.detail?.isPtz && false"
     >
       <CameraViewScale />
       <CameraDirectionControl />

+ 20 - 2
src/views/cameras/overview/CamerasOverview.vue

@@ -131,6 +131,12 @@
     <EditNVRCamera class="add-popover" v-model="showEditNVRPopover" :edit-data="editCameraData" />
     <ShareCamera class="add-popover" v-model="addSharedPopover" :share-data="shareCameraData" />
     <EditSharedCamera class="add-popover" v-model="showSharedPopover" @update-unadd="updateUnaddAmount" />
+    <CameraOverviewPopover
+      v-if="showCameraOverviewPopover"
+      :dialog-visible="showCameraOverviewPopover"
+      :overview-camera-data="overviewCameraData!"
+      @update:dialogVisible="showCameraOverviewPopover = false"
+    />
   </div>
 </template>
 
@@ -148,6 +154,7 @@
   import EditSRSCamera from './components/CameraEditSRSPopover.vue';
   import EditNVRCamera from './components/CameraEditNVRPopover.vue';
   import EditSharedCamera from './components/CameraSharedEdit.vue';
+  import CameraOverviewPopover from './components/CameraOverviewPopover.vue';
   import emptyImg from '@/assets/images/table/table-empty.png';
   import { Plus, DocumentAdd, Edit, Tickets } from '@element-plus/icons-vue';
   import shareIcon from '@/assets/images/table/table-share.png';
@@ -158,9 +165,9 @@
   import { storeToRefs } from 'pinia';
   import { CameraDetailServer } from '@/types/camera/type';
   import { deleteCameraItem, deleteCameraItems } from '@/api/camera/camera-overview';
+  import { getCameraDeatilById } from '@/api/camera/camera-preview';
   import { ElMessage, ElMessageBox } from 'element-plus';
   import useCameraShare from './hooks/useCameraShare';
-  import router from '@/router';
   import { AddType } from '@/types/camera/constant';
   import axios, { AxiosRequestConfig } from 'axios';
   import { getHeaders } from '@/utils/http/axios';
@@ -168,6 +175,8 @@
   import { useSceneTemplateList } from './stores/useSceneTemplateList';
   import { useUserStore } from '@/store/modules/user';
   import { PERM_DEVICE } from '@/types/permission/constants';
+  import useCameraDetailStore from '@/modules/algo-params-setting-base/store/useCameraDetailStore';
+  const cameraDetailStore = useCameraDetailStore();
 
   const userStore = useUserStore();
   const hasCameraAddPermission = () => {
@@ -190,6 +199,7 @@
 
   const useShare = useCameraShare();
   const { totalRow, queryToTenantId, isAddState, conditionSearch } = useShare;
+  const showCameraOverviewPopover = ref(false);
 
   onMounted(() => {
     isAddState.value = false;
@@ -215,6 +225,7 @@
   const showEditSRSPopover = ref(false);
   const showEditNVRPopover = ref(false);
   const editCameraData = ref<CameraDetailServer>();
+  const overviewCameraData = ref<CameraDetailServer>();
   const shareCameraData = ref<CameraDetailServer | null>();
   // 多选操作
   const showActionBar = ref(false);
@@ -260,6 +271,7 @@
             label: '预览',
             icon: previewIcon,
             onClick: handlePreview.bind(null, record.row),
+            disabled: record.row.networkingState !== 0 || record.row.integrationState !== 0,
           },
           {
             label: '删除',
@@ -396,7 +408,13 @@
   };
 
   const handlePreview = (_row) => {
-    router.push(`/algorithm/config?cameraId=${_row.id}`);
+    console.log('handlePreview', _row);
+    showCameraOverviewPopover.value = true;
+    overviewCameraData.value = _row;
+    cameraDetailStore.cameraId = _row.id;
+    getCameraDeatilById(_row.id).then((res) => {
+      cameraDetailStore.setDetail(res);
+    });
   };
 
   const handleDelete = (row) => {

+ 341 - 0
src/views/cameras/overview/components/CameraOverviewPopover.vue

@@ -0,0 +1,341 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    width="719"
+    :title="`预览-${overviewCameraData.name}`"
+    center
+    align-center
+    class="camera-overview-popover--custom"
+    :close-on-click-modal="false"
+    @close="emits('update:dialogVisible', false)"
+  >
+    <div class="camera-overview-popover--custom__content">
+      <main class="main">
+        <div class="cameraVideo">
+          <CameraLiveVideo />
+        </div>
+        <div class="presetAddWrapper" v-if="!!overviewCameraData.isPtz">
+          <CameraViewScale @update:ControlPerspective="activePresetToken = ''" />
+          <CameraDirectionControl @update:ControlPerspective="activePresetToken = ''" />
+          <ElButton type="primary" size="large" style="margin-top: 20px; width: 100px" @click="handleAddPreset"
+            >添加预置位</ElButton
+          >
+        </div>
+      </main>
+      <footer class="footer" v-if="!!overviewCameraData.isPtz && presetList.length > 0">
+        <div class="footer-header">
+          <ElTooltip :content="isEditMode ? '完成编辑' : '开始编辑'" placement="right">
+            <img
+              :src="isEditMode ? EditPresetPositionFocusIcon : EditPresetPositionIcon"
+              alt="编辑预置位"
+              @click="toggleEditMode"
+            />
+          </ElTooltip>
+          <div class="pagination-control" v-if="displayPresetList.length > 0">
+            <el-button type="text" :disabled="currentPage === 1" @click="prevPage" :icon="ArrowLeft" />
+            <span>{{ currentPage }}/{{ totalPages }}</span>
+            <el-button type="text" :disabled="currentPage === totalPages" @click="nextPage" :icon="ArrowRight" />
+          </div>
+        </div>
+        <div class="preset-position-list">
+          <div 
+            class="preset-position-item" 
+            v-for="item in currentPageItems" 
+            :key="item.token"
+            :class="{ 'active-preset': activePresetToken === item.token }"
+          >
+            <img 
+              :src="PresetPositionItem" 
+              alt="预置位" 
+              style="cursor: pointer" 
+              @click="handleGoToPreset(item)" 
+            />
+            <img
+              v-if="isEditMode"
+              :src="DeletePresetPositionIcon"
+              alt="删除预置位"
+              class="delete-preset-position-icon"
+              @click="handleDeletePreset(item)"
+            />
+            <span class="preset-position-name">{{ item.name }}</span>
+          </div>
+        </div>
+      </footer>
+    </div>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+  import { ref, watch, computed, onMounted } from 'vue';
+  import { CameraDetailServer } from '@/types/camera/type';
+  import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
+  import EditPresetPositionIcon from '@/assets/icons/edit-preset-position.svg';
+  import DeletePresetPositionIcon from '@/assets/icons/delete-preset-position.svg';
+  import PresetPositionItem from '@/assets/icons/preset-placeholder-img.svg';
+  import EditPresetPositionFocusIcon from '@/assets/icons/edit-preset-position-focus.svg';
+  import { getPresetListApi, PresetDetailItem, deletePresetApi, createPresetApi, goToPresetApi } from '@/api/camera/camera-preview';
+  import CameraLiveVideo from '@/modules/algo-params-setting-base/components/CameraLiveVideo/CameraLiveVideo.vue';
+  import CameraViewScale from '@/modules/algo-params-setting-base/components/CameraViewSetting/CameraViewScale.vue';
+  import CameraDirectionControl from '@/modules/algo-params-setting-base/components/CameraDirectionControl/CameraDirectionControl.vue';
+  import { ElMessage, ElMessageBox } from 'element-plus';
+  
+  const emits = defineEmits(['update:dialogVisible']);
+  const dialogVisible = ref(false);
+  const presetList = ref<PresetDetailItem[]>([]);
+  const displayPresetList = ref<PresetDetailItem[]>([]);
+  const isEditMode = ref(false);
+  const activePresetToken = ref<string>(''); // 当前激活的预置位token
+  
+  const props = defineProps<{
+    dialogVisible: boolean;
+    overviewCameraData: CameraDetailServer;
+  }>();
+  const currentPage = ref(1);
+  const pageSize = 5;
+  const presetPositionCount = computed(() => displayPresetList.value.length); // 总预置位数量
+
+  const totalPages = computed(() => Math.ceil(presetPositionCount.value / pageSize));
+
+  const currentPageItems = computed(() => {
+    const startIdx = (currentPage.value - 1) * pageSize;
+    const endIdx = Math.min(startIdx + pageSize, presetPositionCount.value);
+    return displayPresetList.value.slice(startIdx, endIdx);
+  });
+
+  const prevPage = () => {
+    if (currentPage.value > 1) {
+      currentPage.value--;
+    }
+  };
+
+  const nextPage = () => {
+    if (currentPage.value < totalPages.value) {
+      currentPage.value++;
+    }
+  };
+
+  const toggleEditMode = () => {
+    isEditMode.value = !isEditMode.value;
+  };
+
+  const handleGoToPreset = (item: PresetDetailItem) => {
+    const cameraId = props.overviewCameraData.id;
+    if (!cameraId) return;
+    
+    // 设置当前激活的预置位
+    activePresetToken.value = item.token;
+    
+    // 调用前往预置位的API
+    goToPresetApi({ presetToken: item.token, cameraId });
+  };
+
+  const handleDeletePreset = (item: PresetDetailItem) => {
+    const cameraId = props.overviewCameraData.id;
+    if (!cameraId) return;
+    const index = displayPresetList.value.findIndex((preset) => preset.token === item.token);
+    if (index !== -1) {
+      ElMessageBox.confirm(
+        '该预置位可能存在关联的电子围栏。删除该预置位将会删除对应的电子围栏信息,请确认是否删除?',
+        '删除确认',
+        {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+        },
+      ).then(async () => {
+        await deletePresetApi({ presetToken: item.token, cameraId });
+        ElMessage.success('删除成功');
+        displayPresetList.value.splice(index, 1);
+        const currentPageStartIdx = (currentPage.value - 1) * pageSize;
+        if (currentPageStartIdx >= displayPresetList.value.length && currentPage.value > 1) {
+          currentPage.value--;
+        }
+      });
+    }
+  };
+
+  const handleAddPreset = () => {
+    ElMessageBox.prompt('', '添加预置位', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      inputPlaceholder: '请输入预置位名称',
+      inputValidator: (value) => {
+        if (!value) {
+          return '预置位名称不能为空';
+        }
+        const isExist = presetList.value.find((item) => item.name === value);
+        if (isExist) {
+          return '预置位名称已存在';
+        }
+        return true;
+      },
+    }).then(async ({ value }) => {
+      const cameraId = props.overviewCameraData.id;
+      if (!cameraId) return;
+      const res = await createPresetApi({ presetName: value, cameraId });
+      if (res) {
+        ElMessage.success('添加预置位成功');
+        await getPresetList();
+      }
+    });
+  };
+
+  const getPresetList = async () => {
+    const cameraId = props.overviewCameraData.id;
+    if (!cameraId) return;
+    presetList.value = await getPresetListApi(cameraId);
+    displayPresetList.value = [...presetList.value];
+  };
+
+  onMounted(async () => {
+    await getPresetList();
+  });
+
+  watch(
+    () => props.dialogVisible,
+    (newVal) => {
+      dialogVisible.value = newVal;
+      if (newVal) {
+        // 每次对话框打开时,重置编辑状态
+        isEditMode.value = false;
+      }
+    },
+    { immediate: true },
+  );
+</script>
+
+<style lang="scss">
+  .camera-overview-popover--custom {
+    padding: 20px 24px;
+    border-radius: 8px;
+    box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05), 0px 6px 16px 0px rgba(0, 0, 0, 0.08),
+      0px 3px 6px -4px rgba(0, 0, 0, 0.12);
+    .el-dialog__header {
+      text-align: left;
+      color: rgba(0, 0, 0, 0.88);
+      font-weight: bold;
+      font-size: 16px;
+    }
+    &__title {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+    &__content {
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+      width: 100%;
+      height: 100%;
+
+      .main {
+        position: relative;
+        flex-shrink: 0;
+        flex-grow: 0;
+        width: 100%;
+        height: 377px;
+      }
+      .footer {
+        display: flex;
+        flex-direction: column;
+        gap: 9px;
+
+        .footer-header {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+
+          .pagination-control {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            font-size: 14px;
+            color: rgba(0, 0, 0, 0.88);
+          }
+        }
+
+        .preset-position-list {
+          display: flex;
+          width: 100%;
+          gap: 12px;
+
+          .preset-position-item {
+            text-align: center;
+            width: 120px;
+            position: relative;
+            transition: all 0.3s ease;
+            
+            &.active-preset {
+              transform: scale(1.05);
+              box-shadow: 0 0 8px 2px rgba(24, 144, 255, 0.6);
+              border-radius: 4px;
+              
+              &::after {
+                content: '';
+                position: absolute;
+                top: 0;
+                left: 0;
+                width: 100%;
+                height: 100%;
+                border: 2px solid #1890ff;
+                border-radius: 4px;
+                box-sizing: border-box;
+                pointer-events: none;
+              }
+              
+              .preset-position-name {
+                background: rgba(24, 144, 255, 0.8);
+              }
+            }
+            
+            .delete-preset-position-icon {
+              position: absolute;
+              top: -4px;
+              right: -4px;
+              width: 16px;
+              height: 16px;
+              cursor: pointer;
+              transition: all 0.3s ease;
+              &:hover {
+                scale: 1.2;
+              }
+            }
+
+            img {
+              width: 120px;
+              height: 68px;
+              object-fit: cover;
+            }
+
+            .preset-position-name {
+              color: #fff;
+              font-size: 12px;
+              background: rgba(0, 0, 0, 0.5);
+              padding: 2px 4px;
+              border-radius: 2px;
+              transition: all 0.3s ease;
+            }
+          }
+        }
+      }
+      .cameraVideo {
+        position: absolute;
+        top: 0;
+        left: 0;
+        z-index: 8;
+        background: #ccc;
+        width: 100%;
+        height: 100%;
+      }
+      .presetAddWrapper {
+        position: absolute;
+        bottom: -50px;
+        right: -30px;
+        flex-direction: column;
+        display: flex;
+        align-items: center;
+        z-index: 10;
+        transform: scale(0.6);
+      }
+    }
+  }
+</style>