Przeglądaj źródła

0726中建材补充功能需求 算法参数的修改

louhangfei 1 rok temu
rodzic
commit
0f7975f01c
36 zmienionych plików z 1240 dodań i 58 usunięć
  1. 10 0
      src/api/camera/camera-overview.ts
  2. 71 0
      src/api/camera/camera-preview.ts
  3. 12 0
      src/assets/icons/algo-setting.svg
  4. 13 0
      src/assets/icons/algo-switch.svg
  5. 15 0
      src/assets/icons/box-select-inner.svg
  6. BIN
      src/assets/icons/box-select-outer.png
  7. 19 0
      src/assets/icons/box-select-outer.svg
  8. 19 0
      src/assets/icons/delete.svg
  9. BIN
      src/assets/icons/scale-large.png
  10. BIN
      src/assets/icons/scale-small.png
  11. 3 0
      src/components/LiveVideo/LiveVideo.vue
  12. 15 1
      src/components/SvgIcon/SvgIcon.vue
  13. 7 3
      src/views/cameras/preview/CameraPreview.vue
  14. 29 0
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoAddBtn.vue
  15. 5 0
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoDeleteIcon.vue
  16. 11 0
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoSettingIcon.vue
  17. 139 0
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchCard.vue
  18. 19 0
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchCardBase.vue
  19. 29 0
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchIcon.vue
  20. 3 0
      src/views/cameras/preview/components/AlgoSwitchCard/WithTooltip.vue
  21. 5 2
      src/views/cameras/preview/components/AlgorithmsSetting/AddAlgoDialog.vue
  22. 3 6
      src/views/cameras/preview/components/AlgorithmsSetting/AlgoSettingCard.vue
  23. 79 9
      src/views/cameras/preview/components/AlgorithmsSetting/AlgorithmsSetting.vue
  24. 1 0
      src/views/cameras/preview/components/CameraDirectionControl/CameraDirectionControl.vue
  25. 51 0
      src/views/cameras/preview/components/CameraViewSetting/CameraViewScale.vue
  26. 46 18
      src/views/cameras/preview/components/CameraViewSetting/CameraViewSetting.vue
  27. 116 0
      src/views/cameras/preview/components/FenceAppSetting/FenceAppSetting.vue
  28. 5 0
      src/views/cameras/preview/components/FenceAppSetting/constants.ts
  29. 16 6
      src/views/cameras/preview/components/FenceToolbar/FenceToolbar.vue
  30. 27 0
      src/views/cameras/preview/components/FenceToolbar/ToggleFenceStatus.vue
  31. 1 0
      src/views/cameras/preview/components/PresetSelect/PresetSelect.vue
  32. 3 3
      src/views/cameras/preview/components/RenderSwitch/RenderSwitch.vue
  33. 5 3
      src/views/cameras/preview/components/ToolbarIcon/ToolbarIcon.vue
  34. 4 2
      src/views/cameras/preview/store/useCameraDetailStore.ts
  35. 5 5
      types/modules.d.ts
  36. 454 0
      vite.config.ts.timestamp-1724323961663-8c4873e72d16e.mjs

+ 10 - 0
src/api/camera/camera-overview.ts

@@ -97,6 +97,16 @@ export interface CameraDetailServer {
   resolution: number;
   //渲染
   render: number | string;
+  // 平台是否显示电子围栏
+  fenceDisplayStatus: FenceDisplayStatus;
+}
+
+// 是否启用
+export enum FenceDisplayStatus {
+  // 启用
+  enabled = 0,
+  // 禁用
+  disabled = 1,
 }
 
 export const getCameraList = (params: CameraQueryParams) => {

+ 71 - 0
src/api/camera/camera-preview.ts

@@ -261,6 +261,24 @@ export const goToPresetApi = (data: { presetToken: string; cameraId: number }) =
   });
 };
 
+export interface CameraAlgoPresetResp {
+  algoId: string;
+  algoCode: string;
+  cameraId: string;
+  cameraCode: string;
+  presetToken: string;
+  electronicFencePolygon: string;
+  status: number;
+}
+
+/** 获取平台相机展示电子围栏的预置位和算法 */
+export const getAppCameraAlgoPreset = (cameraCode: string): Promise<CameraAlgoPresetResp> => {
+  return http.request({
+    url: `/cameraPreview/getCameraPreset?cameraCode=${cameraCode}`,
+    method: 'get',
+  });
+};
+
 interface CameraMoveParam {
   cameraId: number;
   x: number;
@@ -310,3 +328,56 @@ export const renderCamera = (data: RenderPara) => {
     data,
   });
 };
+
+interface PresetType {
+  algoCode: string;
+  cameraCode: string;
+  presetToken: string;
+}
+
+//选择相机算法预设
+export const choosePreset = (data: PresetType) => {
+  return http.request({
+    url: `/cameraPreview/choosePreset`,
+    method: 'post',
+    data,
+  });
+};
+
+interface AlgoInfoType {
+  code: string;
+  id: number;
+  presetInfoList?: AlgoPresetType[];
+  cameraCode: string;
+  cameraId: number;
+  name: string;
+}
+interface AlgoPresetType {
+  electronicFencePolygon: string;
+  presetToken: string;
+  status: number;
+}
+
+//查询相机算法预设列表
+export const getCameraAlgoPresetList = (
+  cameraCode: string,
+): Promise<{ algoInfoVOList: AlgoInfoType[] }> => {
+  return http.request({
+    url: `/cameraPreview/getCameraAlgoPresetList?cameraCode=${cameraCode}`,
+    method: 'get',
+  });
+};
+
+interface UpdateFenceType {
+  cameraCode: string;
+  status: number;
+}
+
+// 开启-关闭平台显示电子围栏
+export const updateFenceDisplayStatus = (data: UpdateFenceType) => {
+  return http.request({
+    url: `/cameraPreview/updateFenceDisplayStatus`,
+    method: 'post',
+    data,
+  });
+};

Plik diff jest za duży
+ 12 - 0
src/assets/icons/algo-setting.svg


Plik diff jest za duży
+ 13 - 0
src/assets/icons/algo-switch.svg


+ 15 - 0
src/assets/icons/box-select-inner.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="二期开发请关注切图及相关状态" transform="translate(-237.000000, -113.000000)">
+            <g id="反选-内" transform="translate(237.000000, 113.000000)">
+                <rect id="矩形" x="0" y="0" width="24" height="24"></rect>
+                <g id="编组" transform="translate(4.000000, 4.000000)" fill="#000000" fill-rule="nonzero">
+                    <path d="M14.5,0 L1.5,0 C0.671874998,0 0,0.671874998 0,1.5 L0,14.5 C0,15.328125 0.671874998,16 1.5,16 L14.5,16 C15.328125,16 16,15.328125 16,14.5 L16,1.5 C16,0.671874998 15.328125,0 14.5,0 L14.5,0 Z M15,14 C15,14.5515625 14.5515625,15 14,15 L2,15 C1.44843751,15 0.999999996,14.5515625 0.999999996,14 L0.999999996,2 C0.999999996,1.44687501 1.44843749,0.999999996 2,0.999999996 L14,0.999999996 C14.5515625,0.999999996 15,1.446875 15,2 L15,14 Z" id="形状"></path>
+                    <path d="M13,4.015625 L13,12 C13,12.5515625 12.5515625,13 12,13 L4.015625,13 C3.4640625,13 3.015625,12.5515625 3.015625,12 L3.015625,4.015625 C3.015625,3.46250001 3.4640625,3.015625 4.015625,3.015625 L12,3.015625 C12.5515625,3.015625 13,3.46250001 13,4.015625 Z" id="路径"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/assets/icons/box-select-outer.png


Plik diff jest za duży
+ 19 - 0
src/assets/icons/box-select-outer.svg


Plik diff jest za duży
+ 19 - 0
src/assets/icons/delete.svg


BIN
src/assets/icons/scale-large.png


BIN
src/assets/icons/scale-small.png


+ 3 - 0
src/components/LiveVideo/LiveVideo.vue

@@ -139,5 +139,8 @@
     font-size: 20px;
     text-align: center;
     margin-top: 200px;
+    position: absolute;
+    left: 0;
+    right: 0;
   }
 </style>

+ 15 - 1
src/components/SvgIcon/SvgIcon.vue

@@ -1,5 +1,11 @@
 <template>
-  <svg class="svg-icon" aria-hidden="true" :style="{ color: color }">
+  <svg
+    class="svg-icon"
+    aria-hidden="true"
+    :style="{ color: color }"
+    :width="width"
+    :height="height"
+  >
     <use :xlink:href="svgName" />
   </svg>
 </template>
@@ -25,6 +31,14 @@
         type: String,
         required: false,
       },
+      width: {
+        type: String,
+        required: false,
+      },
+      height: {
+        type: String,
+        required: false,
+      },
     },
     setup(props) {
       const svgName = computed(() => `#icon-${props.iconName}`);

+ 7 - 3
src/views/cameras/preview/CameraPreview.vue

@@ -32,11 +32,13 @@
   import { IsPtz } from '@/api/camera/camera-overview';
   import { CameraTree, getCameraTree } from '@/api/camera/camera-preview';
   import useCameraStatus from './store/useCameraStatus';
+  import { storeToRefs } from 'pinia';
 
   const cameraStatus = useCameraStatus();
   const { noNetworkingNum, openInterval, closeInterval } = cameraStatus;
 
   const cameraDetailStore = useCameraDetailStore();
+  const { isShowFence } = storeToRefs(cameraDetailStore);
   const cameraAlgoStore = useCameraAlgoStore();
   const fenceStore = useFenceStore();
   const presetListStore = usePresetListStore();
@@ -99,6 +101,7 @@
   watch(
     () => cameraDetailStore.cameraId,
     async (cameraId) => {
+      isShowFence.value = false;
       fenceStore.clear();
       if (cameraId) {
         const presetList = await presetListStore.getPresetList(cameraId);
@@ -156,17 +159,18 @@
   function getCameraDetail(tree: CameraTree[], cameraId: number): CameraTree | null {
     let detail: CameraTree | null = null;
 
-    function getDetail(t: CameraTree[]) {
+    function getDetail(t: CameraTree[], parent) {
       t.forEach((x) => {
         if (x.nodeType === 'camera' && x.id === cameraId) {
           detail = x;
+          detail.workshopId = parent?.workshopId;
         } else if (x.children && x.children.length > 0) {
-          getDetail(x.children);
+          getDetail(x.children, x);
         }
       });
     }
 
-    getDetail(tree);
+    getDetail(tree, null);
 
     return detail;
   }

+ 29 - 0
src/views/cameras/preview/components/AlgoSwitchCard/AlgoAddBtn.vue

@@ -0,0 +1,29 @@
+<template>
+  <AlgoSwitchCardBase class="algoSwitchCardBase">
+    <div class="content">
+      <div class="addIcon">+</div>
+      <div> 点击添加算法 </div>
+    </div>
+  </AlgoSwitchCardBase>
+</template>
+<script lang="ts" setup>
+  import AlgoSwitchCardBase from './AlgoSwitchCardBase.vue';
+</script>
+<style scoped>
+  .content {
+    color: #909399;
+    text-align: center;
+    font-size: 14px;
+    /* margin-top: 10px; */
+    /* height: 100px; */
+    padding-top: 15px;
+  }
+
+  .addIcon {
+    font-size: 30px;
+    height: 45px;
+  }
+  .algoSwitchCardBase {
+    border: 1px dashed #ccc;
+  }
+</style>

+ 5 - 0
src/views/cameras/preview/components/AlgoSwitchCard/AlgoDeleteIcon.vue

@@ -0,0 +1,5 @@
+<template>
+  <svg-icon icon-name="delete" color="#1890ff" :width="100" :height="200" />
+</template>
+<script lang="ts" setup></script>
+<style scoped></style>

+ 11 - 0
src/views/cameras/preview/components/AlgoSwitchCard/AlgoSettingIcon.vue

@@ -0,0 +1,11 @@
+<template>
+  <div>
+    <el-tooltip content="算法参数设置">
+      <svg-icon icon-name="algo-setting" color="#1890ff" />
+    </el-tooltip>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ElTooltip } from 'element-plus';
+</script>
+<style scoped></style>

+ 139 - 0
src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchCard.vue

@@ -0,0 +1,139 @@
+<template>
+  <div class="algoSwitchCardWrapper">
+    <AlgoSwitchCardBase class="algoWrapper" :class="{ active: isAlgoOpen, selected: isSelected }">
+      <div>
+        <!-- <div>开关 </div> -->
+        <AlgoSwitchIcon :active="isAlgoOpen" @click.stop="toggleAlgoStatus" />
+        <div class="algoName">{{ label }}</div>
+        <div class="toolbar">
+          <AlgoSettingIcon class="divideLine algoSettingIcon" v-if="hasAlgoSettingMaxPermisson()" />
+          <!-- <AlgoSettingIcon class="divideLine algoSettingIcon" /> -->
+          <div @click.stop="() => {}">
+            <el-tooltip :content="isFenceOpen ? '关闭电子围栏' : '开启电子围栏'">
+              <ElSwitch
+                size="small"
+                :modelValue="isFenceOpen"
+                @update:modelValue="updateFenceStatus"
+              />
+            </el-tooltip>
+          </div>
+          <div
+            class="divideLine deleteIcon"
+            @click.stop="emits('remove')"
+            v-if="hasDeletePermission()"
+          >
+            <el-tooltip content="删除算法">
+              <img :src="deletePng" alt="删除" style="width: 16px; height: 16px" />
+            </el-tooltip>
+          </div>
+        </div>
+      </div>
+    </AlgoSwitchCardBase>
+  </div>
+</template>
+<script lang="ts" setup>
+  import AlgoSwitchCardBase from './AlgoSwitchCardBase.vue';
+  import AlgoSettingIcon from './AlgoSettingIcon.vue';
+  import AlgoSwitchIcon from './AlgoSwitchIcon.vue';
+  import deletePng from '@/assets/icons/delete.png';
+  import { ElSwitch } from 'element-plus';
+  import { useUserStore } from '@/store/modules/user';
+
+  interface Props {
+    /** 当前算法是否选中 */
+    isSelected: boolean;
+    /** 显示的算法名称 */
+    label: string;
+    /** 算法是否开启 */
+    isAlgoOpen: boolean;
+    /** 电子围栏是否打开 */
+    isFenceOpen: boolean;
+  }
+
+  const props = defineProps<Props>();
+
+  const userStore = useUserStore();
+
+  const emits = defineEmits<{
+    (e: 'toggleAlgo', isOpen: boolean): unknown;
+    (e: 'toggleFence', isOpen: boolean): unknown;
+    (e: 'remove'): unknown;
+  }>();
+  const toggleAlgoStatus = () => {
+    emits('toggleAlgo', !props.isAlgoOpen);
+  };
+
+  const updateFenceStatus = (fenceStatus: boolean) => {
+    emits('toggleFence', fenceStatus);
+  };
+
+  const hasDeletePermission = () => userStore.checkPermission('algo_delete');
+
+  const hasAlgoSettingMaxPermisson = () => userStore.checkPermission('algo_setting_max');
+</script>
+<style scoped>
+  .algoWrapper {
+    /* margin: 10px; */
+    border: 1px solid #f0f2f5;
+    padding: 10px;
+    border-radius: 4px;
+    &.active {
+      background-color: #e8f5ff;
+      border-color: #bee2ff;
+      .algoName {
+        color: #1777ff;
+      }
+      .toolbar {
+        border-color: rgba(23, 119, 255, 0.4);
+      }
+    }
+
+    &.selected {
+      border-color: #1777ff;
+    }
+  }
+
+  .algoName {
+    font-size: 14px;
+    text-align: center;
+    color: #a8abb2;
+  }
+  .toolbar {
+    margin-top: 10px;
+    border-top: 1px solid rgba(168, 171, 178, 0.4);
+    display: flex;
+    /* justify-content: space-between; */
+    justify-content: center;
+    align-items: center;
+    margin-top: 12px;
+    padding-top: 5px;
+  }
+  .divideLine {
+    position: relative;
+    &::before {
+      position: absolute;
+      left: 14px;
+      height: 18px;
+      width: 1px;
+      display: block;
+      background: rgba(168, 171, 178, 0.4);
+      content: '';
+    }
+  }
+
+  .deleteIcon {
+    margin-left: 33px;
+    flex-shrink: 0;
+    cursor: pointer;
+    &::before {
+      left: -18px;
+    }
+  }
+  .algoSettingIcon {
+    margin-right: 33px;
+    margin-left: 6px;
+    &::before {
+      left: 28px;
+    }
+  }
+</style>

+ 19 - 0
src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchCardBase.vue

@@ -0,0 +1,19 @@
+<template>
+  <div class="algo-switch-card-wrapper"><slot></slot></div>
+</template>
+<script lang="ts" setup></script>
+<style scoped>
+  .algo-switch-card-wrapper {
+    width: 160px;
+    height: 100px;
+    margin: 10px;
+    margin-left: 0;
+    background: #fafafa;
+    border-radius: 4px;
+    cursor: pointer;
+
+    &:hover {
+      box-shadow: 2px 2px 10px 0px rgba(110, 110, 116, 0.4);
+    }
+  }
+</style>

+ 29 - 0
src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchIcon.vue

@@ -0,0 +1,29 @@
+<template>
+  <span class="algo-switch-wrapper">
+    <el-tooltip :content="tip" placement="top">
+      <svg-icon icon-name="algo-switch" :color="color" class="algo-switch" />
+    </el-tooltip>
+  </span>
+</template>
+<script lang="ts" setup>
+  import { computed } from 'vue';
+  import { ElTooltip } from 'element-plus';
+
+  const props = defineProps<{ active: boolean }>();
+
+  const color = computed(() => {
+    return props.active ? '#1890ff' : '#A8ABB2';
+  });
+
+  const tip = computed(() => {
+    return props.active ? '关闭算法' : '开启算法';
+  });
+</script>
+<style scoped>
+  .algo-switch {
+    cursor: pointer;
+  }
+  .algo-switch-wrapper {
+    display: inline-block;
+  }
+</style>

+ 3 - 0
src/views/cameras/preview/components/AlgoSwitchCard/WithTooltip.vue

@@ -0,0 +1,3 @@
+<template> </template>
+<script lang="ts" setup></script>
+<style scoped></style>

+ 5 - 2
src/views/cameras/preview/components/AlgorithmsSetting/AddAlgoDialog.vue

@@ -1,7 +1,8 @@
 <template>
-  <ElButton type="primary" @click="showDialog" size="small" style="margin-top: 10px">
+  <!-- <ElButton type="primary" @click="showDialog" size="small" style="margin-top: 10px">
     + 添加算法</ElButton
-  >
+  > -->
+  <AlgoAddBtn @click="showDialog"></AlgoAddBtn>
 
   <ElDialog
     v-model="visible"
@@ -46,6 +47,8 @@
   import { storeToRefs } from 'pinia';
   import { createCameraAlgoApi } from '@/api/camera/camera-preview';
   import useCameraDetailStore from '../../store/useCameraDetailStore';
+  import AlgoAddBtn from '../AlgoSwitchCard/AlgoAddBtn.vue';
+
   const selectedIds = ref<number[]>([]);
   const cameraAlgoStore = useCameraAlgoStore();
 

+ 3 - 6
src/views/cameras/preview/components/AlgorithmsSetting/AlgoSettingCard.vue

@@ -1,7 +1,7 @@
 <template>
   <div id="algoCardWrapper" class="algoCardWrapper">
     <div class="algoCardTitle">
-      <div>{{ selectedAlgoDetail?.algoInfo?.name }}</div>
+      <div>{{ selectedAlgoDetail?.algoInfo?.name }} 算法参数配置</div>
       <div style="display: flex; align-items: center">
         <ElSwitch
           v-model="selectedAlgoDetail.enableCardBool"
@@ -15,7 +15,7 @@
       </div>
     </div>
     <div class="algoCardMain">
-      <div class="algoRow" style="display: flex; align-items: center">
+      <!-- <div class="algoRow" style="display: flex; align-items: center">
         <div class="algoLabel">绘制电子围栏:</div>
         <div>
           <div style="display: flex; align-items: center">
@@ -26,9 +26,6 @@
               </template>
               <el-icon color="#e2e2e2" :size="16" class="tipIcon"><InfoCircleOutlined /></el-icon>
             </el-tooltip>
-            <!-- <span style="font-size: 10px; margin-left: 20px; color: #262626"
-              >备注:请绘制电子围栏</span
-            > -->
             <el-radio-group
               v-model="selectedAlgoDetail.regionJudge"
               :disabled="!selectedAlgoDetail.electronicFenceBool"
@@ -40,7 +37,7 @@
           </div>
           <div class="presetList"> 预置位1 预置位2 预置位3 </div>
         </div>
-      </div>
+      </div> -->
       <div class="algoRow">
         <div class="algoLabel">检测时间段:</div>
         <div class="algoTimeContent">

+ 79 - 9
src/views/cameras/preview/components/AlgorithmsSetting/AlgorithmsSetting.vue

@@ -1,11 +1,11 @@
 <template>
   <div id="algoSetting">
-    <div style="font-size: 14px; font-weight: bold">算法配置</div>
-    <div style="display: flex">
+    <div style="font-size: 14px; font-weight: bold">算法开关</div>
+    <div>
       <div class="algoTagWrapper">
-        <AddAlgoDialog />
+        <AddAlgoDialog v-if="hasAddPermission()" />
 
-        <AlgoTag
+        <!-- <AlgoTag
           v-for="item in cameraAlgoList"
           :key="item.code"
           :label="item.algoInfo?.name"
@@ -14,6 +14,18 @@
           @on-hit="handleSelectAlgo(item.algoId)"
           @on-remove="handleRemove"
           :is-open="item.status === ALGO_ENABLED_STATUS.enabled"
+        /> -->
+        <AlgoSwitchCard
+          v-for="item in cameraAlgoList"
+          :key="item.code"
+          :label="item.algoInfo?.name"
+          :is-selected="item.algoId === selectedAlgoId"
+          :is-algo-open="item.status === ALGO_ENABLED_STATUS.enabled"
+          @click="handleSelectAlgo(item.algoId)"
+          @remove="confirmRemove(item.algoId)"
+          @toggle-algo="handleToggleAlgo(item, $event)"
+          :is-fence-open="item.electronicFence === FENCE_ENBALED_STATUS.enabled"
+          @toggle-fence="handleToggleFence(item, $event)"
         />
       </div>
       <div>
@@ -22,7 +34,7 @@
           @on-cancel="handleCancel"
           v-if="selectedAlgoId"
         />
-        <div style="color: #ccc; margin-top: 20px" v-else>请选择左侧算法</div>
+        <!-- <div style="color: #ccc; margin-top: 20px" v-else>请选择算法</div> -->
       </div>
     </div>
   </div>
@@ -35,17 +47,15 @@
     deleteCameraAlgoApi,
     updateCameraAlgoApi,
     FENCE_ENBALED_STATUS,
+    CameraAlgoItem,
   } from '@/api/camera/camera-preview';
   import { ElMessage, ElMessageBox } from 'element-plus';
-  import AlgoTag from './AlgoTag.vue';
+  import AlgoSwitchCard from '../AlgoSwitchCard/AlgoSwitchCard.vue';
   import useFenceStore from '../../store/useFenceStore';
   import useCameraDetailStore from '../../store/useCameraDetailStore';
   import usePresetListStore from '../../store/usePresetListStore';
   import AddAlgoDialog from './AddAlgoDialog.vue';
   import {
-    createDefaultTime,
-    getDetectionJSON,
-    getDetectionTimeJSON,
     getInferCode,
     getAlgoType,
     getMetaValues,
@@ -55,6 +65,7 @@
   } from './utils';
   import { ALGO_ENABLED_STATUS } from '@/api/camera/camera-preview';
   import { watchEffect } from 'vue';
+  import { useUserStore } from '@/store/modules/user';
 
   const cameraAlgoStore = useCameraAlgoStore();
   const fenceStore = useFenceStore();
@@ -63,6 +74,9 @@
   const { getCameraAlgoList, getAlgoDetail } = cameraAlgoStore;
   const { cameraAlgoList, selectedAlgoId, selectedAlgoDetail } = storeToRefs(cameraAlgoStore);
   const cameraDetailStore = useCameraDetailStore();
+  const userStore = useUserStore();
+
+  const hasAddPermission = () => userStore.checkPermission('algo_add');
 
   const handleSelectAlgo = (algoId: number) => {
     if (selectedAlgoId.value === algoId) {
@@ -132,6 +146,50 @@
     }
   });
 
+  const handleToggleAlgo = (detail: CameraAlgoItem, algoStatus: boolean) => {
+    const cameraId = cameraDetailStore.cameraId;
+    const algoId = detail.algoId;
+    // console.log({ detail, status });
+    const status = algoStatus ? ALGO_ENABLED_STATUS.enabled : ALGO_ENABLED_STATUS.disabled;
+    const params = {
+      cameraId: cameraId,
+      id: detail.id as number,
+      algoId,
+      status,
+    };
+    const initialStatus = detail.status;
+    detail.status = status;
+    updateCameraAlgoApi(params)
+      .then(() => {
+        ElMessage.success(algoStatus ? '算法已开启' : '算法已关闭');
+      })
+      .catch((err) => {
+        detail.status = initialStatus;
+      });
+  };
+
+  const handleToggleFence = (detail: CameraAlgoItem, fenceStatus: boolean) => {
+    const cameraId = cameraDetailStore.cameraId;
+    const algoId = detail.algoId;
+    // console.log({ detail, status });
+    const status = fenceStatus ? FENCE_ENBALED_STATUS.enabled : FENCE_ENBALED_STATUS.disabled;
+    const params = {
+      cameraId: cameraId,
+      id: detail.id as number,
+      algoId,
+      electronicFence: status,
+    };
+    const initialStatus = detail.electronicFence;
+    detail.electronicFence = status;
+    updateCameraAlgoApi(params)
+      .then(() => {
+        ElMessage.success(fenceStatus ? '电子围栏已开启' : '电子围栏已关闭');
+      })
+      .catch((err) => {
+        detail.electronicFence = initialStatus;
+      });
+  };
+
   const handleSubmit = (param) => {
     console.log('submitParam', param);
     const cameraId = cameraDetailStore.cameraId;
@@ -167,6 +225,16 @@
     }
   };
 
+  const confirmRemove = (algoId: number) => {
+    ElMessageBox.confirm('删除后,该算法卡片将无法恢复,请确定是否删除?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }).then(() => {
+      handleRemove(algoId);
+    });
+  };
+
   const handleRemove = (algoId: number) => {
     console.log('remove', algoId);
     deleteCameraAlgoApi({ algoId, cameraId: cameraDetailStore.cameraId }).then(() => {
@@ -189,6 +257,8 @@
   .algoTagWrapper {
     min-width: 150px;
     margin-right: 15px;
+    display: flex;
+    flex-wrap: wrap;
   }
 
   :deep(.el-message-box__status.el-icon) {

+ 1 - 0
src/views/cameras/preview/components/CameraDirectionControl/CameraDirectionControl.vue

@@ -47,6 +47,7 @@
   .cameraDirectionControl {
     width: 180px;
     height: 180px;
+    overflow: hidden;
 
     position: relative;
     .roundCircle {

+ 51 - 0
src/views/cameras/preview/components/CameraViewSetting/CameraViewScale.vue

@@ -0,0 +1,51 @@
+<template>
+  <!-- 相机的视图缩放 -->
+  <div class="viewScaleWrapper">
+    <img :src="scaleLargeImg" alt="放大" class="scaleIcon" @click="handleEnlarge" />
+    <img
+      :src="scaleSmallImg"
+      alt="缩小"
+      class="scaleIcon"
+      @click="handleReduce"
+      style="margin-left: 22px"
+    />
+  </div>
+</template>
+<script lang="ts" setup>
+  import { cameraMoveApi } from '@/api/camera/camera-preview';
+  import scaleLargeImg from '@/assets/icons/scale-large.png';
+  import scaleSmallImg from '@/assets/icons/scale-small.png';
+  import { storeToRefs } from 'pinia';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+
+  const cameraDetailStore = useCameraDetailStore();
+  const { cameraId } = storeToRefs(cameraDetailStore);
+
+  const zoomStep = 0.05;
+
+  const handleEnlarge = () => {
+    cameraMoveApi({ cameraId: cameraId.value, zoom: zoomStep, x: 0, y: 0 });
+    // presetListStore.currentPresetToken = '';
+  };
+
+  const handleReduce = () => {
+    cameraMoveApi({ cameraId: cameraId.value, zoom: -zoomStep, x: 0, y: 0 });
+  };
+</script>
+<style scoped>
+  .viewScaleWrapper {
+    border-radius: 50%;
+    border: 1px solid #ccc;
+    padding: 6px 30px;
+    border-radius: 92px;
+    margin-bottom: 20px;
+    backdrop-filter: blur(8px);
+  }
+
+  .scaleIcon {
+    width: 20px;
+    height: 20px;
+    display: inline-block;
+    cursor: pointer;
+  }
+</style>

+ 46 - 18
src/views/cameras/preview/components/CameraViewSetting/CameraViewSetting.vue

@@ -1,20 +1,14 @@
 <template>
-  <div>
+  <div style="position: relative">
     <div class="toolbarWrapper">
       <!-- <ViewWindowSetting v-model="viewType" @update:model-value="handleUpdateViewType" /> -->
-      <el-tooltip content="全屏">
+      <!-- <el-tooltip content="全屏">
         <el-icon class="el-input__icon" :size="18" style="margin-left: 10px; margin-right: 10px">
           <FullscreenExitOutlined role="full" @click="enterFullscreen" />
         </el-icon>
-      </el-tooltip>
+      </el-tooltip> -->
       <RenderSwitch @change-camera-render="changeRender" />
-      <FenceToolbar
-        :style="{ visibility: drawable ? 'visible' : 'hidden' }"
-        @remove="handleRemove"
-        @save="handleSave"
-        @toggle-editable="toggleEditable"
-        :is-edit="isEdit"
-      />
+      <FenceAppSetting />
     </div>
     <div
       class="cameraViewSettingWrapper"
@@ -33,11 +27,8 @@
         <CameraLiveVideo />
       </div>
     </div>
-    <div
-      class="presetAddWrapper"
-      :class="{ hidePresetControlCls: isEdit }"
-      v-if="!!cameraDetailStore.detail?.isPtz"
-    >
+    <div class="presetAddWrapper" :class="{ hidePresetControlCls: isEdit }">
+      <CameraViewScale />
       <CameraDirectionControl />
       <ElButton
         type="primary"
@@ -51,6 +42,14 @@
   </div>
   <div class="presetWrapper">
     <PresetSelect />
+    <FenceToolbar
+      :style="{ display: drawable ? 'flex' : 'none' }"
+      @remove="handleRemove"
+      @save="handleSave"
+      @toggle-editable="toggleEditable"
+      @toggle-range="toggleRange"
+      :is-edit="isEdit"
+    />
   </div>
 
   <div class="cameraParamsSettingWrapper">
@@ -75,11 +74,14 @@
   import AlgorithmsSetting from '../AlgorithmsSetting/AlgorithmsSetting.vue';
   import RenderSwitch from '../RenderSwitch/RenderSwitch.vue';
   import { FullscreenExitOutlined } from '@vicons/antd';
+  import FenceAppSetting from '../FenceAppSetting/FenceAppSetting.vue';
 
   import { ElMessage } from 'element-plus';
   import CameraDirectionControl from '../CameraDirectionControl/CameraDirectionControl.vue';
+  import CameraViewScale from './CameraViewScale.vue';
   import { canvasHeight, canvasWidth, domHeight, domWidth } from './constants';
   import useFullscreen from 'vue-hooks-plus/lib/useFullscreen';
+  import { updateCameraAlgoApi } from '@/api/camera/camera-preview';
 
   const emits = defineEmits<{
     (e: 'changeTreeRender', render: number | string): unknown;
@@ -95,6 +97,8 @@
   const cameraDetailStore = useCameraDetailStore();
   const cameraAlgoStore = useCameraAlgoStore();
 
+  const { getCameraAlgoList } = cameraAlgoStore;
+
   // const viewType = ref<ViewType>(ViewType.window1);
 
   const addPresetModalVisible = ref(false);
@@ -124,6 +128,28 @@
     }
   };
 
+  const toggleRange = () => {
+    const selectedAlgoDetail = cameraAlgoStore.selectedAlgoDetail;
+    const cameraId = cameraDetailStore.cameraId;
+
+    const extraStr = selectedAlgoDetail.extra;
+    const extraJSON = JSON.parse(extraStr);
+    const regionJudge = !extraJSON.inferParams?.[0]?.regionJudge;
+
+    extraJSON.inferParams[0].regionJudge = regionJudge ? 1 : 0;
+
+    const newParam = {
+      cameraId: cameraId,
+      algoId: selectedAlgoDetail.algoId,
+      extra: JSON.stringify(extraJSON),
+      id: selectedAlgoDetail.id!,
+    };
+    updateCameraAlgoApi(newParam).then(() => {
+      ElMessage.success('更新成功');
+      getCameraAlgoList(cameraId);
+    });
+  };
+
   const handleSave = () => {
     const json = fenceEditorRef.value?.toObject();
     console.log('save json', json);
@@ -241,9 +267,9 @@
   .algorithmsSetting {
     flex: 1;
     min-height: 300px;
-    margin-left: 10px;
+    margin-left: 15px;
     /* border-left: 1px solid #ccc; */
-    padding-left: 15px;
+    /* padding-left: 15px; */
   }
   .cameraParamsSetting {
     flex-basis: 330px;
@@ -259,9 +285,11 @@
 
   .presetWrapper {
     padding: 20px;
-    padding-left: 25px;
+    padding-left: 15px;
     border-bottom: 1px solid #ccc;
     padding-bottom: 10px;
     margin-bottom: 15px;
+    display: flex;
+    justify-content: space-between;
   }
 </style>

+ 116 - 0
src/views/cameras/preview/components/FenceAppSetting/FenceAppSetting.vue

@@ -0,0 +1,116 @@
+<template>
+  <!-- 电子围栏显示在前台的控制开关 -->
+  <div class="wrapper">
+    <el-checkbox
+      label="平台是否显示电子围栏"
+      v-model="isShowFence"
+      @change="changeShowFence"
+      class="checkbox"
+    />
+    <el-cascader
+      v-model="valuePreset"
+      :options="options"
+      size="small"
+      @change="changePreset"
+      v-if="isShowFence"
+    />
+    <a :href="previewUrl" target="_blank" style="margin-left: 20px" v-if="isShowFence">平台预览</a>
+  </div>
+</template>
+<script lang="ts" setup>
+  import {
+    updateFenceDisplayStatus,
+    getCameraAlgoPresetList,
+    choosePreset,
+    getAppCameraAlgoPreset,
+    CameraAlgoPresetResp,
+  } from '@/api/camera/camera-preview';
+  import { computed, onMounted, ref, watch } from 'vue';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+  import { storeToRefs } from 'pinia';
+  import { OptionType } from './constants';
+  import { useGlobSetting } from '@/hooks/setting';
+  const valuePreset = ref<[string, string]>();
+  const cameraDetailStore = useCameraDetailStore();
+  const { isShowFence, detail } = storeToRefs(cameraDetailStore);
+
+  const appFenceCameraDetail = ref<CameraAlgoPresetResp | null>(null);
+
+  const options = ref([]);
+
+  const { appPCUrl } = useGlobSetting();
+
+  const previewUrl = computed(() => {
+    return appPCUrl + `#/shop?id=${detail.value?.workshopId}&cameraId=${detail.value?.code!}`;
+  });
+
+  watch(
+    () => detail.value?.code,
+    (newCode) => {
+      if (!newCode) return;
+      getCameraAlgoPresetList(newCode).then((res) => {
+        options.value = renameKeys(res.algoInfoVOList);
+      });
+
+      getAppCameraAlgoPreset(newCode).then((res) => {
+        appFenceCameraDetail.value = res;
+        valuePreset.value = [res.algoCode, res.presetToken];
+      });
+    },
+  );
+
+  const renameKeys = (data: any) => {
+    return data.map((item) => {
+      const newItem: OptionType = {
+        label: item.name,
+        value: item.code,
+      };
+
+      if (item.presetInfoList) {
+        newItem.children = item.presetInfoList.map((child) => ({
+          label: child.presetName,
+          value: child.presetToken,
+        }));
+      }
+
+      return newItem;
+    });
+  };
+
+  const changeShowFence = async () => {
+    if (isShowFence.value) {
+      const params = {
+        cameraCode: cameraDetailStore.detail?.code!,
+        status: 0,
+      };
+      await updateFenceDisplayStatus(params);
+    } else {
+      const params = {
+        cameraCode: cameraDetailStore.detail?.code!,
+        status: 1,
+      };
+      updateFenceDisplayStatus(params);
+    }
+  };
+
+  const changePreset = (value) => {
+    console.log('value', value);
+    const params = {
+      algoCode: value[0],
+      cameraCode: cameraDetailStore.detail?.code!,
+      presetToken: value[1],
+    };
+    choosePreset(params);
+  };
+</script>
+<style scoped>
+  .wrapper {
+    display: flex;
+    align-items: center;
+    margin-left: 20px;
+  }
+
+  .checkbox {
+    margin-right: 10px;
+  }
+</style>

+ 5 - 0
src/views/cameras/preview/components/FenceAppSetting/constants.ts

@@ -0,0 +1,5 @@
+export interface OptionType {
+  label: string;
+  value: string;
+  children?: { label: string; value: string }[];
+}

+ 16 - 6
src/views/cameras/preview/components/FenceToolbar/FenceToolbar.vue

@@ -1,6 +1,16 @@
 <template>
   <div class="toolbar">
+    <div class="fenceDrawingTip" v-if="isEdit">
+      {{ cameraAlgoStore.selectedAlgoDetail?.algoInfo?.name }}算法电子围栏绘制中
+    </div>
     <template v-if="props.isEdit">
+      <!-- <ToolbarIcon
+        :src="deleteIcon"
+        :active="false"
+        @click="emits('toggleFence')"
+        tip="检测范围反选"
+      /> -->
+      <ToggleFenceStatus @click="emits('toggleRange')" />
       <ToolbarIcon :src="deleteIcon" :active="false" @click="emits('remove')" tip="删除电子围栏" />
       <ToolbarIcon :src="saveIcon" :active="false" @click="emits('save')" tip="保存电子围栏" />
     </template>
@@ -8,9 +18,6 @@
     <ElButton type="primary" size="small" @click="toggleEdit">{{
       props.isEdit ? '退出编辑' : '编辑电子围栏'
     }}</ElButton>
-    <div class="fenceDrawingTip" v-if="isEdit"
-      >{{ cameraAlgoStore.selectedAlgoDetail?.algoInfo?.name }}算法绘制中</div
-    >
   </div>
 </template>
 <script setup lang="ts">
@@ -19,6 +26,7 @@
   import ToolbarIcon from '../ToolbarIcon/ToolbarIcon.vue';
   import saveIcon from '@/assets/images/camera/save.png';
   import deleteIcon from '@/assets/images/camera/delete.png';
+  import ToggleFenceStatus from './ToggleFenceStatus.vue';
   import useCameraAlgoStore from '../../store/useCameraAlgoStore';
 
   const cameraAlgoStore = useCameraAlgoStore();
@@ -27,6 +35,7 @@
 
   const emits = defineEmits<{
     (e: 'toggleEditable', editState: boolean): unknown;
+    (e: 'toggleRange'): unknown;
     (e: 'remove'): unknown;
     (e: 'save'): unknown;
   }>();
@@ -46,12 +55,13 @@
     border-radius: 50px;
   }
   .fenceDrawingTip {
-    background: #e6f4ff;
+    background: #e8f5ff;
     border-radius: 6px;
     border: 1px solid #bae0ff;
     text-align: center;
     font-size: 12px;
-    padding: 2px 40px;
-    margin-left: 50px;
+    padding: 2px 20px;
+    margin-right: 30px;
+    color: #1890ff;
   }
 </style>

+ 27 - 0
src/views/cameras/preview/components/FenceToolbar/ToggleFenceStatus.vue

@@ -0,0 +1,27 @@
+<template>
+  <ToolbarIcon :src="src" :active="false" tip="检测范围反选" />
+</template>
+<script lang="ts" setup>
+  import ToolbarIcon from '../ToolbarIcon/ToolbarIcon.vue';
+  import fenceOut from '@/assets/icons/box-select-outer.png';
+  import fenceInner from '@/assets/icons/box-select-inner.svg';
+  import useCameraAlgoStore, { AlgoParamMetaItem } from '../../store/useCameraAlgoStore';
+
+  import { computed } from 'vue';
+  import { storeToRefs } from 'pinia';
+
+  const props = defineProps<{ active: boolean }>();
+
+  enum RegionJudge {
+    out = 0,
+    in = 1,
+  }
+
+  const src = computed(() => {
+    return selectedAlgoDetail.value.regionJudge === RegionJudge.out ? fenceOut : fenceInner;
+  });
+
+  const cameraAlgoStore = useCameraAlgoStore();
+  const { selectedAlgoDetail } = storeToRefs(cameraAlgoStore);
+</script>
+<style scoped></style>

+ 1 - 0
src/views/cameras/preview/components/PresetSelect/PresetSelect.vue

@@ -86,6 +86,7 @@
   .presetSetting {
     display: flex;
     align-items: center;
+    height: 34px;
   }
 
   .circular {

+ 3 - 3
src/views/cameras/preview/components/RenderSwitch/RenderSwitch.vue

@@ -1,7 +1,7 @@
 <template>
-  <div style="margin-left: 8px; display: flex" v-if="cameraDetailStore.detail">
-    <div style="margin-top: 5px; margin-right: 4px">渲染开关</div>
-    <ElSelect v-model="selectedIds" @change="changeRender" style="width: 150px">
+  <div style="display: flex; margin-left: 16px" v-if="cameraDetailStore.detail">
+    <div style="margin-top: 2px; margin-right: 4px">渲染开关</div>
+    <ElSelect v-model="selectedIds" @change="changeRender" style="width: 150px" size="small">
       <ElOption value="" label="无渲染">无渲染</ElOption>
       <ElOption value="demo" label="有渲染">有渲染</ElOption>
       <!-- <ElOption v-for="item in allAlgoList" :key="item.id" :value="item.id" :label="item.name">

+ 5 - 3
src/views/cameras/preview/components/ToolbarIcon/ToolbarIcon.vue

@@ -1,7 +1,9 @@
 <template>
-  <!-- <ElTooltip :content="props.tip"> -->
-  <img :src="props.src" alt="" class="toolbarIcon" :class="props.active ? 'active' : ''" />
-  <!-- </ElTooltip> -->
+  <div>
+    <ElTooltip :content="props.tip">
+      <img :src="props.src" alt="" class="toolbarIcon" :class="props.active ? 'active' : ''" />
+    </ElTooltip>
+  </div>
 </template>
 <script lang="ts" setup>
   const props = defineProps<{ active: boolean; src: string; tip: string }>();

+ 4 - 2
src/views/cameras/preview/store/useCameraDetailStore.ts

@@ -1,6 +1,6 @@
 /** 相机详情的store */
 
-import { CameraDetailServer } from '@/api/camera/camera-overview';
+import { CameraDetailServer, FenceDisplayStatus } from '@/api/camera/camera-overview';
 import { useRouteQuery } from '@vueuse/router';
 import dayjs from 'dayjs';
 import { defineStore } from 'pinia';
@@ -33,6 +33,7 @@ const formatDateTime = (time: string) => {
 
 const useCameraDetailStore = defineStore('cameraDetail', () => {
   const cameraId = useRouteQuery('cameraId', '', { transform: (str) => Number(str) });
+  const isShowFence = ref<boolean>(false);
 
   const detail = ref<CameraDetailServer | null>(null);
 
@@ -47,13 +48,14 @@ const useCameraDetailStore = defineStore('cameraDetail', () => {
       resolution: newDetail.resolution || VideoResolution.HIGH_RESOLUTION,
       period: newDetail.nvrPeriod || 30,
     };
+    isShowFence.value = newDetail.fenceDisplayStatus === FenceDisplayStatus.enabled ? true : false;
   };
 
   const clear = () => {
     detail.value = null;
     params.value = { ...defaultParams };
   };
-  return { detail, setDetail, cameraId, params, clear };
+  return { detail, setDetail, cameraId, params, clear, isShowFence };
 });
 
 export default useCameraDetailStore;

+ 5 - 5
types/modules.d.ts

@@ -1,8 +1,8 @@
-declare module '*.vue' {
-  import { DefineComponent } from 'vue';
-  const Component: DefineComponent<{}, {}, any>;
-  export default Component;
-}
+// declare module '*.vue' {
+//   import { DefineComponent } from 'vue';
+//   const Component: DefineComponent<{}, {}, any>;
+//   export default Component;
+// }
 
 declare module 'virtual:*' {
   const result: any;

Plik diff jest za duży
+ 454 - 0
vite.config.ts.timestamp-1724323961663-8c4873e72d16e.mjs