Преглед изворни кода

Merge branch 'all-v4-algoConfig' into all-v4-qindao

dao qin пре 1 година
родитељ
комит
bdad088f46
75 измењених фајлова са 6090 додато и 4989 уклоњено
  1. 111 0
      src/api/camera/camera-config.ts
  2. 92 24
      src/api/camera/camera-preview.ts
  3. 8 0
      src/assets/icons/electronic-fence.svg
  4. BIN
      src/assets/icons/more-dot-icon.png
  5. 0 0
      src/components/Pagination/Pagination.vue
  6. 38 54
      src/views/cameras/preview/components/AlgorithmsSetting/AlgoParamsCard.vue
  7. 10 37
      src/views/cameras/preview/components/AlgorithmsSetting/AlgoPeriodCard.vue
  8. 477 522
      src/views/cameras/preview/components/AlgorithmsSetting/AlgoSettingCard.vue
  9. 74 46
      src/views/cameras/preview/components/AlgorithmsSetting/types.ts
  10. 60 0
      src/modules/algo/algo-params-edit/use-algo-params-edit-store.ts
  11. 268 198
      src/views/cameras/preview/components/AlgorithmsSetting/utils.ts
  12. 11 0
      src/router/full-routes.ts
  13. 3 6
      src/views/cameras/algo-management/algoManagement.vue
  14. 138 0
      src/views/cameras/algo-params-setting/AlgoParamsSetting.vue
  15. 44 44
      src/views/cameras/preview/components/AddPresetModal/AddPresetModal.vue
  16. 91 0
      src/views/cameras/algo-params-setting/components/AlgoCanSelect/AlgoCanSelect.vue
  17. 29 29
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoAddBtn.vue
  18. 5 5
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoDeleteIcon.vue
  19. 9 11
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoSettingIcon.vue
  20. 140 149
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchCard.vue
  21. 19 19
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchCardBase.vue
  22. 25 29
      src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchIcon.vue
  23. 21 0
      src/views/cameras/algo-params-setting/components/AlgoSwitchCard/ElectronicFenceIcon.vue
  24. 3 3
      src/views/cameras/preview/components/AlgoSwitchCard/WithTooltip.vue
  25. 94 94
      src/views/cameras/preview/components/AlgorithmsSetting/AddAlgoDialog.vue
  26. 65 65
      src/views/cameras/preview/components/AlgorithmsSetting/AlgoTag.vue
  27. 229 0
      src/views/cameras/algo-params-setting/components/AlgorithmsSetting/AlgorithmsSetting.vue
  28. 61 61
      src/views/cameras/preview/components/CameraDirectionControl/CameraDirectionControl.vue
  29. 70 70
      src/views/cameras/preview/components/CameraDirectionControl/DirectionItem.vue
  30. 36 36
      src/views/cameras/preview/components/CameraLiveVideo/CameraLiveVideo.vue
  31. 81 81
      src/views/cameras/preview/components/CameraParams/CameraParams.vue
  32. 16 16
      src/views/cameras/preview/components/CameraParams/types.ts
  33. 347 347
      src/views/cameras/preview/components/CameraTree/CameraTree.vue
  34. 0 0
      src/views/cameras/algo-params-setting/components/CameraTree/CameraTreeOldVersion.vue
  35. 51 51
      src/views/cameras/preview/components/CameraViewSetting/CameraViewScale.vue
  36. 315 300
      src/views/cameras/preview/components/CameraViewSetting/CameraViewSetting.vue
  37. 10 10
      src/views/cameras/preview/components/CameraViewSetting/constants.ts
  38. 104 104
      src/views/cameras/preview/components/FenceAppSetting/FenceAppSetting.vue
  39. 0 0
      src/views/cameras/algo-params-setting/components/FenceAppSetting/constants.ts
  40. 692 692
      src/views/cameras/preview/components/FenceEditor/FenceEditor.vue
  41. 44 42
      src/views/cameras/preview/components/FenceEditor/constants.ts
  42. 3 3
      src/views/cameras/preview/components/FenceEditor/utils.ts
  43. 308 273
      src/views/cameras/preview/components/FenceEditorV2/FenceEditor.vue
  44. 78 57
      src/views/cameras/preview/components/FenceEditorV2/FenceItem.vue
  45. 17 17
      src/views/cameras/preview/components/FenceEditorV2/constants.ts
  46. 30 17
      src/views/cameras/preview/components/FenceEditorV2/types.ts
  47. 76 0
      src/views/cameras/algo-params-setting/components/FenceEditorV2/utils.ts
  48. 70 0
      src/views/cameras/algo-params-setting/components/FenceToolbar/EditFenceDialog.vue
  49. 74 0
      src/views/cameras/algo-params-setting/components/FenceToolbar/FenceNameItem.vue
  50. 267 0
      src/views/cameras/algo-params-setting/components/FenceToolbar/FenceToolbar.vue
  51. 29 29
      src/views/cameras/preview/components/FenceToolbar/ToggleFenceStatus.vue
  52. 6 6
      src/views/cameras/preview/components/FenceToolbar/constants.ts
  53. 103 103
      src/views/cameras/preview/components/PresetSelect/PresetSelect.vue
  54. 0 0
      src/views/cameras/algo-params-setting/components/RenderSwitch/RenderSwitch.vue
  55. 22 22
      src/views/cameras/preview/components/ToolbarIcon/ToolbarIcon.vue
  56. 26 26
      src/views/cameras/preview/components/ViewWindowSetting/ViewWindowSetting.vue
  57. 4 4
      src/views/cameras/preview/components/ViewWindowSetting/types.ts
  58. 43 0
      src/views/cameras/algo-params-setting/hooks/useParamsSettingFn.ts
  59. 102 137
      src/views/cameras/preview/store/useCameraAlgoStore.ts
  60. 61 61
      src/views/cameras/preview/store/useCameraDetailStore.ts
  61. 0 0
      src/views/cameras/algo-params-setting/store/useCameraStatus.ts
  62. 97 0
      src/views/cameras/algo-params-setting/store/useFenceStore.ts
  63. 30 30
      src/views/cameras/preview/store/usePresetListStore.ts
  64. 42 115
      src/views/cameras/preview/CameraPreview.vue
  65. 0 408
      src/views/cameras/preview/components/AlgorithmsSetting/AlgoPeriodCard copy.vue
  66. 0 349
      src/views/cameras/preview/components/AlgorithmsSetting/AlgorithmsSetting.vue
  67. 7 0
      src/views/cameras/preview/components/CameraConfigGroup/CameraConfigGroup.vue
  68. 291 0
      src/views/cameras/preview/components/CameraConfigSingle/CameraConfigSingle.vue
  69. 410 0
      src/views/cameras/preview/components/CameraConfigSingle/mockData.ts
  70. 0 41
      src/views/cameras/preview/components/FenceEditorV2/utils.ts
  71. 0 67
      src/views/cameras/preview/components/FenceToolbar/FenceToolbar.vue
  72. 0 76
      src/views/cameras/preview/store/useFenceStore.ts
  73. 1 1
      src/views/datamanager/alertformdata/components/default-simple/Default.vue
  74. 1 1
      src/views/datamanager/alertformdata/components/default/Default.vue
  75. 1 1
      src/views/datamanager/alertformdata/components/show/Show.vue

+ 111 - 0
src/api/camera/camera-config.ts

@@ -0,0 +1,111 @@
+import { http } from '@/utils/http/axios';
+
+export interface QueryCameraPageByAlgoParams {
+  pageNumber: number; // 页号
+  pageSize: number; // 每页数量
+  queryParam: {
+    workspaceIdList?: string[]; // 工位id列表
+    algoId?: number; // 算法id
+    isAlgoDisabled?: boolean; // 算法是否开启
+    isRenderDisabled?: boolean; // 渲染是否开启
+    cameraName?: string; // 相机名称
+    cameraCode?: string; // 相机code
+  };
+}
+export interface QueryCameraPageByAlgoRes {
+  cameraId: number; // 相机id
+  cameraName: string; // 相机名称
+  cameraCode: string; // 相机code
+  networkingState: number; // 联网状态: 0-启用, 1-禁用
+  integrationState: number; // 接入状态: 0-启用, 1-禁用
+  cameraImgUrl: string; // 相机预览图片url
+  workshopName: string; // 车间名称
+  workspaceName: string; // 工位名称
+  isRenderDisabled: boolean; // 渲染是否失效: true-关闭, false-开启
+  algoStatusList: {
+    algoName: string; // 算法名称
+    isDisabled: boolean; // 算法是否失效: true-关闭, false-开启
+  }[];
+}
+
+// 按算法分页查询相机列表
+export function getCameraListByAlgo(data: QueryCameraPageByAlgoParams) {
+  return http.request({
+    url: '/admin/cameraConfig/queryCameraPageByAlgo',
+    method: 'post',
+    data,
+  });
+}
+
+// 根据相机id查询算法状态
+export function getAlgoStatusByCameraIds(params: { cameraIdList: number[] }) {
+  return http.request({
+    url: '/admin/algo/queryAlgoStatusByCameraId',
+    method: 'post',
+    params,
+  });
+}
+
+// 相机算法批量添加
+export function addAlgosByBatch(params: { cameraIdList: number[]; algoIdList: number[] }) {
+  return http.request({
+    url: '/admin/algo/batchSaveCameraAlgoRel',
+    method: 'post',
+    params,
+  });
+}
+
+// 相机算法参数批量设置
+export function updateAlgosByBatch(params: { cameraIdList: number[]; algoId: number; algoParam: string }) {
+  return http.request({
+    url: '/admin/algo/batchUpdateCameraAlgoRelParam',
+    method: 'post',
+    params,
+  });
+}
+
+// 单个相机算法全部开启/关闭(isDisabled: true-关闭, false-开启)
+export function updateAlgosStatusByCameraId(params: { cameraId: number; isDisabled: boolean }) {
+  return http.request({
+    url: `/admin/algo/updateCameraAllAlgoRelStatus?cameraId=${params.cameraId}&isDisabled=${params.isDisabled}`,
+    method: 'put',
+    params,
+  });
+}
+
+// 批量相机算法批量开启/关闭(isEnabled: true-开启, false-关闭)
+export function updateAlgosStatusByBatch(data: { cameraIdList: number[]; algoIdList: number[]; isEnabled: boolean }) {
+  return http.request({
+    url: '/admin/algo/batchUpdateCameraAlgoRelStatus',
+    method: 'post',
+    data,
+  });
+}
+
+// 相机算法批量删除
+export function deleteAlgosByBatch(params: { cameraIdList: number[]; algoIdList: number[] }) {
+  return http.request({
+    url: '/admin/algo/batchDeleteCameraAlgoRel',
+    method: 'post',
+    params,
+  });
+}
+
+export interface QueryAlgoInfoRes {
+  id: number; // 算法id
+  code: string; // 算法code
+  name: string; // 算法名称
+  showName: string; // 算法展示名称
+  remark: string; // 算法描述
+  status: number; // 算法状态: 0-启用, 1-禁用
+  isDeleted: number; // 是否删除: 0-否, 1-是
+  extra: string; // 扩展信息(算法默认参数)
+}
+
+// 获取权限范围内所有算法及其默认参数
+export function getAlgosInfo() {
+  return http.request({
+    url: '/admin/algo/queryAlgoInfo',
+    method: 'get',
+  });
+}

+ 92 - 24
src/api/camera/camera-preview.ts

@@ -144,10 +144,12 @@ export const getCameraAlgoListApi = (cameraId: number): Promise<CameraAlgoItem[]
 interface SaveCameraAlgoParam {
   algoId: number;
   cameraId: number;
-  detectionFrequency: number;
-  detectionTime: string;
-  electronicFence: string;
-  status: 0 | 1;
+  detectionFrequency?: number;
+  detectionTime?: string;
+  /** 电子围栏开启还是关闭 */
+  electronicFence?: 0 | 1;
+  /** 算法开启还是关闭 */
+  status?: 0 | 1;
 }
 
 interface CreateCameraAlgoParam {
@@ -207,53 +209,118 @@ export interface GetFenceParams {
 }
 
 /** 查询电子围栏 */
-export const getFenceApi = (params: GetFenceParams): Promise<{ id: number; electronicFencePolygon: string }> => {
+export const getFenceApi = (params: GetFenceParams): Promise<{ id: number; electronicFence: string }> => {
   return http.request({
-    url: '/admin/cameraPreview/queryFence',
-    method: 'GET',
+    url: '/admin/algo/queryFence',
+    method: 'POST',
     params,
   });
 };
 
+// export interface SaveFenceParams {
+//   algoId: number;
+//   cameraId: number;
+//   electronicFencePolygon: string;
+//   presetToken: string;
+// }
+/** 添加电子围栏 */
+// export const saveFenceApi = (data: SaveFenceParams) => {
+//   return http.request({
+//     url: '/admin/cameraPreview/saveFence',
+//     method: 'post',
+//     data,
+//   });
+// };
+
 export interface SaveFenceParams {
-  algoId: number;
+  /*相机id */
   cameraId: number;
-  electronicFencePolygon: string;
+
+  /*算法id */
+  algoId: number;
+
+  /*摄像头预置位token */
   presetToken: string;
+
+  /*电子围栏标签 */
+  fenceLabel?: string;
+
+  /*电子围栏名称 */
+  fenceName?: string;
+
+  /*电子围栏点位信息 */
+  fencePolygon: string;
 }
-/** 添加电子围栏 */
 export const saveFenceApi = (data: SaveFenceParams) => {
   return http.request({
-    url: '/admin/cameraPreview/saveFence',
+    url: '/admin/algo/saveFence',
     method: 'post',
     data,
   });
 };
 
-interface UpdateFenceParams {
-  algoId: number;
+export interface DeleteFenceParams {
+  /*相机id */
   cameraId: number;
-  electronicFencePolygon: string;
-  id: number;
+
+  /*算法id */
+  algoId: number;
+
+  /*摄像头预置位token */
   presetToken: string;
+  fenceId: number;
 }
-/** 编辑电子围栏 */
-export const editFenceApi = (data: UpdateFenceParams) => {
+/** 删除单个电子围栏 */
+export const deleteFenceApi = (data: DeleteFenceParams) => {
   return http.request({
-    url: '/admin/cameraPreview/updateFence',
-    method: 'put',
+    url: '/admin/algo/deleteFence',
+    method: 'post',
     data,
   });
 };
 
-/** 删除电子围栏 */
-export const deleteFenceApi = (cameraAlgoPresetRelId: number) => {
+export interface UpdateFenceParams {
+  /*相机id */
+  cameraId: number;
+
+  /*算法id */
+  algoId: number;
+
+  /*摄像头预置位token */
+  presetToken: string;
+
+  /*电子围栏id */
+  fenceId: number;
+
+  /*电子围栏标签 */
+  fenceLabel?: string;
+
+  /*电子围栏名称 */
+  fenceName?: string;
+
+  /*电子围栏点位信息 */
+  fencePolygon?: string;
+
+  /*是否开启(该算法电子围栏总开关) */
+  isDisabled?: boolean;
+}
+/** 编辑电子围栏 */
+export const editFenceApi = (data: UpdateFenceParams) => {
   return http.request({
-    url: `/cameraPreview/deleteFence?cameraAlgoPresetRelId=${cameraAlgoPresetRelId}`,
-    method: 'delete',
+    url: '/admin/algo/updateFence',
+    method: 'post',
+    data,
   });
 };
 
+// /** 删除电子围栏 */
+// export const deleteFenceApi = (cameraAlgoPresetRelId: number) => {
+//   return http.request({
+//     url: `/cameraPreview/deleteFence?cameraAlgoPresetRelId=${cameraAlgoPresetRelId}`,
+//     method: 'delete',
+//   });
+// };
+
 interface CreatePresetParam {
   presetName: string;
   cameraId: number;
@@ -311,7 +378,8 @@ interface PresetDetailItem {
 export const getPresetListApi = (cameraId: number) => {
   return http.request<PresetDetailItem[]>(
     {
-      url: `/camera/getPresets`,
+      url: `/onvif/getPresets`,
+      // url: `/admin/algo/queryCameraPreset`,
       method: 'get',
       params: { cameraId },
     },

Разлика између датотеке није приказан због своје велике величине
+ 8 - 0
src/assets/icons/electronic-fence.svg


BIN
src/assets/icons/more-dot-icon.png


src/views/datamanager/alertformdata/components/common/Pagination.vue → src/components/Pagination/Pagination.vue


+ 38 - 54
src/views/cameras/preview/components/AlgorithmsSetting/AlgoParamsCard.vue

@@ -1,15 +1,6 @@
 <template>
-  <div
-    class="algo-params-card"
-    :class="{ 'algo-params-card-active': markedParamCardIds.includes(props.id) }"
-  >
-    <el-form
-      ref="ruleFormRef"
-      :model="algoParams"
-      :inline="true"
-      :rules="rules"
-      label-width="120px"
-    >
+  <div class="algo-params-card" :class="{ 'algo-params-card-active': markedParamCardIds.includes(props.id) }">
+    <el-form ref="ruleFormRef" :model="algoParams" :inline="true" :rules="rules" label-width="120px">
       <div v-for="item in paramItems">
         <el-form-item v-if="item.type === 'label'" label="检测对象:" prop="label">
           <el-select v-model="algoParams.label" style="width: 186px" @change="handleLabelChange">
@@ -24,7 +15,7 @@
         </el-form-item>
         <el-form-item
           v-if="item.type === 'criticalCount'"
-          :label="labelNameMap[item.label] + '检测数量:'"
+          :label="labelNameMap[item.label] || '' + '检测数量:'"
           :prop="item.prop"
           required
         >
@@ -36,11 +27,12 @@
             style="width: 186px; margin-right: 5px"
             :disabled="!algoParams.label"
             placeholder="请输入检测数量"
+            @change="handleChange"
           />
         </el-form-item>
         <el-form-item
           v-if="item.type === 'confidence'"
-          :label="labelNameMap[item.label] + '置信度:'"
+          :label="labelNameMap[item.label] || '' + '置信度:'"
           :prop="item.prop"
           required
         >
@@ -52,6 +44,7 @@
               :step="1"
               style="width: 128px; margin-right: 6px"
               :disabled="!algoParams.label"
+              @change="handleChange"
             />
 
             <ElInputNumber
@@ -61,13 +54,14 @@
               :step="1"
               style="width: 88px; margin-right: 5px"
               :disabled="!algoParams.label"
+              @change="handleChange"
             />
             <span>%</span>
           </div>
         </el-form-item>
         <el-form-item
           v-if="item.type === 'minArea'"
-          :label="labelNameMap[item.label] + '最小检测面积:'"
+          :label="labelNameMap[item.label] || '' + '最小检测面积:'"
           required
         >
           <el-form-item :prop="item.label ? item.label + '.' + 'min_width' : 'min_width'">
@@ -78,13 +72,11 @@
               :step="1"
               style="width: 88px; margin-right: 5px"
               :disabled="!algoParams.label"
+              @change="handleChange"
             />
             <span>px</span>
           </el-form-item>
-          <el-form-item
-            :prop="item.label ? item.label + '.' + 'min_height' : 'min_height'"
-            style="margin-left: 17px"
-          >
+          <el-form-item :prop="item.label ? item.label + '.' + 'min_height' : 'min_height'" style="margin-left: 17px">
             <ElInputNumber
               v-model="algoParams[item.label ? item.label + '.' + 'min_height' : 'min_height']"
               controls-position="right"
@@ -92,6 +84,7 @@
               :step="1"
               style="width: 88px; margin-right: 5px"
               :disabled="!algoParams.label"
+              @change="handleChange"
             />
             <span>px</span>
           </el-form-item>
@@ -99,37 +92,44 @@
       </div>
     </el-form>
     <div class="paramOptIcons">
-      <!-- <el-icon v-if="isEdit" size="16px" @click="handleSaveParam(ruleFormRef)">
-        <SaveOutline />
-      </el-icon>
-      <el-icon v-else size="16px" @click="isEdit = true"><Edit /></el-icon> -->
-      <el-icon size="16px" @click="deleteParam(props.id)"><Delete /></el-icon>
+      <el-icon size="16px" @click="handleDelete(props.id)"><Delete /></el-icon>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
   import { computed, ref, watch } from 'vue';
-  import useCameraAlgoStore, { AlgoParamMetaItem } from '../../store/useCameraAlgoStore';
+  import useCameraAlgoStore from './use-algo-params-edit-store';
   import { storeToRefs } from 'pinia';
-  import { ElInputNumber, ElMessage, FormInstance } from 'element-plus';
-  import { Delete, Edit } from '@element-plus/icons-vue';
-  import { SaveOutline } from '@vicons/ionicons5';
-  import { labelNameMap } from './types';
+  import { ElInputNumber, FormInstance } from 'element-plus';
+  import { Delete } from '@element-plus/icons-vue';
+  import { labelNameMap, AlgoParamMetaItem } from './types';
   import { getCriticalCounts } from './utils';
 
   const props = defineProps<{
     id: string;
   }>();
 
+  const emits = defineEmits<{
+    (e: 'change'): unknown;
+  }>();
+
+  const handleChange = () => {
+    emits('change');
+  };
+
   const cameraAlgoStore = useCameraAlgoStore();
   const { selectedAlgoDetail, metaObjList, markedParamCardIds } = storeToRefs(cameraAlgoStore);
   const { deleteParam } = cameraAlgoStore;
 
   const ruleFormRef = ref<FormInstance>();
-  //是否在编辑
-  const isEdit = ref(false);
 
+  const handleDelete = (id: string) => {
+    handleChange();
+    deleteParam(id);
+  };
+
+  //get label list for select, exclude current label and label with nextObj
   const getLabelList = () => {
     return metaObjList.value.map((item) => {
       const entry = selectedAlgoDetail.value.metaValues.find(
@@ -144,14 +144,12 @@
   };
 
   const handleLabelChange = () => {
-    markedParamCardIds.value = selectedAlgoDetail.value.metaValues
-      .filter((item) => !item.label)
-      .map((item) => item.id);
+    handleChange();
+    markedParamCardIds.value = selectedAlgoDetail.value.metaValues.filter((item) => !item.label).map((item) => item.id);
   };
 
   const algoParams = ref<AlgoParamMetaItem>(
-    selectedAlgoDetail.value.metaValues?.find((item) => item.id === props.id) ||
-      ({} as AlgoParamMetaItem),
+    selectedAlgoDetail.value.metaValues?.find((item) => item.id === props.id) || ({} as AlgoParamMetaItem),
   );
 
   const paramItems = ref([
@@ -192,8 +190,7 @@
       if (selectedAlgoDetail.value.metaValues[index]['min_width']) {
         const countList = getCriticalCounts(selectedAlgoDetail.value.extra);
         algoParams.value = selectedAlgoDetail.value.metaValues[index];
-        algoParams.value.criticalCount =
-          countList && countList.length > index ? countList[index] : 0;
+        algoParams.value.criticalCount = countList && countList.length > index ? countList[index] : 0;
       } else {
         algoParams.value.confidence = Number((meta.confidence * 100).toFixed(0));
         algoParams.value['min_width'] = meta['min_width'];
@@ -259,7 +256,7 @@
     return rule;
   });
 
-  const integerJudge = (rule, value, callback) => {
+  const integerJudge = (_, value, callback) => {
     // 整数校验逻辑
     const pattern = /^-?\d+$/;
     if (pattern.test(value)) {
@@ -269,21 +266,10 @@
     }
   };
 
-  const handleSaveParam = async (formEl: FormInstance | undefined) => {
-    if (!formEl) return;
-    await formEl.validate((valid, fields) => {
-      if (valid) {
-        isEdit.value = false;
-      } else {
-        ElMessage.error('保存失败,请检查填写');
-      }
-    });
-  };
-
   const checkValid = async () => {
     if (!ruleFormRef.value) return false;
     let isValid = true;
-    await ruleFormRef.value.validate((valid, fields) => {
+    await ruleFormRef.value.validate((valid) => {
       if (valid) {
       } else {
         isValid = false;
@@ -292,10 +278,6 @@
     return isValid;
   };
 
-  const setEditable = (value: boolean) => {
-    isEdit.value = value;
-  };
-
   defineExpose({
     checkValid,
   });
@@ -333,11 +315,13 @@
   :deep(.el-form--inline .el-form-item) {
     margin-bottom: 10px;
     margin-right: 10px;
+    align-items: center;
   }
 
   :deep(.el-form-item__label) {
     line-height: 16px;
     font-size: 14px;
+    align-items: center;
   }
 
   :deep(.el-form--inline .el-form-item) {

+ 10 - 37
src/views/cameras/preview/components/AlgorithmsSetting/AlgoPeriodCard.vue

@@ -1,12 +1,7 @@
 <template>
   <div class="periodCard" :class="{ 'periodCard-active': markedTimeRangeIds.includes(props.id) }">
     <div class="dayRrange">
-      <el-select
-        class="daySelect"
-        v-model="timeItem.startDay"
-        placeholder="开始日期"
-        @change="changeDay"
-      >
+      <el-select class="daySelect" v-model="timeItem.startDay" placeholder="开始日期" @change="changeDay">
         <el-option
           v-for="item in dayOptions"
           :key="item.value"
@@ -17,12 +12,7 @@
         </el-option>
       </el-select>
       <div class="divider">-</div>
-      <el-select
-        class="daySelect"
-        v-model="timeItem.endDay"
-        placeholder="结束日期"
-        @change="changeDay"
-      >
+      <el-select class="daySelect" v-model="timeItem.endDay" placeholder="结束日期" @change="changeDay">
         <el-option
           v-for="item in dayOptions"
           :key="item.value"
@@ -39,7 +29,6 @@
           v-model="item.startTime"
           format="HH:mm"
           value-format="HH:mm"
-          :teleported="false"
           :editable="false"
           placeholder="开始时间"
           :disabled-hours="() => disabledHours(item.id, 0)"
@@ -52,7 +41,6 @@
           v-model="item.endTime"
           format="HH:mm"
           value-format="HH:mm"
-          :teleported="false"
           :editable="false"
           placeholder="结束时间"
           :disabled-hours="() => disabledHours(item.id, 1)"
@@ -61,12 +49,7 @@
           @blur="handleBlur(item.id)"
         />
       </div>
-      <el-icon
-        size="18px"
-        color="#d0d0d0"
-        @click="handleDeleteTimeRange(item.id)"
-        style="cursor: pointer"
-      >
+      <el-icon size="18px" color="#d0d0d0" @click="handleDeleteTimeRange(item.id)" style="cursor: pointer">
         <CircleClose />
       </el-icon>
     </div>
@@ -83,7 +66,7 @@
   import { computed, ref } from 'vue';
   import { TimePeriodItem } from './types';
   import { Plus, Delete, CircleClose } from '@element-plus/icons-vue';
-  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
+  import useCameraAlgoStore from './use-algo-params-edit-store';
   import { storeToRefs } from 'pinia';
   import { ElMessage } from 'element-plus';
   import { uid } from 'uid';
@@ -130,18 +113,14 @@
   const openTimeType = ref<number | null>();
 
   const timeItem = computed(
-    () =>
-      selectedAlgoDetail.value.timeRangeArr.find((item) => item.id === props.id) ||
-      ({} as TimePeriodItem),
+    () => selectedAlgoDetail.value.timeRangeArr.find((item) => item.id === props.id) || ({} as TimePeriodItem),
   );
 
   const changeDay = () => {
     if (timeItem.value.startDay && timeItem.value.endDay) {
       let isConflict = false;
       //判断日期是否冲突
-      const otherDays = selectedAlgoDetail.value.timeRangeArr.filter(
-        (item) => item.id !== props.id,
-      );
+      const otherDays = selectedAlgoDetail.value.timeRangeArr.filter((item) => item.id !== props.id);
       for (let i = 0; i < otherDays.length; i++) {
         const dayRange = otherDays[i];
         if (
@@ -151,10 +130,7 @@
           isConflict = true;
           break;
         }
-        if (
-          timeItem.value.startDay < dayRange.startDay! &&
-          timeItem.value.endDay > dayRange.endDay!
-        ) {
+        if (timeItem.value.startDay < dayRange.startDay! && timeItem.value.endDay > dayRange.endDay!) {
           isConflict = true;
           break;
         }
@@ -294,14 +270,13 @@
   };
 
   const handleDeleteTimeRange = (curId: string) => {
+    debugger;
     timeItem.value.timeRangeList = timeItem.value.timeRangeList.filter((item) => item.id !== curId);
     markedTimes.value = markedTimes.value.filter((id) => id !== curId);
   };
 
   const handleAddTimeRange = () => {
-    const emptyList = timeItem.value.timeRangeList.filter(
-      (item) => item.startTime === '' || item.endTime === '',
-    );
+    const emptyList = timeItem.value.timeRangeList.filter((item) => item.startTime === '' || item.endTime === '');
     if (emptyList && emptyList.length > 0) {
       emptyList.forEach((time) => markedTimes.value.push(time.id));
       ElMessage.error('请填写完整时间段');
@@ -353,9 +328,7 @@
     if (markedTimes.value.length > 0) {
       return 0; // 存在冲突时间段
     }
-    const emptyTimes = timeItem.value.timeRangeList.filter(
-      (item) => !item.startTime || !item.endTime,
-    );
+    const emptyTimes = timeItem.value.timeRangeList.filter((item) => !item.startTime || !item.endTime);
     if (emptyTimes && emptyTimes.length > 0) {
       emptyTimes.forEach((val) => markedTimes.value.push(val.id));
       return 1; // 存在缺失时间段

Разлика између датотеке није приказан због своје велике величине
+ 477 - 522
src/views/cameras/preview/components/AlgorithmsSetting/AlgoSettingCard.vue


+ 74 - 46
src/views/cameras/preview/components/AlgorithmsSetting/types.ts

@@ -1,46 +1,74 @@
-import { Dayjs } from 'dayjs';
-
-export interface TimeRangeItem {
-  id: string;
-  value: [Dayjs, Dayjs];
-}
-
-export interface TimePeriodItem {
-  id: string;
-  startDay: number | null;
-  endDay: number | null;
-  timeRangeList: { id: string; startTime: string; endTime: string }[];
-}
-
-export const labelNameMap = {
-  '': '',
-  person: '人',
-  withHelmet: '安全帽',
-  bird: '鸟',
-  cat: '猫',
-  dog: '狗',
-  snake: '蛇',
-  mouse: '鼠',
-  vest: '反光背心',
-  car: '小汽车',
-  bus: '大巴',
-  fire: '火焰',
-  sleep: '睡觉',
-  phone: '手机',
-  smoke: '烟雾',
-  withSafeBelt: '戴安全带',
-  red: '红灯',
-  none: '不亮',
-  'baby-blue': '浅蓝工服',
-  'dark-blue': '深蓝工服',
-  'royal-blue': '宝蓝工服',
-  white: '白工服',
-  vague: '模糊工服',
-  airplane: '飞机',
-  planeHead: '机头',
-  frontPlaneBody: '前机身',
-  midPlaneBody: '中机身',
-  rearPlaneBody: '后机身',
-  planeWing: '机翼',
-  planeTail: '机尾',
-};
+import { Dayjs } from 'dayjs';
+import { CameraAlgoItem } from '@/api/camera/camera-preview';
+
+export interface TimeRangeItem {
+  id: string;
+  value: [Dayjs, Dayjs];
+}
+
+export interface TimePeriodItem {
+  id: string;
+  startDay: number | null;
+  endDay: number | null;
+  timeRangeList: { id: string; startTime: string; endTime: string }[];
+}
+
+export const labelNameMap = {
+  '': '',
+  person: '人',
+  withHelmet: '安全帽',
+  bird: '鸟',
+  cat: '猫',
+  dog: '狗',
+  snake: '蛇',
+  mouse: '鼠',
+  vest: '反光背心',
+  car: '小汽车',
+  bus: '大巴',
+  fire: '火焰',
+  sleep: '睡觉',
+  phone: '手机',
+  smoke: '烟雾',
+  withSafeBelt: '戴安全带',
+  red: '红灯',
+  none: '不亮',
+  'baby-blue': '浅蓝工服',
+  'dark-blue': '深蓝工服',
+  'royal-blue': '宝蓝工服',
+  white: '白工服',
+  vague: '模糊工服',
+  airplane: '飞机',
+  planeHead: '机头',
+  frontPlaneBody: '前机身',
+  midPlaneBody: '中机身',
+  rearPlaneBody: '后机身',
+  planeWing: '机翼',
+  planeTail: '机尾',
+};
+
+export interface CameraAlgoItemInCard extends CameraAlgoItem {
+  // detectionJSON: DetectionJSON;
+  inferCode: string;
+  enableCardBool: boolean;
+  electronicFenceBool: boolean;
+  regionJudge: number;
+  timeRangeArr: TimePeriodItem[];
+  metaValues: AlgoParamMetaItem[];
+  judge: number; // 0-小于 1-大于 2-等于
+  detectionFrequency: number;
+  eventDurationMinMs: number;
+  eventDurationMinFrames: number;
+  eventAlarmIntervalMs: number;
+  eventAlarmIntervalFrames: number;
+  timeWindow?: number;
+}
+
+export interface AlgoParamMetaItem {
+  id: string;
+  label: string;
+  criticalCount: number;
+  confidence: number;
+  min_width: number;
+  max_width: number;
+  nextObjs: AlgoParamMetaItem[];
+}

+ 60 - 0
src/modules/algo/algo-params-edit/use-algo-params-edit-store.ts

@@ -0,0 +1,60 @@
+import { defineStore } from 'pinia';
+import { computed, ref } from 'vue';
+import { CameraAlgoItemInCard } from '@/modules/algo/algo-params-edit/types';
+
+const defaultSelectedAlgoDetail = {
+  // detectionJSON: { detectionNum: 0, detectionUnit: 1 },
+  regionJudge: 0,
+};
+
+const useAlgoParamsEditStore = defineStore('algoParamsEdit', () => {
+  // 标记的paramCard集合
+  const markedParamCardIds = ref<string[]>([]);
+
+  // 标记的timeRange集合
+  const markedTimeRangeIds = ref<string[]>([]);
+
+  const selectedAlgoDetail = ref<CameraAlgoItemInCard>({
+    ...defaultSelectedAlgoDetail,
+  } as CameraAlgoItemInCard);
+
+  //计算原始模板数据
+  const metaObjList = computed(() => {
+    const extra = selectedAlgoDetail.value.algoInfo?.extra;
+    if (!extra) return [];
+    const extraObj = JSON.parse(extra);
+    const params = extraObj?.inferParams;
+    if (!params || (params && params.length == 0)) return [];
+    const metaObjs = params[0]?.metaObjs;
+
+    return metaObjs ? metaObjs : [];
+  });
+
+  const deleteParam = (id: string) => {
+    selectedAlgoDetail.value.metaValues = selectedAlgoDetail.value.metaValues.filter((x) => x.id !== id);
+  };
+
+  const deleteTimeRange = (id: string) => {
+    const newTimeRangeArr = selectedAlgoDetail.value.timeRangeArr.filter((item) => item.id !== id);
+    selectedAlgoDetail.value.timeRangeArr = newTimeRangeArr;
+    markedTimeRangeIds.value = markedTimeRangeIds.value.filter((x) => newTimeRangeArr.find((y) => y.id === x));
+  };
+
+  const clear = () => {
+    selectedAlgoDetail.value = { ...defaultSelectedAlgoDetail } as CameraAlgoItemInCard;
+    markedParamCardIds.value = [];
+    markedTimeRangeIds.value = [];
+  };
+
+  return {
+    metaObjList,
+    markedParamCardIds,
+    markedTimeRangeIds,
+    selectedAlgoDetail,
+    clear,
+    deleteParam,
+    deleteTimeRange,
+  };
+});
+
+export default useAlgoParamsEditStore;

+ 268 - 198
src/views/cameras/preview/components/AlgorithmsSetting/utils.ts

@@ -1,198 +1,268 @@
-import dayjs, { Dayjs } from 'dayjs';
-import { uid } from 'uid';
-import NP from 'number-precision';
-import { isEqual } from 'lodash-es';
-
-import { TimeRangeItem, TimePeriodItem } from './types';
-import { CameraAlgoItem } from '@/api/camera/camera-preview';
-import safeParse from '@/utils/safeParse';
-
-// export const createDefaultTime = (): TimeRangeItem => {
-//   return { id: uid(), value: [dayjs(), dayjs().add(1, 'hour')] as [Dayjs, Dayjs] };
-// };
-
-export const createDefaultTime = (): TimePeriodItem => {
-  return {
-    id: uid(),
-    startDay: null,
-    endDay: null,
-    timeRangeList: [{ id: uid(), startTime: '', endTime: '' }],
-  };
-};
-
-export enum FrequencyEnum {
-  second = 1,
-  miniute = 60,
-  hour = 3600,
-}
-
-export const frequencyOptions = [
-  { label: '秒', value: FrequencyEnum.second },
-  { label: '分钟', value: FrequencyEnum.miniute },
-  { label: '小时', value: FrequencyEnum.hour },
-];
-
-export interface DetectionJSON {
-  detectionNum: number;
-  detectionUnit: FrequencyEnum;
-}
-/** 根据后端返回的时间(单位是秒),拆分成单位和数值 */
-export const getDetectionJSON = (time: number | undefined | null): DetectionJSON => {
-  if (time && time > 0) {
-    for (let i = frequencyOptions.length - 1; i >= 0; i--) {
-      const unit = frequencyOptions[i].value;
-      if (time >= unit) {
-        return { detectionNum: NP.divide(time, unit), detectionUnit: unit };
-      }
-    }
-  }
-  return { detectionNum: 5, detectionUnit: FrequencyEnum.miniute };
-};
-
-export const getDetectionTimeJSON = (time?: string): TimeRangeItem[] | null => {
-  if (!time) return null;
-  const timeArr = time.split(';');
-  const nowStr = dayjs().format('YYYY-MM-DD');
-  const timeStrArr = timeArr
-    .map((x) => {
-      const [startDate, endDate] = x.split('-');
-      return [dayjs(`${nowStr} ${startDate}`), dayjs(`${nowStr} ${endDate}`)] as [Dayjs, Dayjs];
-    })
-    .map((x) => {
-      return { id: uid(), value: x };
-    });
-  return timeStrArr;
-};
-
-export const getInferParam = (extra: string | undefined | null) => {
-  if (!extra) return {};
-  const extraObj = safeParse(extra);
-  const params = extraObj?.inferParams;
-  if (!params || (params && params.length == 0)) return {};
-  return params[0];
-};
-
-export const getMetaValues = (extra: string | undefined | null) => {
-  if (!extra) return [];
-  const extraObj = safeParse(extra);
-  const params = extraObj?.inferParams;
-  if (!params || (params && params.length == 0)) return [];
-  const metaObjs = params[0]?.metaObjs;
-  if (!metaObjs || (metaObjs && metaObjs.length == 0)) return [];
-  const metaArr = metaObjs.map((item: any) => {
-    const val = {
-      id: uid(),
-      label: item.label,
-      confidence: Number((item.confidence * 100).toFixed(0)),
-      min_width: item['min_width'],
-      min_height: item['min_height'],
-    } as any;
-    item.nextObjs.forEach((next) => {
-      val[`${next.label}.confidence`] = Number((next.confidence * 100).toFixed(0));
-      val[next.label + '.' + 'min_width'] = next['min_width'];
-      val[next.label + '.' + 'min_height'] = next['min_height'];
-    });
-    return val;
-  });
-
-  return metaArr;
-};
-
-export const getDetectionTime = (time: string | undefined | null) => {
-  if (!time) return [];
-  const timeList = safeParse(time);
-  if (!timeList || (timeList && timeList.length === 0)) {
-    return [];
-  }
-  return timeList;
-};
-
-export const getInferCode = (extra: string | undefined | null) => {
-  if (!extra) return '';
-  const extraObj = safeParse(extra);
-  return extraObj?.inferCode || '';
-};
-
-export const getAlgoType = (extra: string | undefined | null) => {
-  if (!extra) return 0;
-  const extraObj = safeParse(extra);
-  const infers = extraObj?.inferParams;
-  if (!infers || infers.length === 0) return 0;
-  return infers[0]?.algoType || 0;
-};
-
-export const getCriticalCounts = (extra: string | undefined | null) => {
-  if (!extra) return [];
-  const extraObj = safeParse(extra);
-  const infers = extraObj?.inferParams;
-  if (!infers || infers.length === 0) return [];
-  return infers[0]?.criticalCounts || [];
-};
-
-export const getExtraCommonInfo = (detail: CameraAlgoItem | undefined | null) => {
-  if (!detail) return {};
-  let extraValue = getCommonInfo(detail.extra);
-  if (isEqual(extraValue, {})) {
-    extraValue = getCommonInfo(detail.algoInfo?.extra);
-  }
-  return extraValue;
-};
-
-interface CommonInfo {
-  regionJudge?: number;
-  judge?: number;
-  eventDurationMinMs?: number;
-  eventDurationMinFrames?: number;
-  eventAlarmIntervalMs?: number;
-  eventAlarmIntervalFrames?: number;
-  timeWindow?: number;
-}
-
-const getCommonInfo = (extra: string | undefined | null): CommonInfo => {
-  if (!extra) return {};
-  const extraObj = safeParse(extra);
-  const params = extraObj?.inferParams;
-  if (!params || (params && params.length == 0)) return {};
-  const regionJudge = params[0]?.regionJudge;
-  const judge = params[0]?.judge;
-  const eventDurationMinMs = params[0]?.eventDurationMinMs;
-  const eventDurationMinFrames = params[0]?.eventDurationMinFrames;
-  const eventAlarmIntervalMs = params[0]?.eventAlarmIntervalMs;
-  const eventAlarmIntervalFrames = params[0]?.eventAlarmIntervalFrames;
-  const timeWindow = params[0]?.timeWindow;
-  const ret = {} as CommonInfo;
-  if (regionJudge || regionJudge == 0) {
-    ret.regionJudge = regionJudge;
-  }
-  if (judge || judge == 0) {
-    ret.judge = judge;
-  }
-  if (eventDurationMinMs || eventDurationMinMs == 0) {
-    ret.eventDurationMinMs = eventDurationMinMs;
-  }
-  if (eventDurationMinFrames || eventDurationMinFrames == 0) {
-    ret.eventDurationMinFrames = eventDurationMinFrames;
-  }
-  if (eventAlarmIntervalMs || eventAlarmIntervalMs == 0) {
-    ret.eventAlarmIntervalMs = eventAlarmIntervalMs;
-  }
-  if (eventAlarmIntervalFrames || eventAlarmIntervalFrames == 0) {
-    ret.eventAlarmIntervalFrames = eventAlarmIntervalFrames;
-  }
-  if (timeWindow) {
-    ret.timeWindow = timeWindow;
-  }
-  return ret;
-};
-
-export const getTimeCompletion = (time: TimePeriodItem) => {
-  if (!time.startDay || !time.endDay) {
-    return false;
-  }
-  time.timeRangeList.forEach((item) => {
-    if (!item.startTime || !item.endTime) {
-      return false;
-    }
-  });
-  return true;
-};
+import dayjs, { Dayjs } from 'dayjs';
+import { uid } from 'uid';
+import NP from 'number-precision';
+import { isEqual } from 'lodash-es';
+
+import { TimeRangeItem, TimePeriodItem } from '@/modules/algo/algo-params-edit/types';
+
+import { ALGO_ENABLED_STATUS, CameraAlgoItem, FENCE_ENBALED_STATUS } from '@/api/camera/camera-preview';
+import safeParse from '@/utils/safeParse';
+
+// export const createDefaultTime = (): TimeRangeItem => {
+//   return { id: uid(), value: [dayjs(), dayjs().add(1, 'hour')] as [Dayjs, Dayjs] };
+// };
+
+export const createDefaultTime = (): TimePeriodItem => {
+  return {
+    id: uid(),
+    startDay: null,
+    endDay: null,
+    timeRangeList: [{ id: uid(), startTime: '', endTime: '' }],
+  };
+};
+
+export enum FrequencyEnum {
+  second = 1,
+  miniute = 60,
+  hour = 3600,
+}
+
+export const frequencyOptions = [
+  { label: '秒', value: FrequencyEnum.second },
+  { label: '分钟', value: FrequencyEnum.miniute },
+  { label: '小时', value: FrequencyEnum.hour },
+];
+
+export interface DetectionJSON {
+  detectionNum: number;
+  detectionUnit: FrequencyEnum;
+}
+/** 根据后端返回的时间(单位是秒),拆分成单位和数值 */
+export const getDetectionJSON = (time: number | undefined | null): DetectionJSON => {
+  if (time && time > 0) {
+    for (let i = frequencyOptions.length - 1; i >= 0; i--) {
+      const unit = frequencyOptions[i].value;
+      if (time >= unit) {
+        return { detectionNum: NP.divide(time, unit), detectionUnit: unit };
+      }
+    }
+  }
+  return { detectionNum: 5, detectionUnit: FrequencyEnum.miniute };
+};
+
+export const getDetectionTimeJSON = (time?: string): TimeRangeItem[] | null => {
+  if (!time) return null;
+  const timeArr = time.split(';');
+  const nowStr = dayjs().format('YYYY-MM-DD');
+  const timeStrArr = timeArr
+    .map((x) => {
+      const [startDate, endDate] = x.split('-');
+      return [dayjs(`${nowStr} ${startDate}`), dayjs(`${nowStr} ${endDate}`)] as [Dayjs, Dayjs];
+    })
+    .map((x) => {
+      return { id: uid(), value: x };
+    });
+  return timeStrArr;
+};
+
+export const getInferParam = (extra: string | undefined | null) => {
+  if (!extra) return {};
+  const extraObj = safeParse(extra);
+  const params = extraObj?.inferParams;
+  if (!params || (params && params.length == 0)) return {};
+  return params[0];
+};
+
+export const getMetaValues = (extra: string | undefined | null) => {
+  if (!extra) return [];
+  const extraObj = safeParse(extra);
+  const params = extraObj?.inferParams;
+  if (!params || (params && params.length == 0)) return [];
+  const metaObjs = params[0]?.metaObjs;
+  if (!metaObjs || (metaObjs && metaObjs.length == 0)) return [];
+  const metaArr = metaObjs.map((item: any) => {
+    const val = {
+      id: uid(),
+      label: item.label,
+      confidence: Number((item.confidence * 100).toFixed(0)),
+      min_width: item['min_width'],
+      min_height: item['min_height'],
+    } as any;
+    item.nextObjs.forEach((next) => {
+      val[`${next.label}.confidence`] = Number((next.confidence * 100).toFixed(0));
+      val[next.label + '.' + 'min_width'] = next['min_width'];
+      val[next.label + '.' + 'min_height'] = next['min_height'];
+    });
+    return val;
+  });
+
+  return metaArr;
+};
+
+export const getDetectionTime = (time: string | undefined | null) => {
+  if (!time) return [];
+  const timeList = safeParse(time);
+  if (!timeList || (timeList && timeList.length === 0)) {
+    return [];
+  }
+  return timeList;
+};
+
+export const getInferCode = (extra: string | undefined | null) => {
+  if (!extra) return '';
+  const extraObj = safeParse(extra);
+  return extraObj?.inferCode || '';
+};
+
+export const getAlgoType = (extra: string | undefined | null) => {
+  if (!extra) return 0;
+  const extraObj = safeParse(extra);
+  const infers = extraObj?.inferParams;
+  if (!infers || infers.length === 0) return 0;
+  return infers[0]?.algoType || 0;
+};
+
+export const getCriticalCounts = (extra: string | undefined | null) => {
+  if (!extra) return [];
+  const extraObj = safeParse(extra);
+  const infers = extraObj?.inferParams;
+  if (!infers || infers.length === 0) return [];
+  return infers[0]?.criticalCounts || [];
+};
+
+export const getExtraCommonInfo = (detail: CameraAlgoItem | undefined | null) => {
+  if (!detail) return {};
+  let extraValue = getCommonInfo(detail.extra);
+  if (isEqual(extraValue, {})) {
+    extraValue = getCommonInfo(detail.algoInfo?.extra);
+  }
+  return extraValue;
+};
+
+interface CommonInfo {
+  regionJudge?: number;
+  judge?: number;
+  eventDurationMinMs?: number;
+  eventDurationMinFrames?: number;
+  eventAlarmIntervalMs?: number;
+  eventAlarmIntervalFrames?: number;
+  timeWindow?: number;
+}
+
+const getCommonInfo = (extra: string | undefined | null): CommonInfo => {
+  if (!extra) return {};
+  const extraObj = safeParse(extra);
+  const params = extraObj?.inferParams;
+  if (!params || (params && params.length == 0)) return {};
+  const regionJudge = params[0]?.regionJudge;
+  const judge = params[0]?.judge;
+  const eventDurationMinMs = params[0]?.eventDurationMinMs;
+  const eventDurationMinFrames = params[0]?.eventDurationMinFrames;
+  const eventAlarmIntervalMs = params[0]?.eventAlarmIntervalMs;
+  const eventAlarmIntervalFrames = params[0]?.eventAlarmIntervalFrames;
+  const timeWindow = params[0]?.timeWindow;
+  const ret = {} as CommonInfo;
+  if (regionJudge || regionJudge == 0) {
+    ret.regionJudge = regionJudge;
+  }
+  if (judge || judge == 0) {
+    ret.judge = judge;
+  }
+  if (eventDurationMinMs || eventDurationMinMs == 0) {
+    ret.eventDurationMinMs = eventDurationMinMs;
+  }
+  if (eventDurationMinFrames || eventDurationMinFrames == 0) {
+    ret.eventDurationMinFrames = eventDurationMinFrames;
+  }
+  if (eventAlarmIntervalMs || eventAlarmIntervalMs == 0) {
+    ret.eventAlarmIntervalMs = eventAlarmIntervalMs;
+  }
+  if (eventAlarmIntervalFrames || eventAlarmIntervalFrames == 0) {
+    ret.eventAlarmIntervalFrames = eventAlarmIntervalFrames;
+  }
+  if (timeWindow) {
+    ret.timeWindow = timeWindow;
+  }
+  return ret;
+};
+
+export const getTimeCompletion = (time: TimePeriodItem) => {
+  if (!time.startDay || !time.endDay) {
+    return false;
+  }
+  time.timeRangeList.forEach((item) => {
+    if (!item.startTime || !item.endTime) {
+      return false;
+    }
+  });
+  return true;
+};
+
+/** 生成算法相关的提交参数 */
+export const createAlgoSubmitParams = (param, initialAlgoDetail) => {
+  const inferParams = getInferParam(initialAlgoDetail.extra);
+  inferParams.metaObjs = param.metaObjs;
+  inferParams.regionJudge = param.regionJudge;
+  inferParams.criticalCounts = param.criticalCounts;
+  inferParams.judge = param.judge;
+  inferParams.eventDurationMinMs = param.eventDurationMinMs;
+  inferParams.eventDurationMinFrames = param.eventDurationMinFrames;
+  inferParams.eventAlarmIntervalMs = param.eventAlarmIntervalMs;
+  inferParams.eventAlarmIntervalFrames = param.eventAlarmIntervalFrames;
+  inferParams.algoCode = initialAlgoDetail.algoInfo.code;
+  inferParams.algoType = getAlgoType(initialAlgoDetail.algoInfo.extra);
+  if (param.timeWindow) {
+    inferParams.timeWindow = param.timeWindow;
+  }
+  const extraValue = {
+    inferCode: param.inferCode,
+    inferParams: [inferParams],
+  } as any;
+
+  const newParam = {
+    electronicFence: initialAlgoDetail.electronicFence,
+    algoId: initialAlgoDetail.algoId,
+    detectionFrequency: param.detectionFrequency,
+    detectionTime: param.detectionTime,
+    status: initialAlgoDetail.status,
+    extra: JSON.stringify(extraValue),
+  };
+  return newParam;
+};
+
+/** algo的extra详情转化为json */
+export const algoDetailToJSON = (detail) => {
+  const enableCard = detail?.status === ALGO_ENABLED_STATUS.enabled ? true : false;
+  const electronicFenceBool = detail?.electronicFence === FENCE_ENBALED_STATUS.enabled ? true : false;
+
+  // const timeRangeArr = getDetectionTimeJSON(detail?.detectionTime) || [];
+  const timeRangeArr = getDetectionTime(detail?.detectionTime) || [];
+  const metaValues = getMetaValues(detail?.extra) || [];
+
+  const commonInfo = getExtraCommonInfo(detail);
+
+  return {
+    ...detail,
+    inferCode: getInferCode(detail?.extra),
+    // detectionJSON,
+    enableCardBool: enableCard,
+    electronicFenceBool,
+    timeRangeArr,
+    metaValues,
+    regionJudge: commonInfo.regionJudge || 0,
+    judge: commonInfo.judge || commonInfo.judge == 0 ? commonInfo.judge : 1,
+    eventDurationMinMs:
+      commonInfo.eventDurationMinMs || commonInfo.eventDurationMinMs == 0 ? commonInfo.eventDurationMinMs : 1,
+    eventDurationMinFrames:
+      commonInfo.eventDurationMinFrames || commonInfo.eventDurationMinFrames == 0
+        ? commonInfo.eventDurationMinFrames
+        : 1,
+    eventAlarmIntervalMs:
+      commonInfo.eventAlarmIntervalMs || commonInfo.eventAlarmIntervalMs == 0 ? commonInfo.eventAlarmIntervalMs : 1,
+    eventAlarmIntervalFrames:
+      commonInfo.eventAlarmIntervalFrames || commonInfo.eventAlarmIntervalFrames == 0
+        ? commonInfo.eventAlarmIntervalFrames
+        : 1,
+    timeWindow: commonInfo.timeWindow,
+  };
+};

+ 11 - 0
src/router/full-routes.ts

@@ -225,6 +225,17 @@ const fullRoutes: AppRouteRecordRaw[] = [
           title: '算法配置',
         },
       },
+      {
+        // 算法参数设置,也就是设置的详情页
+        path: 'params',
+        name: 'AlgorithmParamsSetting',
+        component: '/cameras/algo-params-setting/AlgoParamsSetting',
+        meta: {
+          ico: '',
+          title: '算法参数设置',
+          activeMenu: 'AlgorithmConfig',
+        },
+      },
     ],
   },
 

+ 3 - 6
src/views/cameras/algo-management/algoManagement.vue

@@ -55,8 +55,7 @@
             </el-scrollbar>
           </div>
           <div class="top_right">
-            <video :src="currentRow?.url" controls autoplay muted style="width: 100%; height: 100%">
-            </video>
+            <video :src="currentRow?.url" controls autoplay muted style="width: 100%; height: 100%"> </video>
           </div>
         </div>
         <div class="right_bottom">
@@ -82,9 +81,7 @@
               />
             </el-form-item>
             <el-form-item>
-              <el-button type="primary" :loading="isSending" @click="onSubmit"
-                >保&nbsp;&nbsp;存</el-button
-              >
+              <el-button type="primary" :loading="isSending" @click="onSubmit">保&nbsp;&nbsp;存</el-button>
             </el-form-item>
           </el-form>
         </div>
@@ -99,7 +96,7 @@
   import useAlgo from './useAlgoData';
   import { Search } from '@element-plus/icons-vue';
   import type { FormInstance, FormRules } from 'element-plus';
-  import { labelNameMap } from '../preview/components/AlgorithmsSetting/types';
+  import { labelNameMap } from '@/modules/algo/algo-params-edit/types';
 
   //调用后端数据
   const algoDatas = useAlgo();

+ 138 - 0
src/views/cameras/algo-params-setting/AlgoParamsSetting.vue

@@ -0,0 +1,138 @@
+<!-- 算法参数配置页面 -->
+<template>
+  <div>
+    <div class="cameraMain">
+      <div class="cameraTree" v-show="cameraTreeVisible">
+        <CameraTreeCom />
+      </div>
+      <div v-if="cameraTreeVisible" class="arrow-icon" @click="cameraTreeVisible = false"
+        ><el-icon><DArrowLeft /></el-icon
+      ></div>
+      <div v-else class="arrow-icon" @click="cameraTreeVisible = true"
+        ><el-icon><DArrowRight /></el-icon
+      ></div>
+      <div class="cameraSettingWrapper">
+        <div class="cameraView">
+          <CameraViewSetting v-if="cameraDetailStore.cameraId" />
+          <div class="cameraPlaceholder" v-else>请选择左侧相机</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { onMounted, ref, watch } from 'vue';
+  import { ElIcon } from 'element-plus';
+  import { DArrowLeft, DArrowRight } from '@element-plus/icons-vue';
+  import { storeToRefs } from 'pinia';
+  import CameraTreeCom from './components/CameraTree/CameraTree.vue';
+  import CameraViewSetting from './components/CameraViewSetting/CameraViewSetting.vue';
+  import useCameraDetailStore from './store/useCameraDetailStore';
+  import useCameraAlgoStore from './store/useCameraAlgoStore';
+  import usePresetListStore from './store/usePresetListStore';
+  import useFenceStore from './store/useFenceStore';
+  import { IsPtz } from '@/types/camera/constant';
+  import { getCameraDeatilById } from '@/api/camera/camera-preview';
+
+  const cameraDetailStore = useCameraDetailStore();
+  const { isShowFence } = storeToRefs(cameraDetailStore);
+  const cameraAlgoStore = useCameraAlgoStore();
+  const fenceStore = useFenceStore();
+  const presetListStore = usePresetListStore();
+
+  const cameraTreeVisible = ref(true);
+
+  watch(
+    () => cameraDetailStore.cameraId,
+    async (cameraId) => {
+      isShowFence.value = false;
+      fenceStore.clear();
+      if (cameraId) {
+        // FIXME: 缺 后端v4 api
+        getCameraDeatilById(cameraId).then(async (res) => {
+          cameraDetailStore.setDetail(res);
+          // 如果isPtz为null,或者为0,都按照枪击相机
+          if (res.isPtz === IsPtz.disabled || !res.isPtz) {
+            const presetList = await presetListStore.getPresetList(cameraId);
+            presetListStore.currentPresetToken = presetList?.[0].token;
+          }
+        });
+
+        cameraAlgoStore.getCameraAlgoList(cameraId);
+        cameraAlgoStore.selectedAlgoId = null;
+      }
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  onMounted(() => {
+    cameraAlgoStore.getAllAlgoList();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .cameraParamsSetting {
+    width: 350px;
+    min-height: 300px;
+    // border: 1px solid #ccc;
+  }
+
+  .cameraParamsSetting {
+    width: 350px;
+    min-height: 300px;
+    // border: 1px solid #ccc;
+  }
+
+  .algorithmsSetting {
+    flex: 1;
+    border-left: 1px solid #ccc;
+    padding-left: 20px;
+  }
+  .cameraMain {
+    display: flex;
+    background: #fff;
+    overflow-x: auto;
+    // height: calc(100vh - 90px);
+  }
+  .cameraTree {
+    min-width: 270px;
+    max-width: 600px;
+    flex-shrink: 0;
+    // height: 800px;
+    // border: 1px solid #ccc;
+    border: 1px solid #f0f2f5;
+    margin: 5px;
+  }
+
+  .cameraPlaceholder {
+    color: #333;
+    text-align: center;
+    margin-top: 100px;
+    margin-left: 100px;
+  }
+
+  .arrow-icon {
+    width: 16px;
+    height: 48px;
+    margin: 320px 0;
+    border-radius: 15px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+    background-color: #bee2ff;
+  }
+
+  .arrow-icon:hover {
+    color: #fff;
+    background-color: #0052d9;
+  }
+
+  .cameraView {
+    width: 1230px;
+    position: relative;
+  }
+</style>

+ 44 - 44
src/views/cameras/preview/components/AddPresetModal/AddPresetModal.vue

@@ -1,44 +1,44 @@
-<template>
-  <ElDialog title="添加预置位" :model-value="true" @close="emits('close')" width="500px">
-    <template #footer>
-      <el-button @click="emits('close')">取消</el-button>
-      <el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
-    </template>
-    <ElInput type="textarea" autosize v-model="presetName" maxlength="15" show-word-limit />
-  </ElDialog>
-</template>
-<script lang="ts" setup>
-import { createPresetApi } from '@/api/camera/camera-preview';
-import { ElDialog, ElInput, ElMessage } from 'element-plus';
-import { ref } from 'vue';
-import usePresetListStore from '../../store/usePresetListStore';
-import useCameraDetailStore from '../../store/useCameraDetailStore';
-const presetName = ref('');
-const loading = ref(false);
-const emits = defineEmits<{ (e: 'close'): unknown; (e: 'ok'): unknown }>();
-const presetStore = usePresetListStore();
-
-const cameraDetailStore = useCameraDetailStore();
-
-const handleSubmit = () => {
-  if (presetStore.isPresetNameExist(presetName.value)) {
-    ElMessage.error('预置位名称已存在');
-    return;
-  }
-  loading.value = true;
-  createPresetApi({ presetName: presetName.value, cameraId: cameraDetailStore.cameraId })
-    .then((val) => {
-      if (val) {
-        ElMessage.success('预置位创建成功');
-        presetStore.currentPresetToken = val;
-        emits('ok');
-      } else {
-        ElMessage.error('创建失败:已有预置位数量可能超限,请删除后再添加');
-      }
-    })
-    .finally(() => {
-      loading.value = false;
-    });
-};
-</script>
-<style scoped></style>
+<template>
+  <ElDialog title="添加预置位" :model-value="true" @close="emits('close')" width="500px">
+    <template #footer>
+      <el-button @click="emits('close')">取消</el-button>
+      <el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
+    </template>
+    <ElInput type="textarea" autosize v-model="presetName" maxlength="15" show-word-limit />
+  </ElDialog>
+</template>
+<script lang="ts" setup>
+import { createPresetApi } from '@/api/camera/camera-preview';
+import { ElDialog, ElInput, ElMessage } from 'element-plus';
+import { ref } from 'vue';
+import usePresetListStore from '../../store/usePresetListStore';
+import useCameraDetailStore from '../../store/useCameraDetailStore';
+const presetName = ref('');
+const loading = ref(false);
+const emits = defineEmits<{ (e: 'close'): unknown; (e: 'ok'): unknown }>();
+const presetStore = usePresetListStore();
+
+const cameraDetailStore = useCameraDetailStore();
+
+const handleSubmit = () => {
+  if (presetStore.isPresetNameExist(presetName.value)) {
+    ElMessage.error('预置位名称已存在');
+    return;
+  }
+  loading.value = true;
+  createPresetApi({ presetName: presetName.value, cameraId: cameraDetailStore.cameraId })
+    .then((val) => {
+      if (val) {
+        ElMessage.success('预置位创建成功');
+        presetStore.currentPresetToken = val;
+        emits('ok');
+      } else {
+        ElMessage.error('创建失败:已有预置位数量可能超限,请删除后再添加');
+      }
+    })
+    .finally(() => {
+      loading.value = false;
+    });
+};
+</script>
+<style scoped></style>

+ 91 - 0
src/views/cameras/algo-params-setting/components/AlgoCanSelect/AlgoCanSelect.vue

@@ -0,0 +1,91 @@
+<template>
+  <!-- 模板部分保持不变 -->
+  <div class="algo-select-container">
+    <el-card>
+      <template #header> <div class="card-title">选择相机关联的算法</div> </template>
+      <div class="algo-list">
+        <div
+          v-for="(item, index) in props.algoList"
+          :key="index"
+          class="algo-item"
+          :class="{ active: selectedIds.includes(item.id), disabled: true }"
+          @click="handleAlgoSelect(item)"
+        >
+          <span class="algo-name">{{ item.name }}</span>
+        </div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { useUserStore } from '@/store/modules/user';
+  import { PERM_ALGO } from '@/types/permission/constants';
+  import { ElCard } from 'element-plus';
+  import { computed, defineEmits } from 'vue';
+  const emit = defineEmits<{
+    (e: 'select', data: number): void;
+  }>();
+  interface AlgoItemLabel {
+    name: string;
+    id: number;
+  }
+
+  const props = defineProps<{ algoList: AlgoItemLabel[]; selectedIds: number[] }>();
+
+  const userStore = useUserStore();
+
+  // 处理方法
+  const handleAlgoSelect = (item: AlgoItemLabel) => {
+    if (!hasAddPermission.value) return;
+    const hasId = props.selectedIds.includes(item.id);
+    if (!hasId) {
+      emit('select', item.id);
+    }
+  };
+
+  const hasAddPermission = computed(() => false && userStore.checkPermission(PERM_ALGO.CONFIG_ADD));
+</script>
+
+<style lang="less" scoped>
+  /* 样式保持不变 */
+  .algo-select-container {
+    margin-left: 10px;
+    width: 250px;
+    .algo-list {
+      // display: flex;
+      // flex-wrap: wrap;
+      // gap: 12px;
+      max-height: 442px;
+      overflow-y: auto;
+    }
+
+    .algo-item {
+      padding: 4px 16px;
+      // border: 1px solid #d9d9d9;
+      margin: 10px 0;
+      border-radius: 4px;
+      cursor: pointer;
+      transition: all 0.3s;
+
+      &:hover {
+        border-color: #1890ff;
+        color: #1890ff;
+      }
+
+      &.active {
+        background: #1890ff;
+        color: #fff;
+        border-color: #1890ff;
+        cursor: not-allowed;
+      }
+      &.disabled {
+        cursor: not-allowed;
+        opacity: 0.5;
+      }
+    }
+  }
+  .card-title {
+    font-weight: bold;
+  }
+</style>

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

@@ -1,29 +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>
+<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 - 5
src/views/cameras/preview/components/AlgoSwitchCard/AlgoDeleteIcon.vue

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

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

@@ -1,11 +1,9 @@
-<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>
+<template>
+  <div>
+    <svg-icon icon-name="algo-setting" color="#1890ff" />
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ElTooltip } from 'element-plus';
+</script>
+<style scoped></style>

+ 140 - 149
src/views/cameras/preview/components/AlgoSwitchCard/AlgoSwitchCard.vue

@@ -1,149 +1,140 @@
-<template>
-  <div class="algoSwitchCardWrapper">
-    <AlgoSwitchCardBase class="algoWrapper" :class="{ active: isAlgoOpen, selected: isSelected }">
-      <div>
-        <!-- <div>开关 </div> -->
-        <AlgoSwitchIcon :active="isAlgoOpen" @click.stop="toggleAlgoStatus" />
-        <el-tooltip :content="label">
-          <div class="algoName">{{ label }}</div>
-        </el-tooltip>
-        <div class="toolbar">
-          <AlgoSettingIcon
-            class="divideLine algoSettingIcon"
-            v-if="hasAlgoSettingMaxPermisson()"
-            @click.stop="emits('toggleSetting')"
-          />
-          <!-- <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';
-  import { PERM_ALGO } from '@/types/permission/constants';
-
-  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;
-    (e: 'toggleSetting'): unknown;
-  }>();
-  const toggleAlgoStatus = () => {
-    emits('toggleAlgo', !props.isAlgoOpen);
-  };
-
-  const updateFenceStatus = (fenceStatus: boolean) => {
-    emits('toggleFence', fenceStatus);
-  };
-
-  const hasDeletePermission = () => userStore.checkPermission(PERM_ALGO.CONFIG_DELETE);
-
-  const hasAlgoSettingMaxPermisson = () => userStore.checkPermission(PERM_ALGO.CONFIG_PARAM);
-</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;
-    white-space: nowrap;
-    overflow: hidden;
-  }
-  .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>
+<template>
+  <div class="algoSwitchCardWrapper">
+    <AlgoSwitchCardBase class="algoWrapper" :class="{ active: isAlgoOpen, selected: isSelected }">
+      <div>
+        <!-- <div>开关 </div> -->
+        <AlgoSwitchIcon :active="isAlgoOpen" @click.stop="toggleAlgoStatus" />
+        <el-tooltip :content="label" v-if="label.length > 12">
+          <div class="algoName">{{ label }}</div>
+        </el-tooltip>
+        <div class="algoName" v-else>{{ label }}</div>
+        <div class="toolbar">
+          <div>
+            <ElectronicFenceIcon :active="isFenceOpen" @click.stop="toggleFenceToolStatus" />
+          </div>
+          <AlgoSettingIcon
+            class="divideLine algoSettingIcon"
+            v-if="hasAlgoSettingMaxPermisson()"
+            @click.stop="emits('toggleSetting')"
+          />
+          <!-- <AlgoSettingIcon class="divideLine algoSettingIcon" /> -->
+
+          <div class="divideLine deleteIcon" @click.stop="emits('remove')" v-if="hasDeletePermission()">
+            <img :src="deletePng" alt="删除" style="width: 16px; height: 16px" />
+          </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 { useUserStore } from '@/store/modules/user';
+  import { PERM_ALGO } from '@/types/permission/constants';
+  import ElectronicFenceIcon from './ElectronicFenceIcon.vue';
+
+  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: 'toggleFenceTool'): unknown;
+    (e: 'remove'): unknown;
+    (e: 'toggleSetting'): unknown;
+  }>();
+  const toggleAlgoStatus = () => {
+    emits('toggleAlgo', !props.isAlgoOpen);
+  };
+
+  /** 切换工具栏的显示隐藏状态 */
+  const toggleFenceToolStatus = () => {
+    emits('toggleFenceTool');
+  };
+
+  const hasDeletePermission = () => userStore.checkPermission(PERM_ALGO.CONFIG_DELETE);
+
+  const hasAlgoSettingMaxPermisson = () => userStore.checkPermission(PERM_ALGO.CONFIG_PARAM);
+</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;
+    white-space: nowrap;
+    overflow: hidden;
+  }
+  .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: 31px;
+    flex-shrink: 0;
+    cursor: pointer;
+    &::before {
+      left: -19px;
+    }
+  }
+  .algoSettingIcon {
+    margin-right: 0;
+    margin-left: 40px;
+    &::before {
+      left: -20px;
+    }
+  }
+</style>

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

@@ -1,19 +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>
+<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>

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

@@ -1,29 +1,25 @@
-<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>
+<template>
+  <span class="algo-switch-wrapper">
+    <el-tooltip :content="tip" placement="top">
+      <ElSwitch v-model="props.active" size="small" />
+    </el-tooltip>
+  </span>
+</template>
+<script lang="ts" setup>
+  import { computed } from 'vue';
+  import { ElTooltip, ElSwitch } from 'element-plus';
+
+  const props = defineProps<{ active: boolean }>();
+
+  const tip = computed(() => {
+    return props.active ? '关闭算法' : '开启算法';
+  });
+</script>
+<style scoped>
+  .algo-switch {
+    cursor: pointer;
+  }
+  .algo-switch-wrapper {
+    display: inline-block;
+  }
+</style>

+ 21 - 0
src/views/cameras/algo-params-setting/components/AlgoSwitchCard/ElectronicFenceIcon.vue

@@ -0,0 +1,21 @@
+<!-- 电子围栏开关按钮 -->
+<template>
+  <div>
+    <!-- <el-tooltip :content="props.active ? '关闭电子围栏' : '开启电子围栏'">
+      <svg-icon icon-name="electronic-fence" :color="color" />
+    </el-tooltip> -->
+    <svg-icon icon-name="electronic-fence" :color="color" />
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ElTooltip } from 'element-plus';
+  import { computed } from 'vue';
+  const props = defineProps<{
+    active: boolean;
+  }>();
+
+  const color = computed(() => {
+    return props.active ? '#409EFF' : '#CDCDCD';
+  });
+</script>
+<style scoped></style>

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

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

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

@@ -1,94 +1,94 @@
-<template>
-  <!-- <ElButton type="primary" @click="showDialog" size="small" style="margin-top: 10px">
-    + 添加算法</ElButton
-  > -->
-
-  <ElDialog title="添加算法" @close="handleClose" width="500px" :model-value="true">
-    <div style="display: flex; justify-content: center">
-      <span>算法:</span>
-      <ElSelect
-        v-model="selectedIds"
-        multiple
-        style="width: 224px"
-        size="small"
-        @visible-change="handleVisibleChange"
-        placeholder="请为该相机选择关联的算法"
-      >
-        <ElOption
-          v-for="item in curOptionsByCode"
-          :key="item.id"
-          :value="item.id"
-          :label="item.name"
-          :disabled="!!isAlgoBind(item.id)"
-        >
-          {{ item.name }}
-          <span style="margin-left: 5px" v-if="isAlgoBind(item.id)">√</span>
-        </ElOption>
-      </ElSelect>
-    </div>
-
-    <template #footer>
-      <el-button @click="handleClose">取消</el-button>
-      <el-button type="primary" @click="handleSubmit"> 确定 </el-button>
-    </template>
-  </ElDialog>
-</template>
-<script lang="ts" setup>
-  import { ElDialog, ElSelect, ElOption, ElButton, ElMessage } from 'element-plus';
-  import { ref, onMounted } from 'vue';
-  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
-  import { createCameraAlgoApi } from '@/api/camera/camera-preview';
-  import useCameraDetailStore from '../../store/useCameraDetailStore';
-  import { AlgoDetail, queryAlgoInfoAllByCameraId } from '@/api/algo/algo';
-  import { storeToRefs } from 'pinia';
-  const selectedIds = ref<number[]>([]);
-  const cameraAlgoStore = useCameraAlgoStore();
-
-  const { isAlgoBind } = cameraAlgoStore;
-  const { getCameraAlgoList } = cameraAlgoStore;
-  const cameraDetailStore = useCameraDetailStore();
-  const { cameraId } = storeToRefs(cameraDetailStore);
-
-  const emits = defineEmits(['close']);
-  const algoListVisiable = ref(false);
-  const handleClose = () => {
-    emits('close');
-  };
-
-  const curOptionsByCode = ref<AlgoDetail[]>([]);
-
-  onMounted(() => {
-    queryAlgoInfoAllByCameraId(cameraId.value).then((res) => {
-      curOptionsByCode.value = res;
-    });
-  });
-
-  const handleVisibleChange = (visible: boolean) => {
-    const t = setTimeout(() => {
-      algoListVisiable.value = visible;
-      clearTimeout(t);
-    }, 100);
-  };
-
-  const handleSubmit = () => {
-    if (selectedIds.value?.length < 1) {
-      ElMessage.warning({ message: '请选择算法' });
-      return;
-    }
-    createCameraAlgoApi({
-      algoIds: selectedIds.value || [],
-      cameraId: cameraDetailStore.cameraId,
-    }).then((res) => {
-      console.log('createAlgo ok', res);
-      // selectedAlgoId.value = selectedIds.value?.[0];
-      getCameraAlgoList(cameraDetailStore.cameraId);
-      ElMessage.success('添加成功,请完成算法参数配置后生效');
-      emits('close');
-    });
-  };
-</script>
-<style scoped>
-  :deep(.el-select .el-input__inner) {
-    min-height: 32px;
-  }
-</style>
+<template>
+  <!-- <ElButton type="primary" @click="showDialog" size="small" style="margin-top: 10px">
+    + 添加算法</ElButton
+  > -->
+
+  <ElDialog title="添加算法" @close="handleClose" width="500px" :model-value="true">
+    <div style="display: flex; justify-content: center">
+      <span>算法:</span>
+      <ElSelect
+        v-model="selectedIds"
+        multiple
+        style="width: 224px"
+        size="small"
+        @visible-change="handleVisibleChange"
+        placeholder="请为该相机选择关联的算法"
+      >
+        <ElOption
+          v-for="item in curOptionsByCode"
+          :key="item.id"
+          :value="item.id"
+          :label="item.name"
+          :disabled="!!isAlgoBind(item.id)"
+        >
+          {{ item.name }}
+          <span style="margin-left: 5px" v-if="isAlgoBind(item.id)">√</span>
+        </ElOption>
+      </ElSelect>
+    </div>
+
+    <template #footer>
+      <el-button @click="handleClose">取消</el-button>
+      <el-button type="primary" @click="handleSubmit"> 确定 </el-button>
+    </template>
+  </ElDialog>
+</template>
+<script lang="ts" setup>
+  import { ElDialog, ElSelect, ElOption, ElButton, ElMessage } from 'element-plus';
+  import { ref, onMounted } from 'vue';
+  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
+  import { createCameraAlgoApi } from '@/api/camera/camera-preview';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+  import { AlgoDetail, queryAlgoInfoAllByCameraId } from '@/api/algo/algo';
+  import { storeToRefs } from 'pinia';
+  const selectedIds = ref<number[]>([]);
+  const cameraAlgoStore = useCameraAlgoStore();
+
+  const { isAlgoBind } = cameraAlgoStore;
+  const { getCameraAlgoList } = cameraAlgoStore;
+  const cameraDetailStore = useCameraDetailStore();
+  const { cameraId } = storeToRefs(cameraDetailStore);
+
+  const emits = defineEmits(['close']);
+  const algoListVisiable = ref(false);
+  const handleClose = () => {
+    emits('close');
+  };
+
+  const curOptionsByCode = ref<AlgoDetail[]>([]);
+
+  onMounted(() => {
+    queryAlgoInfoAllByCameraId(cameraId.value).then((res) => {
+      curOptionsByCode.value = res;
+    });
+  });
+
+  const handleVisibleChange = (visible: boolean) => {
+    const t = setTimeout(() => {
+      algoListVisiable.value = visible;
+      clearTimeout(t);
+    }, 100);
+  };
+
+  const handleSubmit = () => {
+    if (selectedIds.value?.length < 1) {
+      ElMessage.warning({ message: '请选择算法' });
+      return;
+    }
+    createCameraAlgoApi({
+      algoIds: selectedIds.value || [],
+      cameraId: cameraDetailStore.cameraId,
+    }).then((res) => {
+      console.log('createAlgo ok', res);
+      // selectedAlgoId.value = selectedIds.value?.[0];
+      getCameraAlgoList(cameraDetailStore.cameraId);
+      ElMessage.success('添加成功,请完成算法参数配置后生效');
+      emits('close');
+    });
+  };
+</script>
+<style scoped>
+  :deep(.el-select .el-input__inner) {
+    min-height: 32px;
+  }
+</style>

+ 65 - 65
src/views/cameras/preview/components/AlgorithmsSetting/AlgoTag.vue

@@ -1,65 +1,65 @@
-<template>
-  <div
-    class="tagWrapper"
-    @mouseenter="isHover = true"
-    @mouseleave="isHover = false"
-    :style="{ opacity: isHover ? 0.7 : 1 }"
-  >
-    <ElTag
-      hit
-      :type="props.isActive ? '' : 'info'"
-      :class="{ isOpen: props.isOpen, isClose: !props.isOpen }"
-      @click="handleHit"
-    >
-      {{ props.label }}
-    </ElTag>
-    <el-icon v-show="isHover" color="#8f8f8f" style="margin: 4px" @click="handleRemoveAlgo">
-      <CircleCloseFilled />
-    </el-icon>
-  </div>
-</template>
-<script lang="ts" setup>
-  import { CircleCloseFilled } from '@element-plus/icons-vue';
-  import { ref } from 'vue';
-
-  const props = defineProps<{
-    isActive: boolean;
-    label: string;
-    isOpen: boolean;
-    algoId: number;
-  }>();
-  const emits = defineEmits<{
-    (e: 'onRemove', algoId: number): Promise<unknown>;
-    (e: 'onHit', algoId: number): Promise<unknown>;
-  }>();
-
-  const isHover = ref(false);
-
-  const handleRemoveAlgo = () => {
-    emits('onRemove', props.algoId);
-  };
-
-  const handleHit = () => {
-    emits('onHit', props.algoId);
-  };
-</script>
-<style scoped>
-  .tagWrapper {
-    margin: 10px 0;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-  }
-
-  .el-tag--info.isOpen {
-    background-color: #fafafa;
-    color: #409eff;
-    border-color: #909399;
-  }
-
-  .el-tag--info.isClose {
-    background-color: #fafafa;
-    color: #909399;
-    border-color: #909399;
-  }
-</style>
+<template>
+  <div
+    class="tagWrapper"
+    @mouseenter="isHover = true"
+    @mouseleave="isHover = false"
+    :style="{ opacity: isHover ? 0.7 : 1 }"
+  >
+    <ElTag
+      hit
+      :type="props.isActive ? '' : 'info'"
+      :class="{ isOpen: props.isOpen, isClose: !props.isOpen }"
+      @click="handleHit"
+    >
+      {{ props.label }}
+    </ElTag>
+    <el-icon v-show="isHover" color="#8f8f8f" style="margin: 4px" @click="handleRemoveAlgo">
+      <CircleCloseFilled />
+    </el-icon>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { CircleCloseFilled } from '@element-plus/icons-vue';
+  import { ref } from 'vue';
+
+  const props = defineProps<{
+    isActive: boolean;
+    label: string;
+    isOpen: boolean;
+    algoId: number;
+  }>();
+  const emits = defineEmits<{
+    (e: 'onRemove', algoId: number): Promise<unknown>;
+    (e: 'onHit', algoId: number): Promise<unknown>;
+  }>();
+
+  const isHover = ref(false);
+
+  const handleRemoveAlgo = () => {
+    emits('onRemove', props.algoId);
+  };
+
+  const handleHit = () => {
+    emits('onHit', props.algoId);
+  };
+</script>
+<style scoped>
+  .tagWrapper {
+    margin: 10px 0;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+  }
+
+  .el-tag--info.isOpen {
+    background-color: #fafafa;
+    color: #409eff;
+    border-color: #909399;
+  }
+
+  .el-tag--info.isClose {
+    background-color: #fafafa;
+    color: #909399;
+    border-color: #909399;
+  }
+</style>

+ 229 - 0
src/views/cameras/algo-params-setting/components/AlgorithmsSetting/AlgorithmsSetting.vue

@@ -0,0 +1,229 @@
+<template>
+  <div id="algoSetting">
+    <div>
+      <div class="algoTagWrapper">
+        <!-- <AddAlgoDialog v-if="algoDialogVisible" @close="closeDialog" /> -->
+        <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.capture="handleSelectAlgo(item.algoId, $event)"
+          @remove="confirmRemove(item.algoId, item)"
+          @toggle-algo="confirmToggleAlgoOpen(item, $event)"
+          :is-fence-open="item.electronicFence === FENCE_ENBALED_STATUS.enabled"
+          @toggle-fence-tool="toggleFenceTool(item, $event)"
+          @toggle-setting="handleToggleSetting(item.algoId)"
+        />
+      </div>
+      <div>
+        <AlgoParamsSetting
+          @on-submit="handleSubmit"
+          @on-cancel="handleCancel"
+          @change="handleSettingChange"
+          :is-changed="isChanged"
+          :algo-detail="selectedAlgoDetail"
+          v-if="selectedAlgoId && algoSettingIsOpen"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
+  import AlgoParamsSetting from '@/modules/algo/algo-params-edit/index.vue';
+  import { storeToRefs } from 'pinia';
+  import {
+    deleteCameraAlgoApi,
+    updateCameraAlgoApi,
+    FENCE_ENBALED_STATUS,
+    CameraAlgoItem,
+    updateCameraAlgoStatusApi,
+    updateCameraAlgoRelStatus,
+  } from '@/api/camera/camera-preview';
+  import { ElMessage, ElMessageBox } from 'element-plus';
+  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 { createAlgoSubmitParams, algoDetailToJSON } from '@/modules/algo/algo-params-edit/utils';
+  import {
+    getInferCode,
+    getAlgoType,
+    getMetaValues,
+    getExtraCommonInfo,
+    getDetectionTime,
+    getInferParam,
+  } from '@/modules/algo/algo-params-edit/utils';
+
+  import { ALGO_ENABLED_STATUS } from '@/api/camera/camera-preview';
+  import { ref, watchEffect } from 'vue';
+
+  const cameraAlgoStore = useCameraAlgoStore();
+  const fenceStore = useFenceStore();
+  const presetStore = usePresetListStore();
+
+  const { getCameraAlgoList, getAlgoDetail } = cameraAlgoStore;
+  const { cameraAlgoList, selectedAlgoId, selectedAlgoDetail } = storeToRefs(cameraAlgoStore);
+  const cameraDetailStore = useCameraDetailStore();
+
+  // const hasAddPermission = () => userStore.checkPermission(PERM_ALGO.CONFIG_ADD);
+  const algoSettingIsOpen = ref(false);
+
+  // 是否修改过参数配置
+  const isChanged = ref(false);
+
+  const handleSettingChange = () => {
+    isChanged.value = true;
+  };
+
+  const handleToggleSetting = (algoId: number) => {
+    // 如果是在当前选中的卡片上切换设置开关,那么反选即可
+    if (selectedAlgoId.value === algoId) {
+      algoSettingIsOpen.value = !algoSettingIsOpen.value;
+      return;
+    }
+  };
+
+  const handleSelectAlgo = (algoId: number, e) => {
+    // 如果点击的是它自己,那么取消选中当前的算法
+    if (selectedAlgoId.value === algoId) {
+      return;
+    }
+    // 如果点击的是其他卡片,那么先切换到这个卡片,不执行按钮的默认操作
+    e.stopPropagation();
+    const switchToNewAlgo = (_algoId: number) => {
+      selectedAlgoId.value = _algoId;
+      algoSettingIsOpen.value = false;
+      isChanged.value = false;
+      fenceStore.showFenceTool = false;
+    };
+
+    // 如果算法设置有修改,切换到其他卡片时,要先确认提示再切换
+    if (isChanged.value) {
+      confirmSwitchAlgo()
+        .then(() => {
+          switchToNewAlgo(algoId);
+        })
+        .catch(() => {});
+    } else {
+      switchToNewAlgo(algoId);
+    }
+  };
+
+  watchEffect(() => {
+    const algoId = selectedAlgoId.value;
+    if (!algoId) return;
+    const detail = getAlgoDetail(algoId);
+    if (!detail) return;
+    selectedAlgoDetail.value = algoDetailToJSON(detail);
+    fenceStore.getFence({
+      algoId: algoId,
+      cameraId: cameraDetailStore.cameraId,
+      presetToken: presetStore.currentPresetToken,
+    });
+  });
+
+  const toggleFenceTool = () => {
+    const nextShowFenceTool = !fenceStore.showFenceTool;
+    fenceStore.showFenceTool = nextShowFenceTool;
+  };
+
+  const confirmToggleAlgoOpen = (detail: CameraAlgoItem, algoStatus: boolean) => {
+    if (detail.algoId !== selectedAlgoId.value && algoSettingIsOpen.value) {
+      confirmSwitchAlgo().then(() => {
+        handleToggleAlgoOpen(detail, algoStatus);
+      });
+    } else {
+      handleToggleAlgoOpen(detail, algoStatus);
+    }
+  };
+
+  const handleToggleAlgoOpen = (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;
+    selectedAlgoId.value = algoId;
+    isChanged.value = false;
+    algoSettingIsOpen.value = false;
+    updateCameraAlgoRelStatus(params)
+      .then(() => {
+        ElMessage.success(algoStatus ? '算法已开启' : '算法已关闭');
+      })
+      .catch(() => {
+        detail.status = initialStatus;
+      });
+  };
+
+  const handleSubmit = (param) => {
+    const cameraId = cameraDetailStore.cameraId;
+
+    const newParam = { cameraId: cameraId, ...createAlgoSubmitParams(param, selectedAlgoDetail.value) };
+    if (param.id) {
+      updateCameraAlgoApi({ ...newParam, id: param.id }).then(() => {
+        ElMessage.success('更新成功');
+        getCameraAlgoList(cameraId);
+        isChanged.value = false;
+      });
+    }
+  };
+
+  const confirmRemove = (algoId: number, item) => {
+    ElMessageBox.confirm(`请确定是否删除【${item.algoInfo.name}】算法?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }).then(() => {
+      handleRemove(algoId);
+    });
+  };
+
+  const confirmSwitchAlgo = () => {
+    return ElMessageBox.confirm('<strong>确认切换算法吗?</strong><br />切换后未保存的算法配置将被丢弃。', '', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      dangerouslyUseHTMLString: true,
+    });
+  };
+
+  const handleRemove = (algoId: number) => {
+    deleteCameraAlgoApi({ algoId, cameraId: cameraDetailStore.cameraId }).then(() => {
+      ElMessage.success('删除成功');
+      getCameraAlgoList(cameraDetailStore.cameraId);
+      selectedAlgoId.value = undefined;
+      isChanged.value = false;
+    });
+  };
+
+  const handleCancel = () => {
+    isChanged.value = false;
+    selectedAlgoId.value = undefined;
+  };
+</script>
+<style scoped>
+  .algoTagWrapper {
+    min-width: 150px;
+    margin-right: 15px;
+    display: flex;
+    flex-wrap: wrap;
+  }
+
+  :deep(.el-message-box__status.el-icon) {
+    top: 0 !important;
+    transform: none !important;
+  }
+</style>

+ 61 - 61
src/views/cameras/preview/components/CameraDirectionControl/CameraDirectionControl.vue

@@ -1,61 +1,61 @@
-<template>
-  <div class="cameraDirectionControlWrapper">
-    <div class="cameraDirectionControl">
-      <div class="roundCircle"></div>
-      <DirectionItem position="top" @click="handleMoveTop" />
-      <DirectionItem position="right" @click="handleMoveRight" />
-      <DirectionItem position="bottom" @click="handleMoveBottom" />
-      <DirectionItem position="left" @click="handleMoveLeft" />
-    </div>
-  </div>
-</template>
-<script lang="ts" setup>
-  import { cameraMoveApi } from '@/api/camera/camera-preview';
-  import DirectionItem from './DirectionItem.vue';
-  import useCameraDetailStore from '../../store/useCameraDetailStore';
-  import { storeToRefs } from 'pinia';
-  import usePresetListStore from '../../store/usePresetListStore';
-
-  const cameraDetailStore = useCameraDetailStore();
-  const { cameraId } = storeToRefs(cameraDetailStore);
-  const presetListStore = usePresetListStore();
-
-  const STEP = 0.05;
-
-  const handleMoveTop = () => {
-    cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: 0, y: STEP });
-    presetListStore.currentPresetToken = '';
-  };
-  const handleMoveRight = () => {
-    cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: STEP, y: 0 });
-    presetListStore.currentPresetToken = '';
-  };
-  const handleMoveBottom = () => {
-    cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: 0, y: -STEP });
-    presetListStore.currentPresetToken = '';
-  };
-  const handleMoveLeft = () => {
-    cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: -STEP, y: 0 });
-    presetListStore.currentPresetToken = '';
-  };
-</script>
-<style scoped lang="scss">
-  .cameraDirectionControlWrapper {
-    bottom: 100px;
-    right: 0px;
-  }
-  .cameraDirectionControl {
-    width: 180px;
-    height: 180px;
-    overflow: hidden;
-
-    position: relative;
-    .roundCircle {
-      background: #fff;
-      opacity: 0.4;
-      width: 180px;
-      height: 180px;
-      border-radius: 180px;
-    }
-  }
-</style>
+<template>
+  <div class="cameraDirectionControlWrapper">
+    <div class="cameraDirectionControl">
+      <div class="roundCircle"></div>
+      <DirectionItem position="top" @click="handleMoveTop" />
+      <DirectionItem position="right" @click="handleMoveRight" />
+      <DirectionItem position="bottom" @click="handleMoveBottom" />
+      <DirectionItem position="left" @click="handleMoveLeft" />
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { cameraMoveApi } from '@/api/camera/camera-preview';
+  import DirectionItem from './DirectionItem.vue';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+  import { storeToRefs } from 'pinia';
+  import usePresetListStore from '../../store/usePresetListStore';
+
+  const cameraDetailStore = useCameraDetailStore();
+  const { cameraId } = storeToRefs(cameraDetailStore);
+  const presetListStore = usePresetListStore();
+
+  const STEP = 0.05;
+
+  const handleMoveTop = () => {
+    cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: 0, y: STEP });
+    presetListStore.currentPresetToken = '';
+  };
+  const handleMoveRight = () => {
+    cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: STEP, y: 0 });
+    presetListStore.currentPresetToken = '';
+  };
+  const handleMoveBottom = () => {
+    cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: 0, y: -STEP });
+    presetListStore.currentPresetToken = '';
+  };
+  const handleMoveLeft = () => {
+    cameraMoveApi({ cameraId: cameraId.value, zoom: 0, x: -STEP, y: 0 });
+    presetListStore.currentPresetToken = '';
+  };
+</script>
+<style scoped lang="scss">
+  .cameraDirectionControlWrapper {
+    bottom: 100px;
+    right: 0px;
+  }
+  .cameraDirectionControl {
+    width: 180px;
+    height: 180px;
+    overflow: hidden;
+
+    position: relative;
+    .roundCircle {
+      background: #fff;
+      opacity: 0.4;
+      width: 180px;
+      height: 180px;
+      border-radius: 180px;
+    }
+  }
+</style>

+ 70 - 70
src/views/cameras/preview/components/CameraDirectionControl/DirectionItem.vue

@@ -1,70 +1,70 @@
-<template>
-  <div class="sectorWrapper" :class="[props.position, { active: isActive }]" @click="handleClick">
-    <!-- 四分之一圆 -->
-    <div class="sector"></div>
-    <!-- 小三角形 -->
-    <div class="triangle"></div>
-  </div>
-</template>
-<script lang="ts" setup>
-  import { ref } from 'vue';
-  const props = defineProps<{ position: 'top' | 'right' | 'bottom' | 'left' }>();
-  const isActive = ref(false);
-  const handleClick = () => {
-    isActive.value = true;
-    setTimeout(() => {
-      isActive.value = false;
-    }, 200);
-  };
-</script>
-<style scoped lang="scss">
-  .sectorWrapper {
-    width: 90px;
-    height: 90px;
-    transform-origin: bottom right;
-    position: absolute;
-    cursor: pointer;
-    left: 0;
-    top: 0;
-  }
-  .sectorWrapper.top {
-    transform: rotate(45deg);
-  }
-
-  .sectorWrapper.right {
-    transform: rotate(135deg);
-  }
-
-  .sectorWrapper.bottom {
-    transform: rotate(225deg);
-  }
-  .sectorWrapper.left {
-    transform: rotate(315deg);
-  }
-  .sectorWrapper.active {
-    .sector {
-      background-color: #1890ff;
-      opacity: 0.4;
-    }
-    .triangle {
-      border-bottom-color: #1677ff;
-    }
-  }
-  .sector {
-    width: 90px;
-    height: 90px;
-    border-radius: 90px 0 0 0;
-  }
-
-  .triangle {
-    width: 0;
-    height: 0;
-    border: 7px solid transparent;
-    border-bottom-color: #d8d8d8; /* 三角形的颜色 */
-    position: absolute;
-
-    top: 26px;
-    left: 26px;
-    transform: rotate(-43deg);
-  }
-</style>
+<template>
+  <div class="sectorWrapper" :class="[props.position, { active: isActive }]" @click="handleClick">
+    <!-- 四分之一圆 -->
+    <div class="sector"></div>
+    <!-- 小三角形 -->
+    <div class="triangle"></div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  const props = defineProps<{ position: 'top' | 'right' | 'bottom' | 'left' }>();
+  const isActive = ref(false);
+  const handleClick = () => {
+    isActive.value = true;
+    setTimeout(() => {
+      isActive.value = false;
+    }, 200);
+  };
+</script>
+<style scoped lang="scss">
+  .sectorWrapper {
+    width: 90px;
+    height: 90px;
+    transform-origin: bottom right;
+    position: absolute;
+    cursor: pointer;
+    left: 0;
+    top: 0;
+  }
+  .sectorWrapper.top {
+    transform: rotate(45deg);
+  }
+
+  .sectorWrapper.right {
+    transform: rotate(135deg);
+  }
+
+  .sectorWrapper.bottom {
+    transform: rotate(225deg);
+  }
+  .sectorWrapper.left {
+    transform: rotate(315deg);
+  }
+  .sectorWrapper.active {
+    .sector {
+      background-color: #1890ff;
+      opacity: 0.4;
+    }
+    .triangle {
+      border-bottom-color: #1677ff;
+    }
+  }
+  .sector {
+    width: 90px;
+    height: 90px;
+    border-radius: 90px 0 0 0;
+  }
+
+  .triangle {
+    width: 0;
+    height: 0;
+    border: 7px solid transparent;
+    border-bottom-color: #d8d8d8; /* 三角形的颜色 */
+    position: absolute;
+
+    top: 26px;
+    left: 26px;
+    transform: rotate(-43deg);
+  }
+</style>

+ 36 - 36
src/views/cameras/preview/components/CameraLiveVideo/CameraLiveVideo.vue

@@ -1,36 +1,36 @@
-<template>
-  <LiveVideo :url="videoUrl" v-if="videoUrl" />
-  <div v-if="!videoUrl" class="noPushStreamIpTip">暂无相机视频流地址</div>
-</template>
-<script lang="ts" setup>
-  // import bg from '@/assets/images/camera/video-live.png';
-  import LiveVideo from '@/components/LiveVideo/LiveVideo.vue';
-  import { storeToRefs } from 'pinia';
-  import { computed } from 'vue';
-  import useCameraDetailStore from '../../store/useCameraDetailStore';
-  const cameraDetailStore = useCameraDetailStore();
-  const skyeyeVideoPath = localStorage.getItem('skyeyeVideoPath');
-  const { detail } = storeToRefs(cameraDetailStore);
-
-  const videoUrl = computed(() => {
-    if (detail.value?.render) {
-      if (skyeyeVideoPath === 'abs') {
-        return detail.value?.pushStreamDTO?.videoUrls?.pushstreamRenderUrlAbs;
-        // return detail.value?.pushstreamRenderUrlAbs;
-      }
-      return detail.value?.pushStreamDTO?.videoUrls?.pushstreamRenderUrl;
-    } else {
-      if (skyeyeVideoPath === 'abs') {
-        return detail?.value?.pushStreamDTO?.videoUrls?.pushstreamIpAbs;
-      }
-      return detail?.value?.pushStreamDTO?.videoUrls?.pushstreamIp;
-    }
-  });
-</script>
-<style>
-  .noPushStreamIpTip {
-    font-size: 20px;
-    margin-top: 300px;
-    text-align: center;
-  }
-</style>
+<template>
+  <LiveVideo :url="videoUrl" v-if="videoUrl" />
+  <div v-if="!videoUrl" class="noPushStreamIpTip">暂无相机视频流地址</div>
+</template>
+<script lang="ts" setup>
+  // import bg from '@/assets/images/camera/video-live.png';
+  import LiveVideo from '@/components/LiveVideo/LiveVideo.vue';
+  import { storeToRefs } from 'pinia';
+  import { computed } from 'vue';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+  const cameraDetailStore = useCameraDetailStore();
+  const skyeyeVideoPath = localStorage.getItem('skyeyeVideoPath');
+  const { detail } = storeToRefs(cameraDetailStore);
+
+  const videoUrl = computed(() => {
+    if (detail.value?.render) {
+      if (skyeyeVideoPath === 'abs') {
+        return detail.value?.pushStreamDTO?.videoUrls?.pushstreamRenderUrlAbs;
+        // return detail.value?.pushstreamRenderUrlAbs;
+      }
+      return detail.value?.pushStreamDTO?.videoUrls?.pushstreamRenderUrl;
+    } else {
+      if (skyeyeVideoPath === 'abs') {
+        return detail?.value?.pushStreamDTO?.videoUrls?.pushstreamIpAbs;
+      }
+      return detail?.value?.pushStreamDTO?.videoUrls?.pushstreamIp;
+    }
+  });
+</script>
+<style>
+  .noPushStreamIpTip {
+    font-size: 20px;
+    margin-top: 300px;
+    text-align: center;
+  }
+</style>

+ 81 - 81
src/views/cameras/preview/components/CameraParams/CameraParams.vue

@@ -1,81 +1,81 @@
-<template>
-  <div class="cameraParamTitle">相机参数设置</div>
-  <el-form :model="cameraDetailStore" label-width="130px" lable-position="left">
-    <el-form-item label="分辨率:">
-      <el-select v-model="cameraDetailStore.params.resolution" style="width: 100%" size="small">
-        <el-option
-          v-for="x in videoResolutionList"
-          :label="x.label"
-          :value="x.value"
-          :key="x.value"
-        />
-      </el-select>
-    </el-form-item>
-    <el-form-item label="录制周期:">
-      <el-select v-model="cameraDetailStore.params.period" style="width: 100%" size="small">
-        <el-option v-for="x in periodOptions" :key="x.value" :label="x.label" :value="x.value" />
-      </el-select>
-    </el-form-item>
-    <el-form-item label="录制时间:">
-      <el-col :span="11">
-        <el-time-picker
-          v-model="cameraDetailStore.params.startAt"
-          style="width: 100%"
-          size="small"
-        />
-      </el-col>
-      <el-col :span="1">
-        <span class="text-center">-</span>
-      </el-col>
-      <el-col :span="11">
-        <el-time-picker v-model="cameraDetailStore.params.endAt" style="width: 100%" size="small" />
-      </el-col>
-    </el-form-item>
-    <!-- <el-form-item label="返回预置位:">
-      <el-input v-model="cameraDetailStore.params.reservation" size="small" />
-    </el-form-item> -->
-    <el-form-item>
-      <el-button type="primary" @click="onSubmit">保存</el-button>
-    </el-form-item>
-  </el-form>
-</template>
-
-<script lang="ts" setup>
-  import { saveCameraParamsApi } from '@/api/camera/camera-preview';
-  import { formatToDateTime } from '@/utils/dateUtil';
-  import { ElMessage } from 'element-plus';
-  import useCameraDetailStore from '../../store/useCameraDetailStore';
-  import { videoResolutionList, periodOptions } from './types';
-
-  const cameraDetailStore = useCameraDetailStore();
-
-  const onSubmit = () => {
-    const params = cameraDetailStore.params;
-    const DATE_TIME_STR = 'HH:mm:ss';
-    const endAt = formatToDateTime(params.endAt, DATE_TIME_STR);
-    const startAt = formatToDateTime(params.startAt, DATE_TIME_STR);
-    saveCameraParamsApi({
-      ...params,
-      startAt,
-      endAt,
-      cameraId: cameraDetailStore.cameraId,
-    }).then(() => {
-      ElMessage.success('保存成功');
-      if (!cameraDetailStore.detail) return;
-      cameraDetailStore.detail.nvrPeriod = params.period;
-      cameraDetailStore.detail.resolution = params.resolution;
-      cameraDetailStore.detail.nvrStartAt = params.startAt;
-      cameraDetailStore.detail.nvrEndAt = params.endAt;
-    });
-  };
-</script>
-
-<style scoped>
-  .text-center {
-    /* text-align: center; */
-    margin-left: 2px;
-  }
-  .cameraParamTitle {
-    font-weight: bold;
-  }
-</style>
+<template>
+  <div class="cameraParamTitle">相机参数设置</div>
+  <el-form :model="cameraDetailStore" label-width="130px" lable-position="left">
+    <el-form-item label="分辨率:">
+      <el-select v-model="cameraDetailStore.params.resolution" style="width: 100%" size="small">
+        <el-option
+          v-for="x in videoResolutionList"
+          :label="x.label"
+          :value="x.value"
+          :key="x.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="录制周期:">
+      <el-select v-model="cameraDetailStore.params.period" style="width: 100%" size="small">
+        <el-option v-for="x in periodOptions" :key="x.value" :label="x.label" :value="x.value" />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="录制时间:">
+      <el-col :span="11">
+        <el-time-picker
+          v-model="cameraDetailStore.params.startAt"
+          style="width: 100%"
+          size="small"
+        />
+      </el-col>
+      <el-col :span="1">
+        <span class="text-center">-</span>
+      </el-col>
+      <el-col :span="11">
+        <el-time-picker v-model="cameraDetailStore.params.endAt" style="width: 100%" size="small" />
+      </el-col>
+    </el-form-item>
+    <!-- <el-form-item label="返回预置位:">
+      <el-input v-model="cameraDetailStore.params.reservation" size="small" />
+    </el-form-item> -->
+    <el-form-item>
+      <el-button type="primary" @click="onSubmit">保存</el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script lang="ts" setup>
+  import { saveCameraParamsApi } from '@/api/camera/camera-preview';
+  import { formatToDateTime } from '@/utils/dateUtil';
+  import { ElMessage } from 'element-plus';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+  import { videoResolutionList, periodOptions } from './types';
+
+  const cameraDetailStore = useCameraDetailStore();
+
+  const onSubmit = () => {
+    const params = cameraDetailStore.params;
+    const DATE_TIME_STR = 'HH:mm:ss';
+    const endAt = formatToDateTime(params.endAt, DATE_TIME_STR);
+    const startAt = formatToDateTime(params.startAt, DATE_TIME_STR);
+    saveCameraParamsApi({
+      ...params,
+      startAt,
+      endAt,
+      cameraId: cameraDetailStore.cameraId,
+    }).then(() => {
+      ElMessage.success('保存成功');
+      if (!cameraDetailStore.detail) return;
+      cameraDetailStore.detail.nvrPeriod = params.period;
+      cameraDetailStore.detail.resolution = params.resolution;
+      cameraDetailStore.detail.nvrStartAt = params.startAt;
+      cameraDetailStore.detail.nvrEndAt = params.endAt;
+    });
+  };
+</script>
+
+<style scoped>
+  .text-center {
+    /* text-align: center; */
+    margin-left: 2px;
+  }
+  .cameraParamTitle {
+    font-weight: bold;
+  }
+</style>

+ 16 - 16
src/views/cameras/preview/components/CameraParams/types.ts

@@ -1,16 +1,16 @@
-/** 分辨率的枚举值 */
-export enum VideoResolution {
-  'HIGH_RESOLUTION' = 1080,
-  'MEDIUM_RESOLUTION' = 720,
-  'LOW_RESOLUTION' = 360,
-}
-
-export const videoResolutionList = [
-  { label: '高分辨率(1080P)', value: VideoResolution.HIGH_RESOLUTION },
-  { label: '中分辨率(720P)', value: VideoResolution.MEDIUM_RESOLUTION },
-  { label: '低分辨率(360P)', value: VideoResolution.LOW_RESOLUTION },
-];
-
-export const periodOptions = new Array(31).fill('').map((x, index) => {
-  return { label: `${index + 1}天`, value: index + 1 };
-});
+/** 分辨率的枚举值 */
+export enum VideoResolution {
+  'HIGH_RESOLUTION' = 1080,
+  'MEDIUM_RESOLUTION' = 720,
+  'LOW_RESOLUTION' = 360,
+}
+
+export const videoResolutionList = [
+  { label: '高分辨率(1080P)', value: VideoResolution.HIGH_RESOLUTION },
+  { label: '中分辨率(720P)', value: VideoResolution.MEDIUM_RESOLUTION },
+  { label: '低分辨率(360P)', value: VideoResolution.LOW_RESOLUTION },
+];
+
+export const periodOptions = new Array(31).fill('').map((x, index) => {
+  return { label: `${index + 1}天`, value: index + 1 };
+});

+ 347 - 347
src/views/cameras/preview/components/CameraTree/CameraTree.vue

@@ -1,347 +1,347 @@
-<template>
-  <div class="cameraTreeWrapper">
-    <div class="cameraTreeTitle">
-      <span>场景树</span>
-      <span class="detail-num" v-if="totalNum">
-        (总相机:{{ totalNum }} 未联网:{{ noNetworkingNum }} 未进入平台:{{ noIntegrationNum }})
-      </span>
-    </div>
-    <div class="cameraTreeInputWrapper">
-      <el-input
-        class="filterTextInput"
-        v-model="queryForm.queryString"
-        placeholder="请输入相机名称/设备ID/算法名称"
-        @keyup.enter.native="handleSearchCamera"
-        clearable
-        @clear="handleSearchCamera"
-      >
-        <template #suffix>
-          <el-icon class="el-input__icon" @click="handleSearchCamera"><search /></el-icon>
-        </template>
-      </el-input>
-      <div class="cameraTreeCheckboxWrapper">
-        <el-checkbox
-          v-model="queryForm.isEnableAlgo"
-          label="添加算法"
-          @change="handleSearchCamera"
-        />
-        <el-checkbox
-          v-model="queryForm.isEnableRender"
-          label="开启渲染"
-          @change="handleSearchCamera"
-        />
-        <el-button v-if="isSearch" text type="primary" @click="handleCollapseTree">收起</el-button>
-        <el-button v-if="!isSearch" text type="primary" @click="handleExpandTree">展开</el-button>
-      </div>
-      <el-scrollbar class="tree-scroll">
-        <el-tree
-          v-if="treeCollapse"
-          ref="treeRef"
-          node-key="tempCode"
-          :data="cameraTreeTemp"
-          :props="defaultProps"
-          :default-expand-all="isSearch"
-          :default-expanded-keys="workSpaceKeys"
-          @node-click="handleNodeClick"
-        >
-          <template #default="{ node, data }">
-            <span
-              class="flexCenter"
-              :class="{ integrationState: data.integrationState === 1, nodeSelect: isSelect(data) }"
-            >
-              <span
-                v-if="data.nodeType === CameraTreeNodeType.camera"
-                class="iconWrapper flexCenter"
-              >
-                <span
-                  class="cameraCommon"
-                  :class="{
-                    cameraSelect: isSelect(data),
-                  }"
-                ></span>
-
-                <el-icon
-                  class="cameraIcon"
-                  :class="{
-                    iconSelect: isSelect(data),
-                  }"
-                >
-                  <VideoCamera />
-                </el-icon>
-                <el-icon class="invalidCamera" v-if="isInvalid(data)"><WarningFilled /></el-icon>
-              </span>
-              {{ node.label }}
-            </span>
-          </template>
-        </el-tree>
-      </el-scrollbar>
-    </div>
-  </div>
-</template>
-<script lang="ts" setup>
-  import { uid } from 'uid';
-  import { nextTick, onMounted, onUnmounted, ref } from 'vue';
-  import { ElMessage, ElTree } from 'element-plus';
-  import { Search, VideoCamera, WarningFilled } from '@element-plus/icons-vue';
-  import { useRouteQuery } from '@vueuse/router';
-  import useCameraStatus from '../../store/useCameraStatus';
-  import {
-    CameraTree,
-    CameraTreeNodeType,
-    CameraQueryForm,
-    getCameraTree,
-  } from '@/api/camera/camera-preview';
-
-  interface CameraTreeTempType extends CameraTree {
-    tempCode?: string;
-  }
-
-  const cameraId = useRouteQuery('cameraId');
-  const cameraStatus = useCameraStatus();
-  const { noNetworkingNum, openInterval, closeInterval } = cameraStatus;
-
-  const queryForm = ref<CameraQueryForm>({
-    isEnableAlgo: false,
-    isEnableRender: false,
-    queryString: '',
-  });
-  const totalNum = ref(0); // 总相机数
-  const noIntegrationNum = ref(0); // 未进入平台相机数
-  const cameraTree = ref<CameraTree[]>([]); // 保存从接口获取的所有树节点信息
-  const cameraTreeTemp = ref<CameraTreeTempType[]>([]); // 保存修改name之后的树
-  const codeShowList = ref<string[]>([]); // 保存当前所有相机code列表
-  const isSearch = ref(true); // 默认展开全部
-  const treeCollapse = ref(true);
-  const workSpaceKeys = ref<string[]>([]); // 当前要收起的工位code(tempCode)
-
-  const treeRef = ref<InstanceType<typeof ElTree>>();
-  const defaultProps = {
-    children: 'children',
-    label: 'name',
-  };
-
-  const handleNodeClick = (e: CameraTree) => {
-    if (e.integrationState === 1) {
-      ElMessage.error('该相机未进入平台');
-    } else {
-      if (e.nodeType === CameraTreeNodeType.camera) {
-        cameraId.value = String(e.id);
-      }
-    }
-  };
-
-  const isSelect = (data) =>
-    data.nodeType === CameraTreeNodeType.camera && data.id === Number(cameraId.value);
-
-  const isInvalid = (data) => {
-    return data.networkingState !== 0;
-  };
-
-  // 把树节点中所有 nodeType = camera 的 name 替换成 name + code
-  function getCameraNameCode(data) {
-    const cameraNameCode = data;
-    for (let i = 0; i < data.length; i++) {
-      const node = cameraNameCode[i];
-      node.tempCode = uid(); // 为相机树节点创建唯一code
-      if (node.nodeType === 'camera') {
-        node.name = node.name + ` [${node.code}] `;
-      }
-      if (node.children && node.children.length > 0) {
-        getCameraNameCode(node.children);
-      }
-    }
-    return cameraNameCode;
-  }
-
-  // 获取当前树结构下总相机code列表
-  function getCameraList(data) {
-    let cameraList = [] as string[];
-    for (let i = 0; i < data.length; i++) {
-      const node = data[i];
-      if (node.nodeType === 'camera') {
-        cameraList.push(node.code);
-      }
-      if (node.children && node.children.length > 0) {
-        const childCameraList = getCameraList(node.children);
-        cameraList.push(...childCameraList);
-      }
-    }
-    return cameraList;
-  }
-
-  // 获取当前树结构下nodeType=workshop的节点code集合(tempCode)
-  function getWorkShopIdList(data) {
-    let tempIdList = [] as string[];
-    for (let i = 0; i < data.length; i++) {
-      const node = data[i];
-      if (node.nodeType === 'workshop') tempIdList.push(node.tempCode);
-      if (node.children && node.children.length > 0) {
-        const childList = getWorkShopIdList(node.children);
-        tempIdList.push(...childList);
-      }
-    }
-    return tempIdList;
-  }
-
-  // 更新/获取未进入平台相机数量
-  function updateNetworkingState(data, targetData) {
-    let integrationCount = 0;
-    for (let i = 0; i < data.length; i++) {
-      const node = data[i];
-      const matchedNode = targetData.find((item) => item.cameraCode === node.code);
-      if (matchedNode) {
-        node.networkingState = matchedNode.networkingState;
-        node.integrationState = matchedNode.integrationState;
-      }
-      if (node.integrationState === 1) {
-        integrationCount++;
-      }
-      if (node.children && node.children.length > 0) {
-        const childIntegrationCount = updateNetworkingState(node.children, targetData);
-        integrationCount += childIntegrationCount;
-      }
-    }
-    noIntegrationNum.value = integrationCount;
-    return integrationCount;
-  }
-
-  // 输入框回车搜索 + checkbox 搜索
-  const handleSearchCamera = async () => {
-    treeCollapse.value = false;
-    workSpaceKeys.value = [];
-    await getCameraData(queryForm.value);
-    nextTick(() => {
-      isSearch.value = true;
-      treeCollapse.value = true;
-    });
-  };
-
-  // 收起相机,收起到工位
-  const handleCollapseTree = () => {
-    treeCollapse.value = false;
-    workSpaceKeys.value = getWorkShopIdList(cameraTreeTemp.value);
-    nextTick(() => {
-      isSearch.value = false;
-      treeCollapse.value = true;
-    });
-  };
-
-  // 展开相机
-  const handleExpandTree = () => {
-    treeCollapse.value = false;
-    workSpaceKeys.value = [];
-    nextTick(() => {
-      isSearch.value = true;
-      treeCollapse.value = true;
-    });
-  };
-
-  const getCameraData = async (tempQuery) => {
-    await getCameraTree(tempQuery).then((res) => {
-      cameraTree.value = res;
-      cameraTreeTemp.value = getCameraNameCode(res);
-      codeShowList.value = getCameraList(res);
-      totalNum.value = codeShowList.value.length;
-      openInterval(codeShowList.value, (targetData) => {
-        updateNetworkingState(cameraTree.value, targetData);
-      });
-    });
-  };
-
-  onMounted(() => {
-    getCameraData(queryForm.value);
-  });
-
-  onUnmounted(() => {
-    closeInterval();
-  });
-</script>
-<style scoped>
-  .cameraCommon {
-    width: 6px;
-    height: 6px;
-    display: inline-block;
-    align-items: center;
-    margin-right: 6px;
-  }
-
-  .tree-scroll {
-    height: calc(100vh - 64px - 170px);
-  }
-
-  .cameraSelect {
-    width: 6px;
-    height: 6px;
-    background: #0052d9;
-    display: inline-block;
-    border-radius: 6px;
-    margin-right: 6px;
-  }
-  .cameraTreeTitle {
-    background: #f0f2f5;
-    padding: 12px;
-    display: flex;
-  }
-
-  .detail-num {
-    font-size: 10px;
-    margin-top: 4px;
-    margin-left: 6px;
-  }
-
-  .cameraTreeInputWrapper {
-    padding: 8px;
-  }
-  .filterTextInput {
-    margin: 8px 0;
-  }
-  .flexCenter {
-    display: flex;
-    align-items: center;
-  }
-
-  .integrationState {
-    cursor: not-allowed;
-    color: #ccc;
-  }
-  .cameraIcon {
-    margin-right: 5px;
-    font-size: 18px;
-  }
-  .iconSelect {
-    color: #0052d9;
-  }
-
-  .iconWrapper {
-    position: relative;
-  }
-
-  .invalidCamera {
-    color: #dd5869;
-    font-size: 12px;
-    position: absolute;
-    right: 2px;
-    top: -4px;
-  }
-
-  .nodeSelect {
-    color: #0052d9;
-  }
-
-  .el-input__icon {
-    cursor: pointer;
-  }
-
-  .el-input__icon:hover {
-    color: #0052d9;
-  }
-
-  .cameraTreeCheckboxWrapper {
-    display: flex;
-    justify-content: space-between;
-
-    .el-checkbox {
-      margin-right: 0;
-    }
-  }
-</style>
+<template>
+  <div class="cameraTreeWrapper">
+    <div class="cameraTreeTitle">
+      <span>场景树</span>
+      <span class="detail-num" v-if="totalNum">
+        (总相机:{{ totalNum }} 未联网:{{ noNetworkingNum }} 未进入平台:{{ noIntegrationNum }})
+      </span>
+    </div>
+    <div class="cameraTreeInputWrapper">
+      <el-input
+        class="filterTextInput"
+        v-model="queryForm.queryString"
+        placeholder="请输入相机名称/设备ID/算法名称"
+        @keyup.enter.native="handleSearchCamera"
+        clearable
+        @clear="handleSearchCamera"
+      >
+        <template #suffix>
+          <el-icon class="el-input__icon" @click="handleSearchCamera"><search /></el-icon>
+        </template>
+      </el-input>
+      <div class="cameraTreeCheckboxWrapper">
+        <el-checkbox
+          v-model="queryForm.isEnableAlgo"
+          label="添加算法"
+          @change="handleSearchCamera"
+        />
+        <el-checkbox
+          v-model="queryForm.isEnableRender"
+          label="开启渲染"
+          @change="handleSearchCamera"
+        />
+        <el-button v-if="isSearch" text type="primary" @click="handleCollapseTree">收起</el-button>
+        <el-button v-if="!isSearch" text type="primary" @click="handleExpandTree">展开</el-button>
+      </div>
+      <el-scrollbar class="tree-scroll">
+        <el-tree
+          v-if="treeCollapse"
+          ref="treeRef"
+          node-key="tempCode"
+          :data="cameraTreeTemp"
+          :props="defaultProps"
+          :default-expand-all="isSearch"
+          :default-expanded-keys="workSpaceKeys"
+          @node-click="handleNodeClick"
+        >
+          <template #default="{ node, data }">
+            <span
+              class="flexCenter"
+              :class="{ integrationState: data.integrationState === 1, nodeSelect: isSelect(data) }"
+            >
+              <span
+                v-if="data.nodeType === CameraTreeNodeType.camera"
+                class="iconWrapper flexCenter"
+              >
+                <span
+                  class="cameraCommon"
+                  :class="{
+                    cameraSelect: isSelect(data),
+                  }"
+                ></span>
+
+                <el-icon
+                  class="cameraIcon"
+                  :class="{
+                    iconSelect: isSelect(data),
+                  }"
+                >
+                  <VideoCamera />
+                </el-icon>
+                <el-icon class="invalidCamera" v-if="isInvalid(data)"><WarningFilled /></el-icon>
+              </span>
+              {{ node.label }}
+            </span>
+          </template>
+        </el-tree>
+      </el-scrollbar>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { uid } from 'uid';
+  import { nextTick, onMounted, onUnmounted, ref } from 'vue';
+  import { ElMessage, ElTree } from 'element-plus';
+  import { Search, VideoCamera, WarningFilled } from '@element-plus/icons-vue';
+  import { useRouteQuery } from '@vueuse/router';
+  import useCameraStatus from '../../store/useCameraStatus';
+  import {
+    CameraTree,
+    CameraTreeNodeType,
+    CameraQueryForm,
+    getCameraTree,
+  } from '@/api/camera/camera-preview';
+
+  interface CameraTreeTempType extends CameraTree {
+    tempCode?: string;
+  }
+
+  const cameraId = useRouteQuery('cameraId');
+  const cameraStatus = useCameraStatus();
+  const { noNetworkingNum, openInterval, closeInterval } = cameraStatus;
+
+  const queryForm = ref<CameraQueryForm>({
+    isEnableAlgo: false,
+    isEnableRender: false,
+    queryString: '',
+  });
+  const totalNum = ref(0); // 总相机数
+  const noIntegrationNum = ref(0); // 未进入平台相机数
+  const cameraTree = ref<CameraTree[]>([]); // 保存从接口获取的所有树节点信息
+  const cameraTreeTemp = ref<CameraTreeTempType[]>([]); // 保存修改name之后的树
+  const codeShowList = ref<string[]>([]); // 保存当前所有相机code列表
+  const isSearch = ref(true); // 默认展开全部
+  const treeCollapse = ref(true);
+  const workSpaceKeys = ref<string[]>([]); // 当前要收起的工位code(tempCode)
+
+  const treeRef = ref<InstanceType<typeof ElTree>>();
+  const defaultProps = {
+    children: 'children',
+    label: 'name',
+  };
+
+  const handleNodeClick = (e: CameraTree) => {
+    if (e.integrationState === 1) {
+      ElMessage.error('该相机未进入平台');
+    } else {
+      if (e.nodeType === CameraTreeNodeType.camera) {
+        cameraId.value = String(e.id);
+      }
+    }
+  };
+
+  const isSelect = (data) =>
+    data.nodeType === CameraTreeNodeType.camera && data.id === Number(cameraId.value);
+
+  const isInvalid = (data) => {
+    return data.networkingState !== 0;
+  };
+
+  // 把树节点中所有 nodeType = camera 的 name 替换成 name + code
+  function getCameraNameCode(data) {
+    const cameraNameCode = data;
+    for (let i = 0; i < data.length; i++) {
+      const node = cameraNameCode[i];
+      node.tempCode = uid(); // 为相机树节点创建唯一code
+      if (node.nodeType === 'camera') {
+        node.name = node.name + ` [${node.code}] `;
+      }
+      if (node.children && node.children.length > 0) {
+        getCameraNameCode(node.children);
+      }
+    }
+    return cameraNameCode;
+  }
+
+  // 获取当前树结构下总相机code列表
+  function getCameraList(data) {
+    let cameraList = [] as string[];
+    for (let i = 0; i < data.length; i++) {
+      const node = data[i];
+      if (node.nodeType === 'camera') {
+        cameraList.push(node.code);
+      }
+      if (node.children && node.children.length > 0) {
+        const childCameraList = getCameraList(node.children);
+        cameraList.push(...childCameraList);
+      }
+    }
+    return cameraList;
+  }
+
+  // 获取当前树结构下nodeType=workshop的节点code集合(tempCode)
+  function getWorkShopIdList(data) {
+    let tempIdList = [] as string[];
+    for (let i = 0; i < data.length; i++) {
+      const node = data[i];
+      if (node.nodeType === 'workshop') tempIdList.push(node.tempCode);
+      if (node.children && node.children.length > 0) {
+        const childList = getWorkShopIdList(node.children);
+        tempIdList.push(...childList);
+      }
+    }
+    return tempIdList;
+  }
+
+  // 更新/获取未进入平台相机数量
+  function updateNetworkingState(data, targetData) {
+    let integrationCount = 0;
+    for (let i = 0; i < data.length; i++) {
+      const node = data[i];
+      const matchedNode = targetData.find((item) => item.cameraCode === node.code);
+      if (matchedNode) {
+        node.networkingState = matchedNode.networkingState;
+        node.integrationState = matchedNode.integrationState;
+      }
+      if (node.integrationState === 1) {
+        integrationCount++;
+      }
+      if (node.children && node.children.length > 0) {
+        const childIntegrationCount = updateNetworkingState(node.children, targetData);
+        integrationCount += childIntegrationCount;
+      }
+    }
+    noIntegrationNum.value = integrationCount;
+    return integrationCount;
+  }
+
+  // 输入框回车搜索 + checkbox 搜索
+  const handleSearchCamera = async () => {
+    treeCollapse.value = false;
+    workSpaceKeys.value = [];
+    await getCameraData(queryForm.value);
+    nextTick(() => {
+      isSearch.value = true;
+      treeCollapse.value = true;
+    });
+  };
+
+  // 收起相机,收起到工位
+  const handleCollapseTree = () => {
+    treeCollapse.value = false;
+    workSpaceKeys.value = getWorkShopIdList(cameraTreeTemp.value);
+    nextTick(() => {
+      isSearch.value = false;
+      treeCollapse.value = true;
+    });
+  };
+
+  // 展开相机
+  const handleExpandTree = () => {
+    treeCollapse.value = false;
+    workSpaceKeys.value = [];
+    nextTick(() => {
+      isSearch.value = true;
+      treeCollapse.value = true;
+    });
+  };
+
+  const getCameraData = async (tempQuery) => {
+    await getCameraTree(tempQuery).then((res) => {
+      cameraTree.value = res;
+      cameraTreeTemp.value = getCameraNameCode(res);
+      codeShowList.value = getCameraList(res);
+      totalNum.value = codeShowList.value.length;
+      openInterval(codeShowList.value, (targetData) => {
+        updateNetworkingState(cameraTree.value, targetData);
+      });
+    });
+  };
+
+  onMounted(() => {
+    getCameraData(queryForm.value);
+  });
+
+  onUnmounted(() => {
+    closeInterval();
+  });
+</script>
+<style scoped>
+  .cameraCommon {
+    width: 6px;
+    height: 6px;
+    display: inline-block;
+    align-items: center;
+    margin-right: 6px;
+  }
+
+  .tree-scroll {
+    height: calc(100vh - 64px - 170px);
+  }
+
+  .cameraSelect {
+    width: 6px;
+    height: 6px;
+    background: #0052d9;
+    display: inline-block;
+    border-radius: 6px;
+    margin-right: 6px;
+  }
+  .cameraTreeTitle {
+    background: #f0f2f5;
+    padding: 12px;
+    display: flex;
+  }
+
+  .detail-num {
+    font-size: 10px;
+    margin-top: 4px;
+    margin-left: 6px;
+  }
+
+  .cameraTreeInputWrapper {
+    padding: 8px;
+  }
+  .filterTextInput {
+    margin: 8px 0;
+  }
+  .flexCenter {
+    display: flex;
+    align-items: center;
+  }
+
+  .integrationState {
+    cursor: not-allowed;
+    color: #ccc;
+  }
+  .cameraIcon {
+    margin-right: 5px;
+    font-size: 18px;
+  }
+  .iconSelect {
+    color: #0052d9;
+  }
+
+  .iconWrapper {
+    position: relative;
+  }
+
+  .invalidCamera {
+    color: #dd5869;
+    font-size: 12px;
+    position: absolute;
+    right: 2px;
+    top: -4px;
+  }
+
+  .nodeSelect {
+    color: #0052d9;
+  }
+
+  .el-input__icon {
+    cursor: pointer;
+  }
+
+  .el-input__icon:hover {
+    color: #0052d9;
+  }
+
+  .cameraTreeCheckboxWrapper {
+    display: flex;
+    justify-content: space-between;
+
+    .el-checkbox {
+      margin-right: 0;
+    }
+  }
+</style>

src/views/cameras/preview/components/CameraTree/CameraTreeOldVersion.vue → src/views/cameras/algo-params-setting/components/CameraTree/CameraTreeOldVersion.vue


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

@@ -1,51 +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>
+<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>

+ 315 - 300
src/views/cameras/preview/components/CameraViewSetting/CameraViewSetting.vue

@@ -1,300 +1,315 @@
-<template>
-  <div style="position: relative">
-    <div class="toolbarWrapper">
-      <!-- <ViewWindowSetting v-model="viewType" @update:model-value="handleUpdateViewType" /> -->
-      <!-- <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> -->
-      <RenderSwitch />
-      <FenceAppSetting />
-    </div>
-    <div
-      class="cameraViewSettingWrapper"
-      :style="{ width: domWidth + 'px', height: domHeight + 'px' }"
-    >
-      <div class="fenceEditorWrapper" v-if="cameraAlgoStore.selectedAlgoDetail.electronicFenceBool">
-        <FenceEditor
-          ref="fenceEditorRef"
-          :dom-width="domWidth"
-          :canvas-size="{ width: canvasWidth, height: canvasHeight }"
-          :line-points="fenceStore.serverFencePoints || []"
-        />
-      </div>
-
-      <div class="cameraVideo">
-        <CameraLiveVideo />
-      </div>
-    </div>
-    <div
-      class="presetAddWrapper"
-      :class="{ hidePresetControlCls: isEdit }"
-      v-if="!!cameraDetailStore.detail?.isPtz"
-    >
-      <CameraViewScale />
-      <CameraDirectionControl />
-      <ElButton
-        type="primary"
-        @click="handleAddPreset"
-        size="small"
-        style="margin-top: 20px; width: 100px"
-        >添加预置位</ElButton
-      >
-      <AddPresetModal v-if="addPresetModalVisible" @close="handleClose" @ok="handleAddPresetOk" />
-    </div>
-  </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">
-    <!-- <div class="cameraParamsSetting">
-      <CameraParams />
-    </div> -->
-    <div class="algorithmsSetting"> <AlgorithmsSetting /> </div>
-  </div>
-</template>
-<script lang="ts" setup>
-  import { computed, ref, watchEffect } from 'vue';
-  import FenceToolbar from '../FenceToolbar/FenceToolbar.vue';
-  import FenceEditor from '../FenceEditorV2/FenceEditor.vue';
-  import CameraLiveVideo from '../CameraLiveVideo/CameraLiveVideo.vue';
-  import PresetSelect from '../PresetSelect/PresetSelect.vue';
-  // import { ViewType } from '../ViewWindowSetting/types';
-  import useFenceStore from '../../store/useFenceStore';
-  import AddPresetModal from '../AddPresetModal/AddPresetModal.vue';
-  import usePresetListStore from '../../store/usePresetListStore';
-  import useCameraDetailStore from '../../store/useCameraDetailStore';
-  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
-  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';
-  import { RegionJudge } from '../FenceToolbar/constants';
-
-  const emits = defineEmits<{
-    (e: 'changeTreeRender', render: number | string): unknown;
-  }>();
-
-  const [, { enterFullscreen }] = useFullscreen(() => document.querySelector('.cameraVideo'));
-
-  const fenceEditorRef = ref<typeof FenceEditor | null>(null);
-
-  const fenceStore = useFenceStore();
-
-  const presetStore = usePresetListStore();
-  const cameraDetailStore = useCameraDetailStore();
-  const cameraAlgoStore = useCameraAlgoStore();
-
-  const { getCameraAlgoList } = cameraAlgoStore;
-
-  // const viewType = ref<ViewType>(ViewType.window1);
-
-  const addPresetModalVisible = ref(false);
-
-  const handleClose = () => {
-    addPresetModalVisible.value = false;
-  };
-
-  const handleAddPresetOk = () => {
-    presetStore.getPresetList(cameraDetailStore.cameraId);
-    handleClose();
-  };
-
-  const handleRemove = () => {
-    fenceEditorRef.value?.remove();
-  };
-
-  const isEdit = ref(false);
-
-  /** 退出编辑模式 */
-  const toggleEditable = (val: boolean) => {
-    isEdit.value = val;
-    if (val) {
-      fenceEditorRef.value?.setEditMode();
-    } else {
-      fenceEditorRef.value?.exitEditMode();
-    }
-  };
-
-  const toggleRange = () => {
-    const selectedAlgoDetail = cameraAlgoStore.selectedAlgoDetail;
-    const cameraId = cameraDetailStore.cameraId;
-
-    const extraStr = selectedAlgoDetail.extra;
-    const extraJSON = JSON.parse(extraStr);
-    const nextRegionJudge =
-      extraJSON.inferParams?.[0]?.regionJudge === RegionJudge.out
-        ? RegionJudge.in
-        : RegionJudge.out;
-
-    extraJSON.inferParams[0].regionJudge = nextRegionJudge;
-
-    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);
-    const cameraId = cameraDetailStore.cameraId;
-    if (!cameraId) {
-      ElMessage.error('未选中相机');
-      return;
-    }
-    const algoId = cameraAlgoStore.selectedAlgoId;
-    if (!algoId) {
-      ElMessage.error('未选中算法');
-      return;
-    }
-    const presetToken = presetStore.currentPresetToken;
-    if (!presetToken) {
-      ElMessage.error('未选中预置位');
-      return;
-    }
-
-    fenceStore
-      .saveFence({
-        cameraId: cameraId,
-        algoId: algoId,
-        presetToken,
-        electronicFencePolygon: JSON.stringify(json),
-      })
-      ?.then(() => {
-        ElMessage.success('更新成功');
-      });
-  };
-
-  // const handleUpdateViewType = (t: ViewType) => {
-  //   console.log('viewType', t);
-  // };
-
-  const drawable = computed(() => {
-    if (!presetStore.currentPresetToken) return false;
-    if (!cameraAlgoStore.selectedAlgoId) return false;
-    if (!cameraAlgoStore.selectedAlgoDetail?.electronicFenceBool) return false;
-    return true;
-  });
-
-  watchEffect(() => {
-    const electronicFenceBool = cameraAlgoStore.selectedAlgoDetail?.electronicFenceBool;
-
-    if (presetStore.currentPresetToken && cameraAlgoStore.selectedAlgoId && electronicFenceBool) {
-      const points = fenceStore.serverFencePoints || [];
-      if (!points) {
-        fenceEditorRef.value?.clear();
-        return;
-      }
-
-      /** 先清空原有的 */
-      fenceEditorRef.value?.clear();
-      // fenceEditorRef.value?.createLines(rawLinePoints);
-      fenceEditorRef.value?.setEditMode();
-      isEdit.value = true;
-      return;
-    } else {
-      fenceEditorRef.value?.clear();
-      fenceEditorRef.value?.exitEditMode();
-      isEdit.value = false;
-    }
-  });
-
-  const handleAddPreset = () => {
-    addPresetModalVisible.value = true;
-  };
-</script>
-<style scoped>
-  .cameraViewSettingWrapper {
-    position: relative;
-    /* border: 1px solid #ccc; */
-  }
-  .cameraViewOverflow {
-    overflow: hidden;
-    position: relative;
-  }
-
-  .cameraVideo {
-    position: absolute;
-    top: 0;
-    left: 0;
-    z-index: 8;
-    background: #ccc;
-    width: 100%;
-    height: 100%;
-  }
-
-  .toolbarWrapper {
-    display: flex;
-    align-items: center;
-    /* margin-left: 25px; */
-    position: relative;
-  }
-
-  .presetAddWrapper {
-    position: absolute;
-    bottom: 50px;
-    right: 50px;
-    flex-direction: column;
-    display: flex;
-    align-items: center;
-    z-index: 10;
-  }
-
-  .cameraParamsSettingWrapper {
-    display: flex;
-    margin-top: 10px;
-  }
-  .algorithmsSetting {
-    flex: 1;
-    min-height: 300px;
-    margin-left: 15px;
-    /* border-left: 1px solid #ccc; */
-    /* padding-left: 15px; */
-  }
-  .cameraParamsSetting {
-    flex-basis: 330px;
-    flex-shrink: 0;
-  }
-  .hidePresetControlCls {
-    display: none;
-  }
-  .fenceEditorWrapper {
-    position: relative;
-    z-index: 9;
-  }
-
-  .presetWrapper {
-    width: 962px;
-    padding: 20px;
-    padding-left: 15px;
-    border-bottom: 1px solid #ccc;
-    padding-bottom: 10px;
-    margin-bottom: 15px;
-    display: flex;
-    justify-content: space-between;
-  }
-</style>
+<template>
+  <div style="position: relative">
+    <div class="toolbarWrapper">
+      <!-- <ViewWindowSetting v-model="viewType" @update:model-value="handleUpdateViewType" /> -->
+      <!-- <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> -->
+      <!-- <RenderSwitch />
+      <FenceAppSetting /> -->
+    </div>
+    <div class="videoAlgoListWrapper">
+      <div class="cameraViewSettingWrapper" :style="{ width: domWidth + 'px', height: domHeight + 'px' }">
+        <div class="fenceEditorWrapper" v-if="fenceStore.showFenceTool">
+          <FenceEditor
+            ref="fenceEditorRef"
+            :dom-width="domWidth"
+            :canvas-size="{ width: canvasWidth, height: canvasHeight }"
+            :line-points="fenceStore.allFences"
+            :fence-id="fenceStore.currentFenceId"
+            @save="handleSaveFence"
+            @select="handleSelectFencePolygon"
+          />
+        </div>
+
+        <div class="cameraVideo">
+          <CameraLiveVideo />
+        </div>
+      </div>
+      <div>
+        <AlgoCanSelect :algo-list="cameraAllAlgoList" :selected-ids="cameraAlgoIds" @select="handleApplyAlgo"
+      /></div>
+    </div>
+
+    <div
+      class="presetAddWrapper"
+      :class="{ hidePresetControlCls: showFenceTool }"
+      v-if="!!cameraDetailStore.detail?.isPtz"
+    >
+      <CameraViewScale />
+      <CameraDirectionControl />
+      <ElButton type="primary" @click="handleAddPreset" size="small" style="margin-top: 20px; width: 100px"
+        >添加预置位</ElButton
+      >
+      <AddPresetModal v-if="addPresetModalVisible" @close="handleClose" @ok="handleAddPresetOk" />
+    </div>
+  </div>
+  <div class="presetWrapper" :style="{ display: showFenceTool ? 'block' : 'none' }">
+    <FenceToolbar
+      :is-edit="showFenceTool"
+      @remove="handleRemove"
+      @toggle-editable="toggleEditable"
+      @toggle-range="toggleRange"
+      @select="handleSelectFenceList"
+      @toggle-fence-status="paramsSettingFn.toggleFenceStatus"
+    />
+  </div>
+
+  <div class="cameraParamsSettingWrapper">
+    <!-- <div class="cameraParamsSetting">
+      <CameraParams />
+    </div> -->
+    <div class="algorithmsSetting"> <AlgorithmsSetting /> </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { computed, onMounted, ref } from 'vue';
+  import FenceToolbar from '../FenceToolbar/FenceToolbar.vue';
+  import FenceEditor from '../FenceEditorV2/FenceEditor.vue';
+  import CameraLiveVideo from '../CameraLiveVideo/CameraLiveVideo.vue';
+  import useFenceStore from '../../store/useFenceStore';
+  import AddPresetModal from '../AddPresetModal/AddPresetModal.vue';
+  import usePresetListStore from '../../store/usePresetListStore';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
+  import AlgorithmsSetting from '../AlgorithmsSetting/AlgorithmsSetting.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 { createCameraAlgoApi, FENCE_ENBALED_STATUS, updateCameraAlgoApi } from '@/api/camera/camera-preview';
+  import { RegionJudge } from '../FenceToolbar/constants';
+  import AlgoCanSelect from '../AlgoCanSelect/AlgoCanSelect.vue';
+  import { AlgoDetail, queryAlgoInfoAllByCameraId } from '@/api/algo/algo';
+  import { FencePolygonPoints } from '../FenceEditorV2/types';
+  import useParamsSettingFn from '../../hooks/useParamsSettingFn';
+
+  const emits = defineEmits<{
+    (e: 'changeTreeRender', render: number | string): unknown;
+  }>();
+
+  const fenceEditorRef = ref<typeof FenceEditor | null>(null);
+
+  const paramsSettingFn = useParamsSettingFn();
+
+  const fenceStore = useFenceStore();
+
+  const presetStore = usePresetListStore();
+  const cameraDetailStore = useCameraDetailStore();
+  const cameraAlgoStore = useCameraAlgoStore();
+
+  const { getCameraAlgoList } = cameraAlgoStore;
+
+  // const viewType = ref<ViewType>(ViewType.window1);
+
+  const addPresetModalVisible = ref(false);
+
+  const cameraAllAlgoList = ref<AlgoDetail[]>([]);
+
+  const cameraAlgoIds = computed(() => {
+    return cameraAlgoStore.cameraAlgoList?.map((item) => item.algoId) || [];
+  });
+
+  onMounted(() => {
+    queryAlgoInfoAllByCameraId(cameraDetailStore.cameraId).then((res) => {
+      cameraAllAlgoList.value = res;
+    });
+  });
+  const handleClose = () => {
+    addPresetModalVisible.value = false;
+  };
+
+  const handleAddPresetOk = () => {
+    presetStore.getPresetList(cameraDetailStore.cameraId);
+    handleClose();
+  };
+
+  const handleRemove = () => {
+    fenceEditorRef.value?.remove();
+  };
+
+  /** 退出编辑模式 */
+  const toggleEditable = (val: boolean) => {
+    fenceStore.showFenceTool = val;
+    if (val) {
+      fenceEditorRef.value?.setEditMode();
+    } else {
+      fenceEditorRef.value?.exitEditMode();
+    }
+  };
+
+  /** 从图中选中电子围栏多边形 */
+  const handleSelectFencePolygon = (nextFenceId: number) => {
+    fenceStore.currentFenceId = nextFenceId;
+  };
+
+  // 选中电子围栏列表时
+  const handleSelectFenceList = (nextFenceId: number) => {
+    fenceStore.currentFenceId = nextFenceId;
+  };
+
+  const handleSaveFence = (data: { fenceId?: number; polygon: FencePolygonPoints }) => {
+    console.log('提交的fenceId', data);
+    const { fenceId, polygon } = data;
+
+    const cameraId = cameraDetailStore.cameraId;
+    const algoId = cameraAlgoStore.selectedAlgoId;
+    const presetToken = presetStore.currentPresetToken;
+    if (!cameraId) {
+      ElMessage.error('未选中相机');
+      return;
+    }
+    if (!algoId) {
+      ElMessage.error('未选中算法');
+      return;
+    }
+    if (!presetToken) {
+      ElMessage.error('未选中预置位');
+      return;
+    }
+    const param = { cameraId: cameraId, algoId: algoId, presetToken };
+
+    if (!fenceId) {
+      // 不存在的话,就新建电子围栏
+      fenceStore.createFence({ ...param, fencePolygon: JSON.stringify(polygon) });
+    } else {
+      // 否则修改电子围栏
+      fenceStore.editFence({ ...param, fencePolygon: JSON.stringify(polygon), fenceId });
+    }
+  };
+
+  const toggleRange = () => {
+    const selectedAlgoDetail = cameraAlgoStore.selectedAlgoDetail;
+    const cameraId = cameraDetailStore.cameraId;
+
+    const extraStr = selectedAlgoDetail.extra;
+    const extraJSON = JSON.parse(extraStr);
+    const nextRegionJudge =
+      extraJSON.inferParams?.[0]?.regionJudge === RegionJudge.out ? RegionJudge.in : RegionJudge.out;
+
+    extraJSON.inferParams[0].regionJudge = nextRegionJudge;
+
+    const newParam = {
+      cameraId: cameraId,
+      algoId: selectedAlgoDetail.algoId,
+      extra: JSON.stringify(extraJSON),
+      id: selectedAlgoDetail.id!,
+    };
+    updateCameraAlgoApi(newParam).then(() => {
+      ElMessage.success('更新成功');
+      getCameraAlgoList(cameraId);
+    });
+  };
+
+  // const handleUpdateViewType = (t: ViewType) => {
+  //   console.log('viewType', t);
+  // };
+
+  const showFenceTool = computed(() => {
+    if (!presetStore.currentPresetToken) return false;
+    if (!cameraAlgoStore.selectedAlgoId) return false;
+    if (!fenceStore.showFenceTool) return false;
+    return true;
+  });
+
+  const handleAddPreset = () => {
+    addPresetModalVisible.value = true;
+  };
+
+  const handleApplyAlgo = (id: number) => {
+    createCameraAlgoApi({
+      algoIds: [id],
+      cameraId: cameraDetailStore.cameraId,
+    }).then((res) => {
+      getCameraAlgoList(cameraDetailStore.cameraId);
+      ElMessage.success('添加成功,请完成算法参数配置后生效');
+    });
+  };
+</script>
+<style scoped>
+  .cameraViewSettingWrapper {
+    position: relative;
+    flex-shrink: 0;
+    flex-grow: 0;
+    /* border: 1px solid #ccc; */
+  }
+  .cameraViewOverflow {
+    overflow: hidden;
+    position: relative;
+  }
+
+  .cameraVideo {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 8;
+    background: #ccc;
+    width: 100%;
+    height: 100%;
+  }
+
+  .toolbarWrapper {
+    display: flex;
+    align-items: center;
+    /* margin-left: 25px; */
+    position: relative;
+  }
+
+  .presetAddWrapper {
+    position: absolute;
+    bottom: 50px;
+    right: 50px;
+    flex-direction: column;
+    display: flex;
+    align-items: center;
+    z-index: 10;
+  }
+
+  .cameraParamsSettingWrapper {
+    display: flex;
+    margin-top: 10px;
+  }
+  .algorithmsSetting {
+    flex: 1;
+    min-height: 300px;
+    /* margin-left: 15px; */
+    /* border-left: 1px solid #ccc; */
+    /* padding-left: 15px; */
+  }
+  .cameraParamsSetting {
+    flex-basis: 330px;
+    flex-shrink: 0;
+  }
+  .hidePresetControlCls {
+    display: none;
+  }
+  .fenceEditorWrapper {
+    position: relative;
+    z-index: 9;
+  }
+
+  .presetWrapper {
+    position: absolute;
+    right: 0;
+    top: 0;
+    width: 260px;
+    height: 540px;
+    box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1);
+    background: #fff;
+    /* width: 962px; */
+    /* padding: 20px;
+    padding-left: 15px;
+    border-bottom: 1px solid #ccc;
+    padding-bottom: 10px;
+    margin-bottom: 15px;
+    display: flex;
+    justify-content: space-between; */
+    transform: scale(1);
+  }
+  .videoAlgoListWrapper {
+    display: flex;
+  }
+</style>

+ 10 - 10
src/views/cameras/preview/components/CameraViewSetting/constants.ts

@@ -1,10 +1,10 @@
-/** canvas的实际宽度 */
-export const canvasWidth = 1920;
-export const canvasHeight = 1080;
-/** 视频的比例 */
-const videoRatio = canvasHeight / canvasWidth;
-
-/** dom展示出来的宽度,由于屏幕大小限制,完全按照1920来展示,一屏显示不下,所以要进行一定的缩放 */
-export const domWidth = 960;
-export const domHeight = domWidth * videoRatio;
-export const scale = domWidth / canvasWidth;
+/** canvas的实际宽度 */
+export const canvasWidth = 1920;
+export const canvasHeight = 1080;
+/** 视频的比例 */
+const videoRatio = canvasHeight / canvasWidth;
+
+/** dom展示出来的宽度,由于屏幕大小限制,完全按照1920来展示,一屏显示不下,所以要进行一定的缩放 */
+export const domWidth = 960;
+export const domHeight = domWidth * videoRatio;
+export const scale = domWidth / canvasWidth;

+ 104 - 104
src/views/cameras/preview/components/FenceAppSetting/FenceAppSetting.vue

@@ -1,104 +1,104 @@
-<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="previewUrl">平台相机预览</a>
-  </div>
-</template>
-<script lang="ts" setup>
-  import {
-    updateFenceDisplayStatus,
-    getCameraAlgoPresetList,
-    choosePreset,
-    getAppCameraAlgoPreset,
-  } from '@/api/camera/camera-preview';
-  import { computed, ref, watch } from 'vue';
-  import useCameraDetailStore from '../../store/useCameraDetailStore';
-  import { storeToRefs } from 'pinia';
-  import { OptionType } from './constants';
-  import { useGlobSetting } from '@/hooks/setting';
-  import { FenceDisplayStatus } from '@/types/camera/constant';
-  const valuePreset = ref<[string, string]>();
-  const cameraDetailStore = useCameraDetailStore();
-  const { isShowFence, detail } = storeToRefs(cameraDetailStore);
-
-  const options = ref([]);
-
-  const { appPCUrl } = useGlobSetting();
-
-  const previewUrl = computed(() => {
-    const firstSceneId = detail.value?.sceneTemplateList[0]?.sceneId;
-    if (!detail.value?.workshopId || !detail.value?.code || !firstSceneId) return '';
-    return appPCUrl + `#/shop?id=${detail.value?.workshopId}&cameraCode=${detail.value?.code!}&sceneId=${firstSceneId}`;
-  });
-
-  watch(
-    () => detail.value?.id,
-    (newId) => {
-      if (!newId) return;
-      getCameraAlgoPresetList(newId).then((res) => {
-        options.value = renameKeys(res.algoInfoVOList);
-      });
-
-      getAppCameraAlgoPreset(newId).then((res) => {
-        if (res) {
-          valuePreset.value = [res.algoId, res.presetToken];
-        } else {
-          valuePreset.value = undefined;
-        }
-      });
-    },
-  );
-
-  const renameKeys = (data: any) => {
-    return data.map((item) => {
-      const newItem: OptionType = {
-        label: item.name,
-        value: item.id,
-      };
-
-      if (item.presetInfoList) {
-        newItem.children = item.presetInfoList.map((child) => ({
-          label: child.presetName,
-          value: child.presetToken,
-        }));
-      }
-
-      return newItem;
-    });
-  };
-
-  const changeShowFence = async () => {
-    if (!isShowFence.value) {
-      valuePreset.value = undefined;
-    }
-    const params = {
-      cameraCode: cameraDetailStore.detail?.code!,
-      isDisplayFence: isShowFence.value ? FenceDisplayStatus.enabled : FenceDisplayStatus.disabled,
-    };
-
-    updateFenceDisplayStatus(params);
-  };
-
-  const changePreset = (value) => {
-    console.log('value', value);
-    const params = {
-      algoId: value[0],
-      cameraId: cameraDetailStore.detail?.id!,
-      presetToken: value[1],
-    };
-    choosePreset(params);
-  };
-</script>
-<style scoped>
-  .wrapper {
-    display: flex;
-    align-items: center;
-    margin-left: 20px;
-  }
-
-  .checkbox {
-    margin-right: 10px;
-  }
-</style>
+<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="previewUrl">平台相机预览</a>
+  </div>
+</template>
+<script lang="ts" setup>
+  import {
+    updateFenceDisplayStatus,
+    getCameraAlgoPresetList,
+    choosePreset,
+    getAppCameraAlgoPreset,
+  } from '@/api/camera/camera-preview';
+  import { computed, ref, watch } from 'vue';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+  import { storeToRefs } from 'pinia';
+  import { OptionType } from './constants';
+  import { useGlobSetting } from '@/hooks/setting';
+  import { FenceDisplayStatus } from '@/types/camera/constant';
+  const valuePreset = ref<[string, string]>();
+  const cameraDetailStore = useCameraDetailStore();
+  const { isShowFence, detail } = storeToRefs(cameraDetailStore);
+
+  const options = ref([]);
+
+  const { appPCUrl } = useGlobSetting();
+
+  const previewUrl = computed(() => {
+    const firstSceneId = detail.value?.sceneTemplateList[0]?.sceneId;
+    if (!detail.value?.workshopId || !detail.value?.code || !firstSceneId) return '';
+    return appPCUrl + `#/shop?id=${detail.value?.workshopId}&cameraCode=${detail.value?.code!}&sceneId=${firstSceneId}`;
+  });
+
+  watch(
+    () => detail.value?.id,
+    (newId) => {
+      if (!newId) return;
+      getCameraAlgoPresetList(newId).then((res) => {
+        options.value = renameKeys(res.algoInfoVOList);
+      });
+
+      getAppCameraAlgoPreset(newId).then((res) => {
+        if (res) {
+          valuePreset.value = [res.algoId, res.presetToken];
+        } else {
+          valuePreset.value = undefined;
+        }
+      });
+    },
+  );
+
+  const renameKeys = (data: any) => {
+    return data.map((item) => {
+      const newItem: OptionType = {
+        label: item.name,
+        value: item.id,
+      };
+
+      if (item.presetInfoList) {
+        newItem.children = item.presetInfoList.map((child) => ({
+          label: child.presetName,
+          value: child.presetToken,
+        }));
+      }
+
+      return newItem;
+    });
+  };
+
+  const changeShowFence = async () => {
+    if (!isShowFence.value) {
+      valuePreset.value = undefined;
+    }
+    const params = {
+      cameraCode: cameraDetailStore.detail?.code!,
+      isDisplayFence: isShowFence.value ? FenceDisplayStatus.enabled : FenceDisplayStatus.disabled,
+    };
+
+    updateFenceDisplayStatus(params);
+  };
+
+  const changePreset = (value) => {
+    console.log('value', value);
+    const params = {
+      algoId: value[0],
+      cameraId: cameraDetailStore.detail?.id!,
+      presetToken: value[1],
+    };
+    choosePreset(params);
+  };
+</script>
+<style scoped>
+  .wrapper {
+    display: flex;
+    align-items: center;
+    margin-left: 20px;
+  }
+
+  .checkbox {
+    margin-right: 10px;
+  }
+</style>

src/views/cameras/preview/components/FenceAppSetting/constants.ts → src/views/cameras/algo-params-setting/components/FenceAppSetting/constants.ts


Разлика између датотеке није приказан због своје велике величине
+ 692 - 692
src/views/cameras/preview/components/FenceEditor/FenceEditor.vue


+ 44 - 42
src/views/cameras/preview/components/FenceEditor/constants.ts

@@ -1,42 +1,44 @@
-export const toolObject: ToolObjectItem[] = [
-  {
-    name: 'rect',
-    type: 'rect',
-    /* 矩形颜色 */ color: '#75fb4c',
-    /* 顶点颜色 */ anchorColor: 'green',
-    activeColor: '#0f0',
-  },
-  {
-    name: 'poly',
-    type: 'poly',
-    /* 多边形颜色 */ color: '#52FFDA',
-    /* 顶点颜色 */ anchorColor: '#fff',
-    /** 选中模式状态下的边框颜色 */
-    activeColor: '#52FFDA',
-  },
-];
-
-export interface ToolObjectItem {
-  name: string;
-  type: string;
-  /* 矩形颜色 */
-  color: string;
-
-  /* 顶点颜色 */
-  anchorColor: string;
-
-  activeColor: string;
-}
-
-export const GROUP_NAME = '.polygroup';
-export const POLYGON_NAME = '.polypoly';
-
-export type Points = number[];
-
-/** 导出给后端的单个点坐标格式 */
-export type ServerLinePoint = [number, number];
-/** 一个多边形的所有点坐标 */
-export type ServerLine = ServerLinePoint[];
-
-/** 图上所有的多边形 */
-export type ServerLines = ServerLine[];
+export const toolObject: ToolObjectItem[] = [
+  {
+    name: 'rect',
+    type: 'rect',
+    /* 矩形颜色 */ color: '#75fb4c',
+    /* 顶点颜色 */ anchorColor: 'green',
+    activeColor: '#0f0',
+  },
+  {
+    name: 'poly',
+    type: 'poly',
+    /* 多边形颜色 */ color: '#52FFDA',
+    /* 顶点颜色 */ anchorColor: '#fff',
+    /** 选中模式状态下的边框颜色 */
+    activeColor: '#52FFDA',
+  },
+];
+
+export interface ToolObjectItem {
+  name: string;
+  type: string;
+  /* 矩形颜色 */
+  color: string;
+
+  /* 顶点颜色 */
+  anchorColor: string;
+
+  activeColor: string;
+}
+
+export const GROUP_NAME = '.polygroup';
+export const POLYGON_NAME = '.polypoly';
+
+export type Points = number[];
+
+/** 导出给后端的单个点坐标格式 */
+export type ServerLinePoint = [number, number];
+/** 一个多边形的所有点坐标 */
+export type ServerLine = ServerLinePoint[];
+
+export type ServerLineInfo =  { id: number; name: string; label: string; polygon: ServerLine }
+
+/** 图上所有的多边形 */
+export type ServerLineInfos = ServerLineInfo[];

+ 3 - 3
src/views/cameras/preview/components/FenceEditor/utils.ts

@@ -1,3 +1,3 @@
-export function getDefaultScale(scale: number | undefined | null) {
-  return scale ?? 1;
-}
+export function getDefaultScale(scale: number | undefined | null) {
+  return scale ?? 1;
+}

+ 308 - 273
src/views/cameras/preview/components/FenceEditorV2/FenceEditor.vue

@@ -1,273 +1,308 @@
-<template>
-  <div class="overflowWrapper" :style="{ width: props.domWidth + 'px', height: domHeight + 'px' }">
-    <div
-      class="scaleWrapper"
-      :style="{
-        scale: scale,
-        width: props.canvasSize.width + 'px',
-        height: props.canvasSize.height + 'px',
-      }"
-    >
-      <v-stage
-        :config="configKonva"
-        @mouse-down="handleStageMouseDown"
-        @mouse-move="handleStageMouseMove"
-        ref="stageRef"
-      >
-        <v-layer>
-          <FenceItem
-            :fenceGroups="fenceGroups"
-            :draggable="!drawingGroupId"
-            @select-group="handleSelectGroup"
-            :is-edit="isEdit"
-            :current-group-id="currentGroupId"
-          />
-        </v-layer>
-      </v-stage>
-    </div>
-  </div>
-</template>
-<script lang="ts" setup>
-  import Konva from 'konva';
-  import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
-  import FenceItem from './FenceItem.vue';
-  import { createCircleConfigItem, createGroupConfig } from './utils';
-  import { FenceGroup } from './types';
-  import { ElMessage } from 'element-plus';
-  import { GROUP_NAME } from './constants';
-
-  const props = defineProps<{
-    /** 电子围栏的坐标 */
-    linePoints: [number, number][][];
-    /** 画布的大小 */
-    canvasSize: { width: number; height: number };
-    /** dom的真实尺寸 */
-    domWidth: number;
-  }>();
-
-  const scale = computed(() => {
-    return props.domWidth / props.canvasSize.width;
-  });
-
-  const stageRef = ref();
-  const isEdit = ref(false);
-
-  const fenceGroups = ref<FenceGroup[]>([]);
-
-  /** 当前正在画的多边形的groupId */
-  const drawingGroupId = ref('');
-  /** 当前选中的多边形groupId,点击、拖拽、画线都会给它赋值 */
-  const currentGroupId = ref('');
-
-  watch(
-    () => props.linePoints,
-    (newLinePoints) => {
-      const configs: FenceGroup[] =
-        newLinePoints.map((points) => {
-          const flattenedPoints = points.reduce((total, next) => {
-            return [...total, ...next];
-          }, [] as number[]);
-          return createGroupConfig(flattenedPoints, scale.value);
-        }) || [];
-      fenceGroups.value = configs;
-    },
-    {
-      immediate: true,
-    },
-  );
-
-  onMounted(() => {
-    /** 取消默认的右键 */
-    document.oncontextmenu = function () {
-      return false;
-    };
-  });
-
-  onUnmounted(() => {
-    /** 取消默认的右键 */
-    document.oncontextmenu = function () {
-      return true;
-    };
-  });
-
-  const configKonva = computed(() => {
-    return props.canvasSize;
-  });
-
-  const canvasRatio = computed(() => {
-    const size = props.canvasSize;
-    if (!size) return 1;
-    return size.height / size.width;
-  });
-
-  const domHeight = computed(() => {
-    return props.domWidth * canvasRatio.value;
-  });
-
-  const handleStageMouseDown = (e) => {
-    if (!isEdit.value) return;
-    /**
-     * parent存在,说明点击的不是stage
-     * !drawingGroupId,说明当前不处于绘制多边形中
-     * 这两种情况,都不能执行stage的点击事件,要执行点击对象的默认事件
-     */
-    if (e.target.parent && !drawingGroupId.value) return;
-    const stage = e.currentTarget as Konva.Stage;
-    // 获取当前鼠标相对舞台的位置
-    const mousePosition = stage.getPointerPosition();
-    if (!mousePosition?.x || !mousePosition?.y) return;
-    const point = [mousePosition.x, mousePosition.y] as [number, number];
-
-    /** 如果还没开始画线,那么增加第一个点 */
-    if (!drawingGroupId.value) {
-      const groupConfig = createGroupConfig(point, scale.value);
-      drawingGroupId.value = groupConfig.uid;
-      currentGroupId.value = groupConfig.uid;
-      groupConfig._temp.points = point;
-      fenceGroups.value.push(groupConfig);
-    } else {
-      /** 右键点击,取消最后一个点 */
-      if (e.evt.button === 2) {
-        /** 否则就追加点 */
-        const groupConfig = fenceGroups.value.find((x) => x.uid === drawingGroupId.value);
-        if (!groupConfig) {
-          console.error('drawingGroupId无效', drawingGroupId.value);
-          return;
-        }
-        if ((groupConfig._temp.points.length || 0) <= 4) {
-          ElMessage({
-            message: '顶点数必须大于2个!',
-            type: 'warning',
-            center: true,
-            duration: 1000,
-          });
-          /** 要把当前的group给删除掉 */
-          fenceGroups.value = fenceGroups.value.filter((x) => x.uid !== drawingGroupId.value);
-          groupConfig.lineConfig.points = [];
-          groupConfig.circleConfigs = [];
-          currentGroupId.value = '';
-        } else {
-          groupConfig.lineConfig.points = groupConfig._temp.points;
-        }
-        drawingGroupId.value = '';
-        groupConfig._temp.points = [];
-        return;
-      }
-      /** 否则就追加点 */
-      const groupConfig = fenceGroups.value.find((x) => x.uid === drawingGroupId.value);
-      if (!groupConfig) {
-        console.error('drawingGroupId无效', drawingGroupId.value);
-        return;
-      }
-      const tempPoints = groupConfig._temp?.points || [];
-      const finalPoints = [...tempPoints, ...point];
-      groupConfig.lineConfig.points = finalPoints;
-      groupConfig._temp.points = finalPoints;
-      currentGroupId.value = groupConfig.uid;
-
-      const circleConfig = createCircleConfigItem(
-        point,
-        groupConfig.circleConfigs.length,
-        scale.value,
-      );
-      groupConfig.circleConfigs.push(circleConfig);
-    }
-  };
-
-  const handleStageMouseMove = (e) => {
-    if (!isEdit.value) return;
-    const stage = e.currentTarget as Konva.Stage;
-    /** 获取当前鼠标的坐标 */
-    const mousePosition = stage.getPointerPosition();
-    if (!mousePosition?.x || !mousePosition?.y) return;
-    const newPoint = [mousePosition.x, mousePosition.y];
-    if (drawingGroupId.value) {
-      const groupConfig: FenceGroup | undefined = fenceGroups.value.find(
-        (x) => x.uid === drawingGroupId.value,
-      );
-      if (!groupConfig) {
-        console.error('drawingGroupId无效', drawingGroupId.value);
-        return;
-      }
-
-      /** 如果正在画线,那么替换最后一个点 */
-      const initialPoints = groupConfig.lineConfig.points as number[];
-      if (groupConfig._temp.points.length > 0) {
-        groupConfig.lineConfig.points = [...groupConfig._temp.points, ...newPoint];
-      } else {
-        groupConfig._temp.points = initialPoints;
-      }
-    }
-  };
-
-  const handleSelectGroup = (groupId: string) => {
-    currentGroupId.value = groupId;
-  };
-
-  /** 清空所有元素 */
-  const clear = () => {
-    fenceGroups.value = [];
-  };
-
-  /** 删除当前选中的group项 */
-  const remove = () => {
-    fenceGroups.value = fenceGroups.value.filter((x) => x.uid !== currentGroupId.value);
-    currentGroupId.value = '';
-  };
-
-  /** 导出为json格式 */
-  const toObject = () => {
-    const stage = stageRef.value.getStage();
-    const fenceGroups = stage?.find('.' + GROUP_NAME);
-    const gropuPoints = fenceGroups
-      ?.map((item) => {
-        const groupX = item.x();
-        const groupY = item.y();
-
-        const line = (item as Konva.Group).findOne(
-          (x: any) => x.className === 'Line',
-        ) as Konva.Line;
-        const points = line?.points();
-        /** 有些line对象存在,但是没有点坐标,所以要判断过滤一下 */
-        if (points && points.length > 0) {
-          const newPoints: number[][] = [];
-          /** 存到后端的时候,只给点的坐标信息,不会给group的位置信息,所以要将点的坐标加上group的位移,才是之后点的最终坐标 */
-          for (let i = 0; i < points.length; i += 2) {
-            newPoints.push([Math.floor(points[i] + groupX), Math.floor(points[i + 1] + groupY)]);
-          }
-          return newPoints;
-        }
-        return null;
-      })
-      .filter(Boolean);
-    return gropuPoints;
-  };
-
-  /** 退出编辑模式 */
-  const exitEditMode = () => {
-    currentGroupId.value = '';
-    isEdit.value = false;
-  };
-  /** 进入编辑模式 */
-  const setEditMode = () => {
-    isEdit.value = true;
-  };
-
-  defineExpose({
-    clear,
-    remove,
-    toObject,
-    exitEditMode,
-    setEditMode,
-  });
-</script>
-
-<style scoped>
-  .scaleWrapper {
-    transform-origin: left top;
-  }
-  .overflowWrapper {
-    overflow: hidden;
-    border: 1px solid #efefef;
-  }
-</style>
+<template>
+  <div class="overflowWrapper" :style="{ width: props.domWidth + 'px', height: domHeight + 'px' }">
+    <div
+      class="scaleWrapper"
+      :style="{
+        scale: scale,
+        width: props.canvasSize.width + 'px',
+        height: props.canvasSize.height + 'px',
+      }"
+    >
+      <v-stage
+        :config="configKonva"
+        @mouse-down="handleStageMouseDown"
+        @mouse-move="handleStageMouseMove"
+        ref="stageRef"
+      >
+        <v-layer>
+          <FenceItem
+            :fenceGroups="fenceGroups"
+            :draggable="!drawingGroupId"
+            :is-edit="isEdit"
+            :current-group-id="currentGroupId"
+            @select-group="handleSelectGroup"
+            @group-end-move="handleGroupEndMove"
+          />
+        </v-layer>
+      </v-stage>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import Konva from 'konva';
+  import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
+  import FenceItem from './FenceItem.vue';
+  import { createCircleConfigItem, createGroupConfig, polygonPoint1ToPoint2, stageGroupToPoint2 } from './utils';
+  import { FenceGroup, FencePolygonPoints, SingleFence } from './types';
+  import { ElMessage } from 'element-plus';
+  import { GROUP_NAME } from './constants';
+
+  const props = defineProps<{
+    /** 电子围栏的坐标 */
+    linePoints: SingleFence[];
+    /** 画布的大小 */
+    canvasSize: { width: number; height: number };
+    /** dom的真实尺寸 */
+    domWidth: number;
+    /** 当前选中的是哪个分组 */
+    fenceId?: number | null;
+  }>();
+
+  // 保存修改后的电子围栏, fenceId为空表示新建的,否则表示编辑已存在的
+  interface EmitsEvents {
+    (e: 'save', data: { fenceId?: number; polygon: FencePolygonPoints }): unknown;
+    (e: 'select', fenceId: number | null): unknown;
+  }
+  const emits = defineEmits<EmitsEvents>();
+
+  const scale = computed(() => {
+    return props.domWidth / props.canvasSize.width;
+  });
+
+  const stageRef = ref();
+  const isEdit = ref(true);
+
+  const fenceGroups = ref<FenceGroup[]>([]);
+
+  /** 当前正在画的多边形的groupId */
+  const drawingGroupId = ref('');
+  /** 当前选中的多边形groupId,点击、拖拽、画线都会给它赋值 */
+  const currentGroupId = ref('');
+
+  watch(
+    () => props.linePoints,
+    (newLinePoints) => {
+      const configs: FenceGroup[] =
+        newLinePoints.map((points) => {
+          const polygonJSON = points.polygon || [];
+          const flattenedPoints = polygonJSON.reduce((total, next) => {
+            return [...total, ...next];
+          }, [] as number[]);
+          return createGroupConfig({ fenceId: points.id, points: flattenedPoints, scale: scale.value });
+        }) || [];
+      fenceGroups.value = configs;
+
+      const currentGroup = fenceGroups.value.find((x) => x.fenceId === props.fenceId);
+      const groupId = currentGroup?.uid;
+      if (groupId) {
+        currentGroupId.value = groupId;
+      }
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  watch(
+    () => props.fenceId,
+    (nextFenceId) => {
+      const currentGroup = fenceGroups.value.find((x) => x.fenceId === nextFenceId);
+      const groupId = currentGroup?.uid;
+      if (groupId) {
+        currentGroupId.value = groupId;
+      }
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  onMounted(() => {
+    /** 取消默认的右键 */
+    document.oncontextmenu = function () {
+      return false;
+    };
+  });
+
+  onUnmounted(() => {
+    /** 取消默认的右键 */
+    document.oncontextmenu = function () {
+      return true;
+    };
+  });
+
+  const configKonva = computed(() => {
+    return props.canvasSize;
+  });
+
+  const canvasRatio = computed(() => {
+    const size = props.canvasSize;
+    if (!size) return 1;
+    return size.height / size.width;
+  });
+
+  const domHeight = computed(() => {
+    return props.domWidth * canvasRatio.value;
+  });
+
+  const handleStageMouseDown = (e) => {
+    if (!isEdit.value) return;
+    /**
+     * parent存在,说明点击的不是stage
+     * !drawingGroupId,说明当前不处于绘制多边形中
+     * 这两种情况,都不能执行stage的点击事件,要执行点击对象的默认事件
+     */
+    if (e.target.parent && !drawingGroupId.value) return;
+    const stage = e.currentTarget as Konva.Stage;
+    // 获取当前鼠标相对舞台的位置
+    const mousePosition = stage.getPointerPosition();
+    if (!mousePosition?.x || !mousePosition?.y) return;
+    const point = [mousePosition.x, mousePosition.y] as [number, number];
+
+    /** 如果还没开始画线,那么增加第一个点 */
+    if (!drawingGroupId.value) {
+      const groupConfig = createGroupConfig({ points: point, scale: scale.value });
+      drawingGroupId.value = groupConfig.uid;
+      currentGroupId.value = groupConfig.uid;
+      groupConfig._temp.points = point;
+      fenceGroups.value.push(groupConfig);
+      // 创建新点时,要把之前选中的电子围栏取消高亮
+      emits('select', null);
+    } else {
+      /** 右键点击,取消最后一个点 */
+      if (e.evt.button === 2) {
+        /** 否则就追加点 */
+        const groupConfig = fenceGroups.value.find((x) => x.uid === drawingGroupId.value);
+        if (!groupConfig) {
+          console.error('drawingGroupId无效', drawingGroupId.value);
+          return;
+        }
+        if ((groupConfig._temp.points.length || 0) <= 4) {
+          ElMessage({
+            message: '顶点数必须大于2个!',
+            type: 'warning',
+            center: true,
+            duration: 1000,
+          });
+          /** 要把当前的group给删除掉 */
+          fenceGroups.value = fenceGroups.value.filter((x) => x.uid !== drawingGroupId.value);
+          groupConfig.lineConfig.points = [];
+          groupConfig.circleConfigs = [];
+          currentGroupId.value = '';
+        } else {
+          const points = groupConfig._temp.points;
+          groupConfig.lineConfig.points = points;
+          console.log('完成了一个电子围栏的编辑groupConfig', groupConfig);
+          const points2 = polygonPoint1ToPoint2(points);
+          if (points2) {
+            emits('save', { fenceId: groupConfig.fenceId, polygon: points2 });
+          }
+        }
+        drawingGroupId.value = '';
+        groupConfig._temp.points = [];
+        return;
+      }
+      /** 否则就追加点 */
+      const groupConfig = fenceGroups.value.find((x) => x.uid === drawingGroupId.value);
+      if (!groupConfig) {
+        console.error('drawingGroupId无效', drawingGroupId.value);
+        return;
+      }
+      const tempPoints = groupConfig._temp?.points || [];
+      const finalPoints = [...tempPoints, ...point];
+      groupConfig.lineConfig.points = finalPoints;
+      groupConfig._temp.points = finalPoints;
+      currentGroupId.value = groupConfig.uid;
+
+      const circleConfig = createCircleConfigItem(point, groupConfig.circleConfigs.length, scale.value);
+      groupConfig.circleConfigs.push(circleConfig);
+    }
+  };
+
+  const handleStageMouseMove = (e) => {
+    if (!isEdit.value) return;
+    const stage = e.currentTarget as Konva.Stage;
+    /** 获取当前鼠标的坐标 */
+    const mousePosition = stage.getPointerPosition();
+    if (!mousePosition?.x || !mousePosition?.y) return;
+    const newPoint = [mousePosition.x, mousePosition.y];
+    if (drawingGroupId.value) {
+      const groupConfig: FenceGroup | undefined = fenceGroups.value.find((x) => x.uid === drawingGroupId.value);
+      if (!groupConfig) {
+        console.error('drawingGroupId无效', drawingGroupId.value);
+        return;
+      }
+
+      /** 如果正在画线,那么替换最后一个点 */
+      const initialPoints = groupConfig.lineConfig.points as number[];
+      if (groupConfig._temp.points.length > 0) {
+        groupConfig.lineConfig.points = [...groupConfig._temp.points, ...newPoint];
+      } else {
+        groupConfig._temp.points = initialPoints;
+      }
+    }
+  };
+
+  const handleSelectGroup = (groupId: string) => {
+    currentGroupId.value = groupId;
+    const thisFence = fenceGroups.value.find((x) => x.uid === groupId);
+    if (thisFence?.fenceId) {
+      emits('select', thisFence.fenceId);
+    }
+  };
+
+  /** 清空所有元素 */
+  const clear = () => {
+    fenceGroups.value = [];
+  };
+
+  /** 删除当前选中的group项 */
+  const remove = () => {
+    fenceGroups.value = fenceGroups.value.filter((x) => x.uid !== currentGroupId.value);
+    currentGroupId.value = '';
+  };
+
+  /** 导出为json格式 */
+  const toObject = () => {
+    const stage = stageRef.value.getStage();
+    const fenceGroups = stage?.find('.' + GROUP_NAME);
+    const gropuPoints = fenceGroups
+      ?.map((item) => {
+        return stageGroupToPoint2(item);
+      })
+      .filter(Boolean);
+    return gropuPoints;
+  };
+
+  // 分组移动后保存位置信息
+  const handleGroupEndMove = (groupId: string) => {
+    const stage = stageRef.value.getStage();
+    const stageGroups = stage?.find('.' + GROUP_NAME);
+    const group = stageGroups?.find((item) => {
+      return item.attrs.groupId === groupId;
+    });
+    if (!group) {
+      console.error('未匹配到groupId', groupId);
+    }
+    const points = stageGroupToPoint2(group);
+    emits('save', { fenceId: group.attrs.fenceId, polygon: points! });
+  };
+
+  /** 退出编辑模式 */
+  const exitEditMode = () => {
+    currentGroupId.value = '';
+    isEdit.value = false;
+  };
+  /** 进入编辑模式 */
+  const setEditMode = () => {
+    isEdit.value = true;
+  };
+
+  defineExpose({
+    clear,
+    remove,
+    toObject,
+    exitEditMode,
+    setEditMode,
+  });
+</script>
+
+<style scoped>
+  .scaleWrapper {
+    transform-origin: left top;
+  }
+  .overflowWrapper {
+    overflow: hidden;
+    border: 1px solid #efefef;
+  }
+</style>

+ 78 - 57
src/views/cameras/preview/components/FenceEditorV2/FenceItem.vue

@@ -1,57 +1,78 @@
-<!-- eslint-disable vue/no-use-v-if-with-v-for -->
-<template>
-  <v-group
-    v-for="group in props.fenceGroups"
-    :key="group.uid"
-    :groupId="group.uid"
-    :draggable="props.draggable && props.isEdit"
-    :name="group.name"
-    @mouse-down="handleGroupMouseDown"
-  >
-    <v-line :config="group.lineConfig" />
-    <v-circle
-      v-if="props.isEdit && props.currentGroupId === group.uid"
-      v-for="circleConfig in group.circleConfigs"
-      :config="circleConfig"
-      :key="circleConfig"
-      @mouse-down="handleCircleMouseDown"
-      @drag-move="handleCircleDragMove(circleConfig, $event)"
-    />
-  </v-group>
-</template>
-<script lang="ts" setup>
-  import { FenceCircleConfig, FenceGroup } from './types';
-
-  const props = defineProps<{
-    fenceGroups: FenceGroup[];
-    draggable: boolean;
-    isEdit: boolean;
-    /** 当前选中的分组 */
-    currentGroupId: string;
-  }>();
-
-  const emits = defineEmits<{ (e: 'selectGroup', groupId: string): unknown }>();
-
-  const handleCircleDragMove = (circleConfig: FenceCircleConfig, e) => {
-    console.log('circle move', e);
-    console.log('circle move circleConfig', circleConfig);
-    const lineAttrs = e.target.parent.find('Line')[0].attrs;
-    const { x, y, idx } = e.target.attrs;
-    lineAttrs.points[idx * 2] = x;
-    lineAttrs.points[idx * 2 + 1] = y;
-    circleConfig.x = x;
-    circleConfig.y = y;
-  };
-
-  const handleCircleMouseDown = (e: { cancelBubble: boolean }) => {
-    /** 阻止冒泡 */
-    e.cancelBubble = true;
-  };
-
-  const handleGroupMouseDown = (e) => {
-    if (!props.isEdit) return;
-    e.target.parent.moveToTop();
-    emits('selectGroup', e.target.parent.attrs.groupId);
-  };
-</script>
-<style scoped></style>
+<!-- eslint-disable vue/no-use-v-if-with-v-for -->
+<template>
+  <v-group
+    v-for="group in props.fenceGroups"
+    :key="group.uid"
+    :groupId="group.uid"
+    :fenceId="group.fenceId"
+    :draggable="props.draggable && props.isEdit"
+    :name="group.name"
+    @mouse-down="handleGroupMouseDown"
+    @dragend="handleGroupDragEnd"
+  >
+    <v-line :config="group.lineConfig" />
+    <v-circle
+      v-if="props.isEdit && props.currentGroupId === group.uid"
+      v-for="circleConfig in group.circleConfigs"
+      :config="circleConfig"
+      :key="circleConfig"
+      @mouse-down="handleCircleMouseDown"
+      @drag-move="handleCircleDragMove(circleConfig, $event)"
+      @dragend="handleCircleDragEnd"
+    />
+  </v-group>
+</template>
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import { FenceCircleConfig, FenceGroup } from './types';
+
+  const props = defineProps<{
+    fenceGroups: FenceGroup[];
+    draggable: boolean;
+    isEdit: boolean;
+    /** 当前选中的分组 */
+    currentGroupId: string;
+  }>();
+
+  const emits = defineEmits<{
+    (e: 'selectGroup', groupId: string): unknown;
+    (e: 'groupEndMove', groupId: string): unknown;
+  }>();
+
+  const handleCircleDragMove = (circleConfig: FenceCircleConfig, e) => {
+    const lineAttrs = e.target.parent.find('Line')[0].attrs;
+    const { x, y, idx } = e.target.attrs;
+    lineAttrs.points[idx * 2] = x;
+    lineAttrs.points[idx * 2 + 1] = y;
+    circleConfig.x = x;
+    circleConfig.y = y;
+  };
+
+  const handleCircleMouseDown = (e: { cancelBubble: boolean }) => {
+    /** 阻止冒泡 */
+    e.cancelBubble = true;
+  };
+
+  const handleGroupMouseDown = (e) => {
+    if (!props.isEdit) return;
+    e.target.parent.moveToTop();
+    console.log('groupMouseDown', e);
+    emits('selectGroup', e.target.parent.attrs.groupId);
+  };
+
+  const handleGroupDragEnd = (e) => {
+    const currentGroupId = e.target.attrs.groupId;
+    console.log('groupDragEnd', e);
+    // 分组停止移动
+    emits('groupEndMove', currentGroupId);
+  };
+
+  const handleCircleDragEnd = (e) => {
+    // 阻止触发group的dragend事件
+    e.cancelBubble = true;
+    const groupId = e.target.parent.attrs.groupId;
+    console.log('circle drag end', e);
+    emits('groupEndMove', groupId);
+  };
+</script>
+<style scoped></style>

+ 17 - 17
src/views/cameras/preview/components/FenceEditorV2/constants.ts

@@ -1,17 +1,17 @@
-export const defaultLineStyle = {
-  stroke: '#52FFDA',
-  strokeWidth: 3,
-  closed: true,
-  // fill: '#ff0000',
-};
-
-export const defaultCircleStyle = {
-  /** 圆的半径 */
-  radius: 4,
-  /** 点击区域 */
-  hitStrokeWidth: 10,
-  fill: '#52FFDA',
-  draggable: true,
-};
-
-export const GROUP_NAME = 'fenceGroup';
+export const defaultLineStyle = {
+  stroke: '#52FFDA',
+  strokeWidth: 3,
+  closed: true,
+  // fill: '#ff0000',
+};
+
+export const defaultCircleStyle = {
+  /** 圆的半径 */
+  radius: 4,
+  /** 点击区域 */
+  hitStrokeWidth: 10,
+  fill: '#52FFDA',
+  draggable: true,
+};
+
+export const GROUP_NAME = 'fenceGroup';

+ 30 - 17
src/views/cameras/preview/components/FenceEditorV2/types.ts

@@ -1,17 +1,30 @@
-import Konva from 'konva';
-
-export type FenceLineConfig = Konva.LineConfig;
-export interface FenceCircleConfig extends Konva.CircleConfig {
-  uid: string;
-}
-
-export interface FenceGroup {
-  lineConfig: FenceLineConfig;
-  /** 临时存放点坐标 */
-  _temp: {
-    points: number[];
-  };
-  circleConfigs: FenceCircleConfig[];
-  uid: string;
-  name: string;
-}
+import Konva from 'konva';
+
+export type FenceLineConfig = Konva.LineConfig;
+export interface FenceCircleConfig extends Konva.CircleConfig {
+  uid: string;
+}
+
+export interface FenceGroup {
+  lineConfig: FenceLineConfig;
+  /** 临时存放点坐标 */
+  _temp: {
+    points: number[];
+  };
+  circleConfigs: FenceCircleConfig[];
+  uid: string;
+  name: string;
+  /** 电子围栏的id */
+  fenceId?: number;
+}
+
+/** 单个电子围栏的多边形点 */
+export type FencePolygonPoints = [number, number][];
+
+/** 单个电子围栏信息 */
+export interface SingleFence {
+  id: number;
+  label: string;
+  name: string;
+  polygon: FencePolygonPoints;
+}

+ 76 - 0
src/views/cameras/algo-params-setting/components/FenceEditorV2/utils.ts

@@ -0,0 +1,76 @@
+import { GROUP_NAME, defaultCircleStyle, defaultLineStyle } from './constants';
+import { uid } from 'uid';
+import { FenceGroup } from './types';
+import Konva from 'konva';
+
+export const getCircleConfig = (points: number[], scale: number) => {
+  const circlePoints = [];
+  for (let i = 0; i < points.length - 1; i += 2) {
+    circlePoints.push([points[i], points[i + 1]]);
+  }
+  return circlePoints.map((point, idx) => {
+    return createCircleConfigItem(point as [number, number], idx, scale);
+  });
+};
+
+export const createCircleConfigItem = (point: [number, number], idx: number, scale: number) => {
+  return {
+    ...defaultCircleStyle,
+    radius: defaultCircleStyle.radius / scale,
+    hitStrokeWidth: defaultCircleStyle.hitStrokeWidth / scale,
+    x: point[0],
+    y: point[1],
+    uid: uid(),
+    idx,
+  };
+};
+
+export const createGroupConfig = (data: { points: number[]; scale: number; fenceId?: number }): FenceGroup => {
+  const { points, scale, fenceId } = data;
+  const lineConfig = {
+    ...defaultLineStyle,
+    strokeWidth: defaultLineStyle.strokeWidth / scale,
+    points: points,
+  };
+  const circleConfigs = getCircleConfig(points, scale);
+  return {
+    lineConfig,
+    name: GROUP_NAME,
+    circleConfigs,
+    uid: uid(),
+    fenceId,
+    _temp: { points: [] },
+  };
+};
+
+/** 将stage中的多边形转化为二维点坐标 */
+export const stageGroupToPoint2 = (fenceGroup: Konva.Group): [number, number][] | null => {
+  const groupX = fenceGroup.x();
+  const groupY = fenceGroup.y();
+
+  const line = (fenceGroup as Konva.Group).findOne((x: any) => x.className === 'Line') as Konva.Line;
+  const points = line?.points();
+  /** 有些line对象存在,但是没有点坐标,所以要判断过滤一下 */
+  if (points && points.length > 0) {
+    const newPoints: [number, number][] = [];
+    /** 存到后端的时候,只给点的坐标信息,不会给group的位置信息,所以要将点的坐标加上group的位移,才是之后点的最终坐标 */
+    for (let i = 0; i < points.length; i += 2) {
+      newPoints.push([Math.floor(points[i] + groupX), Math.floor(points[i + 1] + groupY)]);
+    }
+    return newPoints;
+  }
+  return null;
+};
+
+/** 一个多边形一维点转化为二维的点 */
+export const polygonPoint1ToPoint2 = (points: number[]): [number, number][] | null => {
+  if (points.length < 2) {
+    console.error('多边形的点数量少于1个');
+    return null;
+  }
+  const newPoints: [number, number][] | null = [];
+  for (let i = 0; i < points.length; i += 2) {
+    newPoints.push([points[i], points[i + 1]]);
+  }
+  return newPoints;
+};

+ 70 - 0
src/views/cameras/algo-params-setting/components/FenceToolbar/EditFenceDialog.vue

@@ -0,0 +1,70 @@
+<template>
+  <ElDialog title="编辑电子围栏" v-model="visible" append-to=".presetWrapper" width="95%" top="50px">
+    <!-- 设置 label-position 为 top -->
+    <ElForm :model="form" label-width="100%" label-position="top" ref="formRef" :rules="rules">
+      <!-- 添加名称输入项 -->
+      <ElFormItem label="名称" prop="name">
+        <ElInput v-model="form.name" placeholder="请输入名称" />
+      </ElFormItem>
+      <!-- 添加标签输入项 -->
+      <ElFormItem label="标签" prop="label">
+        <ElInput v-model="form.label" placeholder="请输入标签" />
+      </ElFormItem>
+    </ElForm>
+
+    <template #footer>
+      <ElButton @click="handleCancel">取消</ElButton>
+      <ElButton type="primary" @click="handleConfirm">确定</ElButton>
+    </template>
+  </ElDialog>
+</template>
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import { ElDialog, ElForm, ElFormItem, ElInput, ElButton, FormInstance } from 'element-plus';
+
+  // 定义表单数据
+  const props = defineProps<{
+    detail: {
+      name: string;
+      label: string;
+    };
+  }>();
+  const emits = defineEmits<{ (e: 'cancel'): void; (e: 'submit', param: { name: string; label: string }): void }>();
+
+  const form = ref({
+    name: props.detail.name,
+    label: props.detail.label,
+  });
+
+  const formRef = ref<FormInstance>();
+  const rules = ref({
+    name: [
+      { required: true, message: '不能为空', trigger: 'change' },
+      { max: 15, message: '最多支持15个字符', trigger: 'change' },
+    ],
+    label: [{ max: 15, message: '最多支持15个字符', trigger: 'change' }],
+  });
+
+  const visible = ref(true);
+
+  // 处理取消按钮点击事件
+  const handleCancel = () => {
+    // 这里可以添加关闭对话框的逻辑
+    emits('cancel');
+  };
+
+  // 处理确定按钮点击事件
+  const handleConfirm = () => {
+    if (!formRef.value) return;
+    // 这里可以添加提交表单的逻辑
+    formRef.value.validate((valid) => {
+      if (valid) {
+        emits('submit', form.value);
+      } else {
+        console.log('表单验证失败');
+      }
+    });
+  };
+</script>
+
+<style></style>

+ 74 - 0
src/views/cameras/algo-params-setting/components/FenceToolbar/FenceNameItem.vue

@@ -0,0 +1,74 @@
+<!-- 电子围栏名称的一项 -->
+<template>
+  <div :class="props.active ? 'active' : ''" class="fenceItem">
+    <div>
+      {{ props.detail.name }}
+    </div>
+
+    <ElTag type="success" size="small" class="fenceLabel" v-if="props.detail.label">{{ props.detail.label }}</ElTag>
+    <el-popover placement="bottom" trigger="click" popper-style="min-width: 80px;width: 80px">
+      <template #reference>
+        <img :src="moreDotIcon" alt="菜单" class="moreDot" />
+      </template>
+      <template #default>
+        <div class="popoverMenu" @click="handleEdit">编辑</div>
+        <div class="popoverMenu" @click="handleDelete">删除</div>
+      </template>
+    </el-popover>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { ElTag } from 'element-plus';
+  import moreDotIcon from '@/assets/icons/more-dot-icon.png';
+  interface FenceDetail {
+    name: string;
+    id: number;
+    label: string;
+  }
+  const props = defineProps<{ detail: FenceDetail; active: boolean }>();
+
+  const emits = defineEmits<{ (e: 'edit', id: number): unknown; (e: 'delete', id: number): unknown }>();
+
+  const handleDelete = () => {
+    emits('delete', props.detail.id);
+  };
+  const handleEdit = () => {
+    emits('edit', props.detail.id);
+  };
+</script>
+
+<style>
+  .fenceItem.active {
+    background: #409eff;
+    color: #fff;
+    border-color: transparent;
+  }
+  .moreDot {
+    width: 20px;
+    height: 20px;
+    position: absolute;
+    right: 0;
+    top: 0;
+    margin: auto;
+    bottom: 0;
+  }
+  .fenceItem {
+    position: relative;
+    padding: 15px;
+    border: 1px solid #ccc;
+    margin: 5px 0;
+    cursor: pointer;
+  }
+  .popoverMenu {
+    text-align: center;
+    margin: 10px 0px;
+    cursor: pointer;
+  }
+  .fenceLabel {
+    position: absolute;
+    bottom: 2px;
+    right: 20px;
+    height: 14px;
+  }
+</style>

+ 267 - 0
src/views/cameras/algo-params-setting/components/FenceToolbar/FenceToolbar.vue

@@ -0,0 +1,267 @@
+<template>
+  <div class="fenceWrapper">
+    <div>
+      <div class="fenceTitle">电子围栏</div>
+      <ElSwitch
+        size="small"
+        class="fenceSwitchBtn"
+        v-model="selectedAlgoDetail.electronicFenceBool"
+        @update:modelValue="handleUpdateFenceStatus"
+      />
+    </div>
+    <div class="algoName">
+      {{ selectedAlgoDetail?.algoInfo?.name }}
+    </div>
+    <PresetSelect />
+
+    <div style="display: flex">
+      <ElCheckbox label="检测围栏外部" v-model="isFenceRegionOut" @update:modelValue="handleUpdateRegion" />
+      <ElCheckbox label="前台画面显示" v-model="isDisplayFenceInVideo" @update:modelValue="handleUpdateDisplay" />
+      <a :href="previewUrl" target="_blank" style="margin-left: 20px" v-if="previewUrl && false">平台相机预览</a>
+    </div>
+    <div class="fenceListWrapper">
+      <FenceNameItem
+        :active="item.id === fenceStore.currentFenceId"
+        v-for="item in fenceStore.allFences"
+        :detail="item"
+        :key="item.id"
+        @click="handleSelectFence(item.id)"
+        @delete="handleDeleteFence"
+        @edit="handleEditFenceInfo(item)"
+      />
+    </div>
+    <div>
+      <EditFenceDialog
+        v-if="showEditFenceDialog"
+        @cancel="handleEditCancel"
+        @submit="handleEditSubmit"
+        :detail="selectedDetail"
+      />
+    </div>
+
+    <!-- <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> -->
+
+    <!-- <ElButton type="primary" size="small" @click="toggleEdit">{{
+        props.isEdit ? '退出编辑' : '编辑电子围栏'
+      }}</ElButton> -->
+    <!-- </div> -->
+  </div>
+</template>
+<script setup lang="ts">
+  import { computed, defineEmits, ref, watch } from 'vue';
+  import { ElButton, ElSwitch } from 'element-plus';
+  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';
+  import PresetSelect from '../PresetSelect/PresetSelect.vue';
+  import FenceNameItem from './FenceNameItem.vue';
+  import useFenceStore from '../../store/useFenceStore';
+  import useCameraDetailStore from '../../store/useCameraDetailStore';
+  import usePresetListStore from '../../store/usePresetListStore';
+  import EditFenceDialog from './EditFenceDialog.vue';
+  import { ServerLineInfo } from '../FenceEditor/constants';
+  import { storeToRefs } from 'pinia';
+  import { RegionJudge } from './constants';
+  import { choosePreset, updateFenceDisplayStatus } from '@/api/camera/camera-preview';
+  import { FenceDisplayStatus } from '@/types/camera/constant';
+  import { useGlobSetting } from '@/hooks/setting';
+  const cameraAlgoStore = useCameraAlgoStore();
+
+  const fenceStore = useFenceStore();
+  const cameraDetailStore = useCameraDetailStore();
+  const presetStore = usePresetListStore();
+  const props = defineProps<{ isEdit: boolean }>();
+
+  const showEditFenceDialog = ref(false);
+  const selectedDetail = ref<ServerLineInfo | null>(null);
+
+  const { selectedAlgoDetail } = storeToRefs(cameraAlgoStore);
+
+  const emits = defineEmits<{
+    // (e: 'toggleEditable', editState: boolean): unknown;
+    (e: 'toggleRange'): unknown;
+    (e: 'remove'): unknown;
+    (e: 'save'): unknown;
+    (e: 'select', fenceId: number): unknown;
+    /** 切换电子围栏打开关闭状态 */
+    (e: 'toggleFenceStatus', nextStatus: boolean): unknown;
+  }>();
+
+  const isFenceRegionOut = ref(false);
+  const isDisplayFenceInVideo = ref(false);
+
+  const { detail } = storeToRefs(cameraDetailStore);
+
+  const { appPCUrl } = useGlobSetting();
+
+  const previewUrl = computed(() => {
+    const firstSceneId = detail.value?.sceneTemplateList[0]?.sceneId;
+    if (!detail.value?.workshopId || !detail.value?.code || !firstSceneId) return '';
+    return appPCUrl + `#/shop?id=${detail.value?.workshopId}&cameraCode=${detail.value?.code!}&sceneId=${firstSceneId}`;
+  });
+
+  watch(
+    () => selectedAlgoDetail.value?.regionJudge,
+    (newVal) => {
+      isFenceRegionOut.value = newVal === RegionJudge.out;
+    },
+    {
+      immediate: true,
+    },
+  );
+  watch(
+    () => cameraDetailStore.isShowFence,
+    (isShowFence) => {
+      isDisplayFenceInVideo.value = isShowFence;
+    },
+    {
+      immediate: true,
+    },
+  );
+
+  const handleSelectFence = (nextFenceId: number) => {
+    emits('select', nextFenceId);
+  };
+
+  const handleUpdateRegion = (val: string) => {
+    console.log('isFenceRegionOut', isFenceRegionOut.value);
+    console.log('region', val);
+    emits('toggleRange');
+  };
+
+  const handleEditFenceInfo = (detail) => {
+    showEditFenceDialog.value = true;
+    selectedDetail.value = detail;
+  };
+
+  const handleEditCancel = () => {
+    showEditFenceDialog.value = false;
+    selectedDetail.value = null;
+  };
+
+  const handleDeleteFence = (fenceId: number) => {
+    const cameraId = cameraDetailStore.cameraId;
+    const algoId = cameraAlgoStore.selectedAlgoId!;
+    const presetToken = presetStore.currentPresetToken;
+    fenceStore.deleteFence({ cameraId, algoId, presetToken, fenceId });
+  };
+
+  const handleEditSubmit = (data: { label: string; name: string }) => {
+    const cameraId = cameraDetailStore.cameraId;
+    const algoId = cameraAlgoStore.selectedAlgoId!;
+    const presetToken = presetStore.currentPresetToken;
+    const fenceId = selectedDetail.value?.id;
+    if (!fenceId) return;
+    fenceStore
+      .editFence({
+        cameraId,
+        algoId,
+        presetToken,
+        fenceId: fenceId,
+        fenceLabel: data.label,
+        fenceName: data.name,
+      })
+      .then(() => {
+        handleEditCancel();
+      });
+  };
+
+  const handleUpdateDisplay = (nextStatus: boolean) => {
+    const params = {
+      cameraCode: cameraDetailStore.detail?.code!,
+      isDisplayFence: nextStatus ? FenceDisplayStatus.enabled : FenceDisplayStatus.disabled,
+    };
+
+    updateFenceDisplayStatus(params);
+    if (nextStatus) {
+      // 由于历史原因,需要调用两次接口
+      const cameraId = cameraDetailStore.cameraId;
+      const algoId = cameraAlgoStore.selectedAlgoId!;
+      const presetToken = presetStore.currentPresetToken;
+      const params = {
+        algoId,
+        cameraId,
+        presetToken,
+      };
+      choosePreset(params);
+    }
+  };
+
+  const handleUpdateFenceStatus = (nextStatus: boolean) => {
+    console.log('nextFenceStatus', nextStatus);
+    emits('toggleFenceStatus', nextStatus);
+  };
+</script>
+
+<style scoped>
+  .toolbar {
+    display: flex;
+
+    align-items: center;
+    z-index: 10;
+    padding: 5px;
+    border-radius: 50px;
+  }
+  .fenceDrawingTip {
+    background: #e8f5ff;
+    border-radius: 6px;
+    border: 1px solid #bae0ff;
+    text-align: center;
+    font-size: 12px;
+    padding: 2px 20px;
+    margin-right: 30px;
+    color: #1890ff;
+  }
+
+  .fenceSwitchBtn {
+    position: absolute;
+    right: 10px;
+    top: 10px;
+  }
+
+  .algoName {
+    color: #ccc;
+    margin-top: 10px;
+    font-size: 12px;
+  }
+
+  .fenceWrapper {
+    padding: 10px;
+  }
+  .fenceListWrapper {
+    max-height: 435px;
+    overflow-y: auto;
+  }
+
+  .fenceTitle {
+    font-weight: bold;
+    font-size: 16px;
+    position: relative;
+    margin-left: 10px;
+    &::before {
+      content: '';
+      width: 4px;
+      position: absolute;
+      left: -8px;
+      height: 20px;
+      top: 2px;
+      background-color: #1890ff;
+    }
+  }
+</style>

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

@@ -1,29 +1,29 @@
-<template>
-  <ToolbarIcon :src="src" :active="false" :tip="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';
-  import { RegionJudge } from './constants';
-
-  const props = defineProps<{ active: boolean }>();
-
-  const src = computed(() => {
-    return selectedAlgoDetail.value.regionJudge === RegionJudge.out ? fenceOut : fenceInner;
-  });
-
-  const tip = computed(() => {
-    return selectedAlgoDetail.value.regionJudge === RegionJudge.out
-      ? '点击后将检测电子围栏内部'
-      : '点击后将检测电子围栏外部';
-  });
-
-  const cameraAlgoStore = useCameraAlgoStore();
-  const { selectedAlgoDetail } = storeToRefs(cameraAlgoStore);
-</script>
-<style scoped></style>
+<template>
+  <ToolbarIcon :src="src" :active="false" :tip="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 from '../../store/useCameraAlgoStore';
+
+  import { computed } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { RegionJudge } from './constants';
+
+  const props = defineProps<{ active: boolean }>();
+
+  const src = computed(() => {
+    return selectedAlgoDetail.value.regionJudge === RegionJudge.out ? fenceOut : fenceInner;
+  });
+
+  const tip = computed(() => {
+    return selectedAlgoDetail.value.regionJudge === RegionJudge.out
+      ? '点击后将检测电子围栏内部'
+      : '点击后将检测电子围栏外部';
+  });
+
+  const cameraAlgoStore = useCameraAlgoStore();
+  const { selectedAlgoDetail } = storeToRefs(cameraAlgoStore);
+</script>
+<style scoped></style>

+ 6 - 6
src/views/cameras/preview/components/FenceToolbar/constants.ts

@@ -1,6 +1,6 @@
-export enum RegionJudge {
-  // 电子围栏外部区域生效
-  out = 1,
-  // 电子围栏内部区域生效
-  in = 0,
-}
+export enum RegionJudge {
+  // 电子围栏外部区域生效
+  out = 1,
+  // 电子围栏内部区域生效
+  in = 0,
+}

+ 103 - 103
src/views/cameras/preview/components/PresetSelect/PresetSelect.vue

@@ -1,103 +1,103 @@
-<template>
-  <div class="presetSetting">
-    <div style="margin-right: 10px">预置位 </div>
-    <div>
-      <ElSelect class="pre-select" :model-value="currentPresetToken" size="small" filterable
-        @update:model-value="handleChangeValue" :loading="loading"
-        :disabled="Boolean(!cameraDetailStore.detail?.isPtz)">
-        <ElOption v-for="item in presetOptions" :key="item.token" :label="item.name" :value="item.token">
-          <span style="float: left">{{ item.name }}</span>
-          <span style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
-            <!-- 点击删除的时候,阻止选中菜单 -->
-            <el-icon @click.stop="handleDeletePreset(item.token)">
-              <CircleCloseFilled />
-            </el-icon>
-          </span>
-        </ElOption>
-      </ElSelect>
-    </div>
-  </div>
-</template>
-<script lang="ts" setup>
-import { ElSelect, ElOption, ElMessage, ElMessageBox, ElLoading } from 'element-plus';
-import { CircleCloseFilled } from '@element-plus/icons-vue';
-import usePresetListStore from '../../store/usePresetListStore';
-import { storeToRefs } from 'pinia';
-import { deletePresetApi, goToPresetApi } from '@/api/camera/camera-preview';
-import useCameraDetailStore from '../../store/useCameraDetailStore';
-import useFenceStore from '../../store/useFenceStore';
-import useCameraAlgoStore from '../../store/useCameraAlgoStore';
-
-const presetListStore = usePresetListStore();
-const cameraAlgoStore = useCameraAlgoStore();
-
-const cameraDetailStore = useCameraDetailStore();
-
-const { data: presetOptions, currentPresetToken, loading } = storeToRefs(presetListStore);
-
-const fenceStore = useFenceStore();
-
-const handleDeletePreset = (token: string) => {
-  ElMessageBox({
-    message:
-      '该预置位可能存在关联的电子围栏。删除该预置位将会删除对应的电子围栏信息,请确认是否删除',
-    title: '删除确认',
-    showCancelButton: true,
-    confirmButtonText: '确认删除',
-    beforeClose: (action, instance, done) => {
-      if (action === 'confirm') {
-        instance.confirmButtonLoading = true;
-        deletePresetApi({ presetToken: token, cameraId: cameraDetailStore.cameraId })
-          .then((res) => {
-            ElMessage.success('删除成功!');
-            presetListStore.getPresetList(cameraDetailStore.cameraId);
-            currentPresetToken.value = presetOptions.value?.[0].token || '';
-            done();
-          })
-          .finally(() => {
-            instance.confirmButtonLoading = true;
-          });
-      } else {
-        done();
-      }
-    },
-  });
-};
-
-const handleChangeValue = (val) => {
-  currentPresetToken.value = val;
-  goToPresetApi({ presetToken: val, cameraId: cameraDetailStore.cameraId });
-  fenceStore.getFence({
-    presetToken: val,
-    algoId: cameraAlgoStore.selectedAlgoId!,
-    cameraId: cameraDetailStore.cameraId,
-  });
-};
-</script>
-<style scoped>
-.presetSetting {
-  display: flex;
-  align-items: center;
-  height: 34px;
-}
-
-.pre-select {
-  width: 145px;
-}
-
-.circular {
-  display: inline;
-  height: 30px;
-  width: 30px;
-  animation: loading-rotate 2s linear infinite;
-}
-
-.path {
-  animation: loading-dash 1.5s ease-in-out infinite;
-  stroke-dasharray: 90, 150;
-  stroke-dashoffset: 0;
-  stroke-width: 2;
-  stroke: var(--el-color-primary);
-  stroke-linecap: round;
-}
-</style>
+<template>
+  <div class="presetSetting">
+    <div style="margin-right: 10px">预置位 </div>
+    <div>
+      <ElSelect class="pre-select" :model-value="currentPresetToken" size="small" filterable
+        @update:model-value="handleChangeValue" :loading="loading"
+        :disabled="Boolean(!cameraDetailStore.detail?.isPtz)">
+        <ElOption v-for="item in presetOptions" :key="item.token" :label="item.name" :value="item.token">
+          <span style="float: left">{{ item.name }}</span>
+          <span style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
+            <!-- 点击删除的时候,阻止选中菜单 -->
+            <el-icon @click.stop="handleDeletePreset(item.token)">
+              <CircleCloseFilled />
+            </el-icon>
+          </span>
+        </ElOption>
+      </ElSelect>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ElSelect, ElOption, ElMessage, ElMessageBox, ElLoading } from 'element-plus';
+import { CircleCloseFilled } from '@element-plus/icons-vue';
+import usePresetListStore from '../../store/usePresetListStore';
+import { storeToRefs } from 'pinia';
+import { deletePresetApi, goToPresetApi } from '@/api/camera/camera-preview';
+import useCameraDetailStore from '../../store/useCameraDetailStore';
+import useFenceStore from '../../store/useFenceStore';
+import useCameraAlgoStore from '../../store/useCameraAlgoStore';
+
+const presetListStore = usePresetListStore();
+const cameraAlgoStore = useCameraAlgoStore();
+
+const cameraDetailStore = useCameraDetailStore();
+
+const { data: presetOptions, currentPresetToken, loading } = storeToRefs(presetListStore);
+
+const fenceStore = useFenceStore();
+
+const handleDeletePreset = (token: string) => {
+  ElMessageBox({
+    message:
+      '该预置位可能存在关联的电子围栏。删除该预置位将会删除对应的电子围栏信息,请确认是否删除',
+    title: '删除确认',
+    showCancelButton: true,
+    confirmButtonText: '确认删除',
+    beforeClose: (action, instance, done) => {
+      if (action === 'confirm') {
+        instance.confirmButtonLoading = true;
+        deletePresetApi({ presetToken: token, cameraId: cameraDetailStore.cameraId })
+          .then((res) => {
+            ElMessage.success('删除成功!');
+            presetListStore.getPresetList(cameraDetailStore.cameraId);
+            currentPresetToken.value = presetOptions.value?.[0].token || '';
+            done();
+          })
+          .finally(() => {
+            instance.confirmButtonLoading = true;
+          });
+      } else {
+        done();
+      }
+    },
+  });
+};
+
+const handleChangeValue = (val) => {
+  currentPresetToken.value = val;
+  goToPresetApi({ presetToken: val, cameraId: cameraDetailStore.cameraId });
+  fenceStore.getFence({
+    presetToken: val,
+    algoId: cameraAlgoStore.selectedAlgoId!,
+    cameraId: cameraDetailStore.cameraId,
+  });
+};
+</script>
+<style scoped>
+.presetSetting {
+  display: flex;
+  align-items: center;
+  height: 34px;
+}
+
+.pre-select {
+  width: 145px;
+}
+
+.circular {
+  display: inline;
+  height: 30px;
+  width: 30px;
+  animation: loading-rotate 2s linear infinite;
+}
+
+.path {
+  animation: loading-dash 1.5s ease-in-out infinite;
+  stroke-dasharray: 90, 150;
+  stroke-dashoffset: 0;
+  stroke-width: 2;
+  stroke: var(--el-color-primary);
+  stroke-linecap: round;
+}
+</style>

src/views/cameras/preview/components/RenderSwitch/RenderSwitch.vue → src/views/cameras/algo-params-setting/components/RenderSwitch/RenderSwitch.vue


+ 22 - 22
src/views/cameras/preview/components/ToolbarIcon/ToolbarIcon.vue

@@ -1,22 +1,22 @@
-<template>
-  <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 }>();
-</script>
-<style scoped>
-  .toolbarIcon {
-    width: 24px;
-    height: 24px;
-    padding: 4px;
-    cursor: pointer;
-    margin-right: 20px;
-    &.active {
-      background: #f0f2f5;
-    }
-  }
-</style>
+<template>
+  <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 }>();
+</script>
+<style scoped>
+  .toolbarIcon {
+    width: 24px;
+    height: 24px;
+    padding: 4px;
+    cursor: pointer;
+    margin-right: 20px;
+    &.active {
+      background: #f0f2f5;
+    }
+  }
+</style>

+ 26 - 26
src/views/cameras/preview/components/ViewWindowSetting/ViewWindowSetting.vue

@@ -1,26 +1,26 @@
-<template>
-  <!-- 先不展示 -->
-  <div style="display: flex" v-if="false">
-    <ToolbarIcon
-      :src="window1"
-      :active="props.modelValue === ViewType.window1"
-      @click="emits('update:modelValue', ViewType.window1)"
-    />
-    <ToolbarIcon
-      :src="window4"
-      :active="props.modelValue === ViewType.window4"
-      @click="emits('update:modelValue', ViewType.window4)"
-    />
-  </div>
-</template>
-<script lang="ts" setup>
-  import ToolbarIcon from '../ToolbarIcon/ToolbarIcon.vue';
-  import window1 from '@/assets/images/camera/window1.png';
-  import window4 from '@/assets/images/camera/window4.png';
-  import { ViewType } from './types';
-
-  const props = defineProps<{ modelValue: ViewType }>();
-
-  const emits = defineEmits<{ (e: 'update:modelValue', val: ViewType): unknown }>();
-</script>
-<style scoped></style>
+<template>
+  <!-- 先不展示 -->
+  <div style="display: flex" v-if="false">
+    <ToolbarIcon
+      :src="window1"
+      :active="props.modelValue === ViewType.window1"
+      @click="emits('update:modelValue', ViewType.window1)"
+    />
+    <ToolbarIcon
+      :src="window4"
+      :active="props.modelValue === ViewType.window4"
+      @click="emits('update:modelValue', ViewType.window4)"
+    />
+  </div>
+</template>
+<script lang="ts" setup>
+  import ToolbarIcon from '../ToolbarIcon/ToolbarIcon.vue';
+  import window1 from '@/assets/images/camera/window1.png';
+  import window4 from '@/assets/images/camera/window4.png';
+  import { ViewType } from './types';
+
+  const props = defineProps<{ modelValue: ViewType }>();
+
+  const emits = defineEmits<{ (e: 'update:modelValue', val: ViewType): unknown }>();
+</script>
+<style scoped></style>

+ 4 - 4
src/views/cameras/preview/components/ViewWindowSetting/types.ts

@@ -1,4 +1,4 @@
-export enum ViewType {
-  window1,
-  window4,
-}
+export enum ViewType {
+  window1,
+  window4,
+}

+ 43 - 0
src/views/cameras/algo-params-setting/hooks/useParamsSettingFn.ts

@@ -0,0 +1,43 @@
+import { FENCE_ENBALED_STATUS, updateCameraAlgoApi } from '@/api/camera/camera-preview';
+import { ElMessage } from 'element-plus';
+import useCameraAlgoStore from '../store/useCameraAlgoStore';
+import useCameraDetailStore from '../store/useCameraDetailStore';
+import useFenceStore from '../store/useFenceStore';
+import usePresetListStore from '../store/usePresetListStore';
+
+/** 各种store相关的方法都可以提取到这个hooks中 */
+export const useParamsSettingFn = () => {
+  const fenceStore = useFenceStore();
+
+  const presetStore = usePresetListStore();
+  const cameraDetailStore = useCameraDetailStore();
+  const cameraAlgoStore = useCameraAlgoStore();
+
+  /** 切换电子围栏的开启关闭状态  */
+  const toggleFenceStatus = (nextStatus: boolean) => {
+    const cameraId = cameraDetailStore.cameraId;
+    const algoId = cameraAlgoStore.selectedAlgoId;
+    if (!algoId) return;
+
+    const status = nextStatus ? FENCE_ENBALED_STATUS.enabled : FENCE_ENBALED_STATUS.disabled;
+    const params = {
+      cameraId: cameraId,
+      id: cameraAlgoStore.selectedAlgoDetail?.id!,
+      algoId,
+      electronicFence: status,
+    };
+
+    updateCameraAlgoApi(params).then(() => {
+      ElMessage.success(nextStatus ? '电子围栏已开启' : '电子围栏已关闭');
+      cameraAlgoStore.cameraAlgoList?.forEach((item) => {
+        if (item.algoId === algoId) {
+          item.electronicFence = status;
+        }
+      });
+    });
+  };
+
+  return { toggleFenceStatus };
+};
+
+export default useParamsSettingFn;

+ 102 - 137
src/views/cameras/preview/store/useCameraAlgoStore.ts

@@ -1,137 +1,102 @@
-import { getAllAlgosApi, getCameraAlgoListApi, CameraAlgoItem } from '@/api/camera/camera-preview';
-import { defineStore } from 'pinia';
-import { computed, ref } from 'vue';
-import { useRequest } from 'vue-hooks-plus';
-import { TimePeriodItem } from '../components/AlgorithmsSetting/types';
-import { DetectionJSON } from '../components/AlgorithmsSetting/utils';
-
-interface CameraAlgoItemInCard extends CameraAlgoItem {
-  // detectionJSON: DetectionJSON;
-  inferCode: string;
-  enableCardBool: boolean;
-  electronicFenceBool: boolean;
-  regionJudge: number;
-  timeRangeArr: TimePeriodItem[];
-  metaValues: AlgoParamMetaItem[];
-  judge: number; // 0-小于 1-大于 2-等于
-  detectionFrequency: number;
-  eventDurationMinMs: number;
-  eventDurationMinFrames: number;
-  eventAlarmIntervalMs: number;
-  eventAlarmIntervalFrames: number;
-  timeWindow?: number;
-}
-
-export interface AlgoParamMetaItem {
-  id: string;
-  label: string;
-  criticalCount: number;
-  confidence: number;
-  min_width: number;
-  max_width: number;
-  nextObjs: AlgoParamMetaItem[];
-}
-
-const defaultSelectedAlgoDetail = {
-  // detectionJSON: { detectionNum: 0, detectionUnit: 1 },
-  regionJudge: 0,
-};
-
-const useCameraAlgoStore = defineStore('cameraAlgo', () => {
-  const {
-    data: cameraAlgoList,
-    runAsync: getCameraAlgoList,
-    mutate: mutateCameraAlgoList,
-  } = useRequest(
-    (cameraId: number) => {
-      return getCameraAlgoListApi(cameraId);
-    },
-    { manual: true },
-  );
-
-  // 选中的算法id列表
-  const selectedAlgoList = ref<number[]>([]);
-
-  const {
-    data: allAlgoList,
-    runAsync: getAllAlgoList,
-    mutate: mutateAllAlgoList,
-  } = useRequest(getAllAlgosApi, {
-    manual: true,
-  });
-
-  // 标记的paramCard集合
-  const markedParamCardIds = ref<string[]>([]);
-
-  // 标记的timeRange集合
-  const markedTimeRangeIds = ref<string[]>([]);
-
-  /** 所有算法列表中选定的算法id */
-  const selectedAlgoId = ref<number | null | undefined>();
-
-  const selectedAlgoDetail = ref<CameraAlgoItemInCard>({
-    ...defaultSelectedAlgoDetail,
-  } as CameraAlgoItemInCard);
-
-  //计算原始模板数据
-  const metaObjList = computed(() => {
-    const extra = selectedAlgoDetail.value.algoInfo?.extra;
-    if (!extra) return [];
-    const extraObj = JSON.parse(extra);
-    const params = extraObj?.inferParams;
-    if (!params || (params && params.length == 0)) return [];
-    const metaObjs = params[0]?.metaObjs;
-
-    return metaObjs ? metaObjs : [];
-  });
-
-  const getAlgoDetail = (algoId: number): null | CameraAlgoItem => {
-    const detail = cameraAlgoList.value?.find((x) => x.algoId === algoId);
-    if (!detail) return null;
-    return detail;
-  };
-
-  /** 算法是否已经绑定到相机 */
-  const isAlgoBind = (algoId: number) => {
-    return cameraAlgoList.value?.find((x) => x.algoId === algoId);
-  };
-
-  const deleteParam = (id: string) => {
-    selectedAlgoDetail.value.metaValues = selectedAlgoDetail.value.metaValues.filter(
-      (x) => x.id !== id,
-    );
-  };
-
-  const deleteTimeRange = (id: string) => {
-    selectedAlgoDetail.value.timeRangeArr = selectedAlgoDetail.value.timeRangeArr.filter(
-      (item) => item.id !== id,
-    );
-  };
-
-  const clear = () => {
-    mutateCameraAlgoList();
-    mutateAllAlgoList();
-    selectedAlgoDetail.value = { ...defaultSelectedAlgoDetail } as CameraAlgoItemInCard;
-    selectedAlgoList.value = [];
-  };
-
-  return {
-    cameraAlgoList,
-    getCameraAlgoList,
-    selectedAlgoId,
-    selectedAlgoList,
-    metaObjList,
-    allAlgoList,
-    markedParamCardIds,
-    markedTimeRangeIds,
-    getAllAlgoList,
-    getAlgoDetail,
-    selectedAlgoDetail,
-    isAlgoBind,
-    clear,
-    deleteParam,
-    deleteTimeRange,
-  };
-});
-
-export default useCameraAlgoStore;
+import { getAllAlgosApi, getCameraAlgoListApi, CameraAlgoItem } from '@/api/camera/camera-preview';
+import { defineStore } from 'pinia';
+import { computed, ref } from 'vue';
+import { useRequest } from 'vue-hooks-plus';
+import { TimePeriodItem } from '@/modules/algo/algo-params-edit/types';
+
+interface CameraAlgoItemInCard extends CameraAlgoItem {
+  // detectionJSON: DetectionJSON;
+  inferCode: string;
+  enableCardBool: boolean;
+  electronicFenceBool: boolean;
+  regionJudge: number;
+  timeRangeArr: TimePeriodItem[];
+  metaValues: AlgoParamMetaItem[];
+  judge: number; // 0-小于 1-大于 2-等于
+  detectionFrequency: number;
+  eventDurationMinMs: number;
+  eventDurationMinFrames: number;
+  eventAlarmIntervalMs: number;
+  eventAlarmIntervalFrames: number;
+  timeWindow?: number;
+}
+
+export interface AlgoParamMetaItem {
+  id: string;
+  label: string;
+  criticalCount: number;
+  confidence: number;
+  min_width: number;
+  max_width: number;
+  nextObjs: AlgoParamMetaItem[];
+}
+
+const defaultSelectedAlgoDetail = {
+  // detectionJSON: { detectionNum: 0, detectionUnit: 1 },
+  regionJudge: 0,
+};
+
+const useCameraAlgoStore = defineStore('cameraAlgo', () => {
+  const {
+    data: cameraAlgoList,
+    runAsync: getCameraAlgoList,
+    mutate: mutateCameraAlgoList,
+  } = useRequest(
+    (cameraId: number) => {
+      return getCameraAlgoListApi(cameraId);
+    },
+    { manual: true },
+  );
+
+  // 选中的算法id列表
+  const selectedAlgoList = ref<number[]>([]);
+
+  const {
+    data: allAlgoList,
+    runAsync: getAllAlgoList,
+    mutate: mutateAllAlgoList,
+  } = useRequest(getAllAlgosApi, {
+    manual: true,
+  });
+
+  /** 所有算法列表中选定的算法id */
+  const selectedAlgoId = ref<number | null | undefined>();
+
+  const selectedAlgoDetail = ref<CameraAlgoItemInCard>({
+    ...defaultSelectedAlgoDetail,
+  } as CameraAlgoItemInCard);
+
+  const getAlgoDetail = (algoId: number): null | CameraAlgoItem => {
+    const detail = cameraAlgoList.value?.find((x) => x.algoId === algoId);
+    if (!detail) return null;
+    return detail;
+  };
+
+  /** 算法是否已经绑定到相机 */
+  const isAlgoBind = (algoId: number) => {
+    return cameraAlgoList.value?.find((x) => x.algoId === algoId);
+  };
+
+  const clear = () => {
+    mutateCameraAlgoList();
+    mutateAllAlgoList();
+    selectedAlgoDetail.value = { ...defaultSelectedAlgoDetail } as CameraAlgoItemInCard;
+    selectedAlgoList.value = [];
+  };
+
+  return {
+    cameraAlgoList,
+    getCameraAlgoList,
+    selectedAlgoId,
+    selectedAlgoList,
+    allAlgoList,
+
+    getAllAlgoList,
+    getAlgoDetail,
+    selectedAlgoDetail,
+    isAlgoBind,
+    clear,
+  };
+});
+
+export default useCameraAlgoStore;

+ 61 - 61
src/views/cameras/preview/store/useCameraDetailStore.ts

@@ -1,61 +1,61 @@
-/** 相机详情的store */
-import { CameraDetailServer } from '@/types/camera/type';
-import { FenceDisplayStatus } from '@/types/camera/constant';
-import { useRouteQuery } from '@vueuse/router';
-import dayjs from 'dayjs';
-import { defineStore } from 'pinia';
-import { ref } from 'vue';
-import { VideoResolution } from '../components/CameraParams/types';
-
-interface CameraParams {
-  startAt: string;
-  endAt: string;
-  resolution: number;
-  period: number;
-}
-
-/** 宽/长的比例 */
-export const WIDTH_HEIGHT_RATIO = 0.5625;
-/** 分辨率以1920为基础 */
-export const BASE_RESOLUTION = 1920;
-
-const defaultParams: CameraParams = {
-  startAt: '',
-  endAt: '',
-  resolution: VideoResolution.HIGH_RESOLUTION,
-  period: 30,
-};
-
-const formatDateTime = (time: string) => {
-  const dayStr = dayjs().format('YYYY-MM-DD');
-  return dayStr + ' ' + time;
-};
-
-const useCameraDetailStore = defineStore('cameraDetail', () => {
-  const cameraId = useRouteQuery('cameraId', '', { transform: (str) => Number(str) });
-  const isShowFence = ref<boolean>(false);
-
-  const detail = ref<CameraDetailServer | null>(null);
-
-  /** 参数设置 */
-  const params = ref<CameraParams>({ ...defaultParams });
-
-  const setDetail = (newDetail: CameraDetailServer) => {
-    detail.value = newDetail;
-    params.value = {
-      startAt: formatDateTime(newDetail.nvrStartAt || '00:00:00'),
-      endAt: formatDateTime(newDetail.nvrEndAt || '23:59:59'),
-      resolution: newDetail.resolution || VideoResolution.HIGH_RESOLUTION,
-      period: newDetail.nvrPeriod || 30,
-    };
-    isShowFence.value = newDetail.isDisplayFence === FenceDisplayStatus.enabled ? true : false;
-  };
-
-  const clear = () => {
-    detail.value = null;
-    params.value = { ...defaultParams };
-  };
-  return { detail, setDetail, cameraId, params, clear, isShowFence };
-});
-
-export default useCameraDetailStore;
+/** 相机详情的store */
+import { CameraDetailServer } from '@/types/camera/type';
+import { FenceDisplayStatus } from '@/types/camera/constant';
+import { useRouteQuery } from '@vueuse/router';
+import dayjs from 'dayjs';
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import { VideoResolution } from '../components/CameraParams/types';
+
+interface CameraParams {
+  startAt: string;
+  endAt: string;
+  resolution: number;
+  period: number;
+}
+
+/** 宽/长的比例 */
+export const WIDTH_HEIGHT_RATIO = 0.5625;
+/** 分辨率以1920为基础 */
+export const BASE_RESOLUTION = 1920;
+
+const defaultParams: CameraParams = {
+  startAt: '',
+  endAt: '',
+  resolution: VideoResolution.HIGH_RESOLUTION,
+  period: 30,
+};
+
+const formatDateTime = (time: string) => {
+  const dayStr = dayjs().format('YYYY-MM-DD');
+  return dayStr + ' ' + time;
+};
+
+const useCameraDetailStore = defineStore('cameraDetail', () => {
+  const cameraId = useRouteQuery('cameraId', '', { transform: (str) => Number(str) });
+  const isShowFence = ref<boolean>(false);
+
+  const detail = ref<CameraDetailServer | null>(null);
+
+  /** 参数设置 */
+  const params = ref<CameraParams>({ ...defaultParams });
+
+  const setDetail = (newDetail: CameraDetailServer) => {
+    detail.value = newDetail;
+    params.value = {
+      startAt: formatDateTime(newDetail.nvrStartAt || '00:00:00'),
+      endAt: formatDateTime(newDetail.nvrEndAt || '23:59:59'),
+      resolution: newDetail.resolution || VideoResolution.HIGH_RESOLUTION,
+      period: newDetail.nvrPeriod || 30,
+    };
+    isShowFence.value = newDetail.isDisplayFence === FenceDisplayStatus.enabled ? true : false;
+  };
+
+  const clear = () => {
+    detail.value = null;
+    params.value = { ...defaultParams };
+  };
+  return { detail, setDetail, cameraId, params, clear, isShowFence };
+});
+
+export default useCameraDetailStore;

src/views/cameras/preview/store/useCameraStatus.ts → src/views/cameras/algo-params-setting/store/useCameraStatus.ts


+ 97 - 0
src/views/cameras/algo-params-setting/store/useFenceStore.ts

@@ -0,0 +1,97 @@
+import {
+  GetFenceParams,
+  getFenceApi,
+  saveFenceApi,
+  SaveFenceParams,
+  editFenceApi,
+  UpdateFenceParams,
+  deleteFenceApi,
+  DeleteFenceParams,
+} from '@/api/camera/camera-preview';
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import { ServerLine, ServerLineInfos } from '../components/FenceEditor/constants';
+import safeParse from '@/utils/safeParse';
+
+/** 当前电子围栏的store */
+export const useFenceStore = defineStore('fencePolygonStore', () => {
+  /** 当前相机-预置位-算法对应的所有的电子围栏 */
+  const allFences = ref<ServerLineInfos>([]);
+  /** 当前正在操作的电子围栏id */
+  const currentFenceId = ref<number | null>(null);
+
+  /** 电子围栏的分组id */
+  const currentFenceGroupId = ref<number | null>(null);
+
+  const loading = ref(false);
+
+  // 是否展示电子围栏的工具栏
+  const showFenceTool = ref(false);
+
+  /** 获取电子围栏 */
+  const getFence = (param: GetFenceParams) => {
+    loading.value = true;
+    if (!param.algoId || !param.cameraId || !param.presetToken) return;
+    return getFenceApi(param)
+      .then((res) => {
+        const fence = res.electronicFence ? (JSON.parse(res.electronicFence) as []) : ([] as { polygon: string }[]);
+        // const pointsJSON = points.poly
+        const newFence = fence.map((x) => {
+          return { ...x, polygon: safeParse(x.polygon) as ServerLine };
+        }) as unknown as ServerLineInfos;
+        allFences.value = newFence;
+        currentFenceGroupId.value = res.id;
+      })
+      .catch(() => {
+        currentFenceId.value = null;
+        allFences.value = [];
+      })
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  /** 新建电子围栏 */
+  const createFence = (param: SaveFenceParams) => {
+    // 还需要把名字加上。
+
+    return saveFenceApi({ fenceName: '电子围栏', ...param }).then((res) => {
+      console.log('save success', res);
+      currentFenceId.value = res;
+      getFence(param);
+    });
+  };
+
+  /** 修改电子围栏信息 */
+  const editFence = (param: UpdateFenceParams) => {
+    return editFenceApi(param).then(() => getFence(param));
+  };
+
+  const deleteFence = (param: DeleteFenceParams) => {
+    const { fenceId } = param;
+    if (currentFenceId.value === fenceId) {
+      currentFenceId.value = null;
+    }
+    allFences.value = allFences.value.filter((x) => x.id !== fenceId);
+    return deleteFenceApi(param);
+  };
+
+  const clear = () => {
+    allFences.value = [];
+    currentFenceId.value = null;
+  };
+
+  return {
+    allFences,
+    currentFenceId,
+    getFence,
+    createFence,
+    editFence,
+    deleteFence,
+    clear,
+    showFenceTool,
+    currentFenceGroupId,
+  };
+});
+
+export default useFenceStore;

+ 30 - 30
src/views/cameras/preview/store/usePresetListStore.ts

@@ -1,30 +1,30 @@
-import { getPresetListApi } from '@/api/camera/camera-preview';
-import { defineStore } from 'pinia';
-import { ref } from 'vue';
-import { useRequest } from 'vue-hooks-plus';
-
-/** 当前电子围栏的store */
-export const usePresetListStore = defineStore('presetListStore', () => {
-  /** 当前选中的预置位 */
-  const currentPresetToken = ref('');
-
-  const { data, loading, runAsync, mutate } = useRequest(
-    (cameraId: number) => {
-      return getPresetListApi(cameraId);
-    },
-    { manual: true },
-  );
-
-  const isPresetNameExist = (name: string) => {
-    return data.value?.find((x) => x.name === name);
-  };
-
-  const clear = () => {
-    mutate();
-    currentPresetToken.value = '';
-  };
-
-  return { data, currentPresetToken, getPresetList: runAsync, isPresetNameExist, loading, clear };
-});
-
-export default usePresetListStore;
+import { getPresetListApi } from '@/api/camera/camera-preview';
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import { useRequest } from 'vue-hooks-plus';
+
+/** 当前电子围栏的store */
+export const usePresetListStore = defineStore('presetListStore', () => {
+  /** 当前选中的预置位 */
+  const currentPresetToken = ref('');
+
+  const { data, loading, runAsync, mutate } = useRequest(
+    (cameraId: number) => {
+      return getPresetListApi(cameraId);
+    },
+    { manual: true },
+  );
+
+  const isPresetNameExist = (name: string) => {
+    return data.value?.find((x) => x.name === name);
+  };
+
+  const clear = () => {
+    mutate();
+    currentPresetToken.value = '';
+  };
+
+  return { data, currentPresetToken, getPresetList: runAsync, isPresetNameExist, loading, clear };
+});
+
+export default usePresetListStore;

+ 42 - 115
src/views/cameras/preview/CameraPreview.vue

@@ -1,131 +1,58 @@
 <template>
-  <div>
-    <div class="cameraMain">
-      <div class="cameraTree" v-show="cameraTreeVisible">
-        <CameraTreeCom />
-      </div>
-      <div v-if="cameraTreeVisible" class="arrow-icon" @click="cameraTreeVisible = false"
-        ><el-icon><DArrowLeft /></el-icon
-      ></div>
-      <div v-else class="arrow-icon" @click="cameraTreeVisible = true"
-        ><el-icon><DArrowRight /></el-icon
-      ></div>
-      <div class="cameraSettingWrapper">
-        <div class="cameraView">
-          <CameraViewSetting v-if="cameraDetailStore.cameraId" />
-          <div class="cameraPlaceholder" v-else>请选择左侧相机</div>
-        </div>
-      </div>
+  <div class="container-box">
+    <div class="tabs">
+      <div class="tab" :class="{ active: activeName === 'single' }" @click="activeName = 'single'"> 单相机配置 </div>
+      <div class="tab" :class="{ active: activeName === 'group' }" @click="activeName = 'group'"> 组相机配置 </div>
     </div>
+    <component :is="currentComponent" />
   </div>
 </template>
 
 <script lang="ts" setup>
-  import { onMounted, ref, watch } from 'vue';
-  import { ElIcon } from 'element-plus';
-  import { DArrowLeft, DArrowRight } from '@element-plus/icons-vue';
-  import { storeToRefs } from 'pinia';
-  import CameraTreeCom from './components/CameraTree/CameraTree.vue';
-  import CameraViewSetting from './components/CameraViewSetting/CameraViewSetting.vue';
-  import useCameraDetailStore from './store/useCameraDetailStore';
-  import useCameraAlgoStore from './store/useCameraAlgoStore';
-  import usePresetListStore from './store/usePresetListStore';
-  import useFenceStore from './store/useFenceStore';
-  import { IsPtz } from '@/types/camera/constant';
-  import { getCameraDeatilById } from '@/api/camera/camera-preview';
+  import { computed, ref } from 'vue';
+  import CameraConfigSingle from './components/CameraConfigSingle/CameraConfigSingle.vue';
+  import CameraConfigGroup from './components/CameraConfigGroup/CameraConfigGroup.vue';
 
-  const cameraDetailStore = useCameraDetailStore();
-  const { isShowFence } = storeToRefs(cameraDetailStore);
-  const cameraAlgoStore = useCameraAlgoStore();
-  const fenceStore = useFenceStore();
-  const presetListStore = usePresetListStore();
-
-  const cameraTreeVisible = ref(true);
-
-  watch(
-    () => cameraDetailStore.cameraId,
-    async (cameraId) => {
-      isShowFence.value = false;
-      fenceStore.clear();
-      if (cameraId) {
-        // FIXME: 缺 后端v4 api
-        getCameraDeatilById(cameraId).then(async (res) => {
-          cameraDetailStore.setDetail(res);
-          // 如果isPtz为null,或者为0,都按照枪击相机
-          if (res.isPtz === IsPtz.disabled || !res.isPtz) {
-            const presetList = await presetListStore.getPresetList(cameraId);
-            presetListStore.currentPresetToken = presetList?.[0].token;
-          }
-        });
-
-        cameraAlgoStore.getCameraAlgoList(cameraId);
-        cameraAlgoStore.selectedAlgoId = null;
-      }
-    },
-    {
-      immediate: true,
-    },
-  );
-
-  onMounted(() => {
-    cameraAlgoStore.getAllAlgoList();
+  const activeName = ref('single');
+  const currentComponent = computed(() => {
+    return activeName.value === 'single' ? CameraConfigSingle : CameraConfigGroup;
   });
 </script>
 
 <style lang="scss" scoped>
-  .cameraParamsSetting {
-    width: 350px;
-    min-height: 300px;
-    // border: 1px solid #ccc;
+  .container-box {
+    width: 100%;
+    height: 100%;
+    min-height: calc(100vh - 90px);
+    padding: 20px 20px 10px 20px;
+    background-color: rgba(255, 255, 255, 1);
+    border-radius: 10px;
   }
 
-  .cameraParamsSetting {
-    width: 350px;
-    min-height: 300px;
-    // border: 1px solid #ccc;
-  }
-
-  .algorithmsSetting {
-    flex: 1;
-    border-left: 1px solid #ccc;
-    padding-left: 20px;
-  }
-  .cameraMain {
+  .tabs {
     display: flex;
-    background: #fff;
-    // height: calc(100vh - 90px);
-  }
-  .cameraTree {
-    min-width: 270px;
-    max-width: 600px;
-    flex-shrink: 0;
-    // height: 800px;
-    // border: 1px solid #ccc;
-    border: 1px solid #f0f2f5;
-    margin: 5px;
-  }
-
-  .cameraPlaceholder {
-    color: #333;
-    text-align: center;
-    margin-top: 100px;
-    margin-left: 100px;
-  }
-
-  .arrow-icon {
-    width: 16px;
-    height: 48px;
-    margin: 320px 0;
-    border-radius: 15px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    cursor: pointer;
-    background-color: #bee2ff;
-  }
-
-  .arrow-icon:hover {
-    color: #fff;
-    background-color: #0052d9;
+    margin-bottom: 20px;
+
+    .tab {
+      width: 125px;
+      height: 40px;
+      border: 1px solid #d9dce3;
+      border-radius: 5px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      margin-right: 20px;
+      font-size: 15px;
+      letter-spacing: 1px;
+      cursor: pointer;
+    }
+
+    .tab.active,
+    .tab:hover {
+      color: #fff;
+      background-color: #409eff;
+      border: none;
+      font-weight: bold;
+    }
   }
 </style>

+ 0 - 408
src/views/cameras/preview/components/AlgorithmsSetting/AlgoPeriodCard copy.vue

@@ -1,408 +0,0 @@
-<template>
-  <div class="periodCard">
-    <div class="dayRrange">
-      <el-select class="daySelect" v-model="startDay" placeholder="开始日期">
-        <el-option
-          v-for="item in dayOptions"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value"
-          :disabled="endDay && item.value > endDay"
-        >
-        </el-option>
-      </el-select>
-      <div class="divider">-</div>
-      <el-select class="daySelect" v-model="endDay" placeholder="结束日期">
-        <el-option
-          v-for="item in dayOptions"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value"
-          :disabled="startDay && item.value < startDay"
-        >
-        </el-option>
-      </el-select>
-    </div>
-    <div class="timeRange" v-for="(item, index) in timeRangeList">
-      <el-time-picker
-        v-model="item.startTime"
-        format="HH:mm"
-        value-format="HH:mm"
-        :teleported="false"
-        :editable="true"
-        placeholder="开始时间"
-        :disabled-hours="() => disabledHours(index, false)"
-        :disabled-minutes="(hour:number)=>disabledMinutes(hour, index, false)"
-      />
-      <div class="divider">-</div>
-      <el-time-picker
-        v-model="item.endTime"
-        format="HH:mm"
-        value-format="HH:mm"
-        :teleported="false"
-        :editable="true"
-        placeholder="结束时间"
-        :disabled-hours="() => disabledHours(index, true)"
-        :disabled-minutes="(hour:number)=>disabledMinutes(hour, index, true)"
-      />
-      <div v-if="index == timeRangeList.length - 1" class="timeAdd" @click="handleAddTimeRange">
-        <el-icon color="#d0d0d0"><Plus /></el-icon>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-  import { ref } from 'vue';
-  import { TimePeriodItem } from './types';
-  import { Plus } from '@element-plus/icons-vue';
-
-  const props = defineProps<{ periodData: TimePeriodItem }>();
-
-  const dayOptions = [
-    {
-      value: 1,
-      label: '周一',
-    },
-    {
-      value: 2,
-      label: '周二',
-    },
-    {
-      value: 3,
-      label: '周三',
-    },
-    {
-      value: 4,
-      label: '周四',
-    },
-    {
-      value: 5,
-      label: '周五',
-    },
-    {
-      value: 6,
-      label: '周六',
-    },
-    {
-      value: 7,
-      label: '周日',
-    },
-  ];
-
-  const makeRange = (start: number, end: number): number[] => {
-    let result: number[] = [];
-    for (let i = start; i <= end; i++) {
-      result.push(i);
-    }
-    return result;
-  };
-
-  const startDay = ref(props.periodData.startDay);
-  const endDay = ref(props.periodData.endDay);
-  const timeRangeList = ref<{ startTime: Date | string; endTime: Date | string }[]>(
-    props.periodData.timeRangeList,
-  );
-
-  const handleAddTimeRange = () => {
-    if (!timeRangeList.value.find((item) => item.startTime === '' || item.endTime === '')) {
-      timeRangeList.value.push({ startTime: '', endTime: '' });
-      console.log(timeRangeList.value);
-    }
-  };
-
-  const disabledHours = (index: number, isEnd: boolean) => {
-    const newList = timeRangeList.value.filter(
-      (item, i) => item.startTime && item.endTime && i !== index,
-    );
-    let result: number[] = [];
-    // 选开始时间,结束时间被约束
-    if (!isEnd && timeRangeList.value[index].endTime) {
-      const curEnd = timeRangeList.value[index].endTime;
-      const cureh = Number(String(curEnd).split(':')[0]);
-      const curem = Number(String(curEnd).split(':')[1]);
-      if (newList.length == 0) {
-        if (curem == 0) {
-          result = result.concat(makeRange(cureh, 23));
-        } else {
-          result = result.concat(makeRange(getH(cureh + 1), 23));
-        }
-      }
-      for (let i = 0; i < newList.length; i++) {
-        if (compareHM(newList[i].startTime, curEnd)) {
-          if (i == 0) {
-            if (curem == 0) {
-              result = result.concat(makeRange(cureh, 23));
-              break;
-            } else {
-              result = result.concat(makeRange(getH(cureh + 1), 23));
-              break;
-            }
-          } else {
-            let sh = 0;
-            const lastEnd = newList[i - 1].endTime;
-            const lasteh = Number(String(lastEnd).split(':')[0]);
-            const lastem = Number(String(lastEnd).split(':')[1]);
-            if (lastem == 59) {
-              sh = lasteh;
-            } else {
-              sh = getH(lasteh - 1);
-            }
-            let eh = 0;
-            if (curem == 0) {
-              eh = cureh;
-            } else {
-              eh = getH(cureh + 1);
-            }
-            result = result.concat(makeRange(0, sh)).concat(makeRange(eh, 23));
-            break;
-          }
-        }
-        if (i == newList.length - 1) {
-          let sh = 0;
-          const lastEnd = newList[i].endTime;
-          const lasteh = Number(String(lastEnd).split(':')[0]);
-          const lastem = Number(String(lastEnd).split(':')[1]);
-          if (lastem == 59) {
-            sh = lasteh;
-          } else {
-            sh = getH(lasteh - 1);
-          }
-          let eh = 0;
-          if (curem == 0) {
-            eh = cureh;
-          } else {
-            eh = getH(cureh + 1);
-          }
-          result = result.concat(makeRange(0, sh)).concat(makeRange(eh, 23));
-        }
-      }
-    }
-    // 选结束时间,开始时间被约束
-    if (isEnd && timeRangeList.value[index].startTime) {
-      const curStart = timeRangeList.value[index].startTime;
-      const cursh = Number(String(curStart).split(':')[0]);
-      const cursm = Number(String(curStart).split(':')[1]);
-      if (newList.length == 0) {
-        if (cursm == 59) {
-          result = result.concat(makeRange(0, cursh));
-        } else {
-          result = result.concat(makeRange(0, getH(cursh - 1)));
-        }
-      }
-      for (let i = 0; i < newList.length; i++) {
-        if (compareHM(curStart, newList[i].endTime)) {
-          if (i == newList.length - 1) {
-            if (cursm == 59) {
-              result = result.concat(makeRange(0, cursh));
-              break;
-            } else {
-              result = result.concat(makeRange(0, getH(cursh - 1)));
-              break;
-            }
-          } else {
-            let sh = 0;
-            if (cursm == 59) {
-              sh = cursh;
-            } else {
-              sh = getH(cursh - 1);
-            }
-            let eh = 0;
-            const nextStart = newList[i + 1].startTime;
-            const nextsh = Number(String(nextStart).split(':')[0]);
-            const nextsm = Number(String(nextStart).split(':')[1]);
-            if (nextsm == 0) {
-              eh = nextsh;
-            } else {
-              eh = getH(nextsh + 1);
-            }
-            result = result.concat(makeRange(0, sh)).concat(makeRange(eh, 23));
-            break;
-          }
-        }
-        if (i == newList.length - 1) {
-          let sh = 0;
-          if (cursm == 59) {
-            sh = cursh;
-          } else {
-            sh = getH(cursh - 1);
-          }
-          let eh = 0;
-          const nextStart = newList[0].startTime;
-          const nextsh = Number(String(nextStart).split(':')[0]);
-          const nextsm = Number(String(nextStart).split(':')[1]);
-          if (nextsm == 0) {
-            eh = nextsh;
-          } else {
-            eh = getH(nextsh + 1);
-          }
-          result = result.concat(makeRange(0, sh)).concat(makeRange(eh, 23));
-        }
-      }
-    }
-    // 另一个时间为空
-    if (
-      (!isEnd && !timeRangeList.value[index].endTime) ||
-      (isEnd && !timeRangeList.value[index].startTime)
-    ) {
-      for (let i = 0; i < newList.length; i++) {
-        const timeRange = newList[i];
-        const s = timeRange.startTime;
-        const e = timeRange.endTime;
-        const sh = Number(String(s).split(':')[0]);
-        const sm = Number(String(s).split(':')[1]);
-        const eh = Number(String(e).split(':')[0]);
-        const em = Number(String(e).split(':')[1]);
-        if (eh - sh > 1) {
-          result = result.concat(makeRange(getH(sh + 1), getH(eh - 1)));
-          if (sm == 0) result.push(sh);
-          if (em == 59) result.push(eh);
-        }
-        if (sm == 0 && em == 59) result.push(sh);
-      }
-    }
-
-    return result;
-  };
-
-  const disabledMinutes = (hour: number, index: number, isEnd: boolean) => {
-    const newList = timeRangeList.value.filter(
-      (item, i) => item.startTime && item.endTime && i !== index,
-    );
-    let result: number[] = [];
-    for (let i = 0; i < newList.length; i++) {
-      const timeRange = newList[i];
-      const s = timeRange.startTime;
-      const e = timeRange.endTime;
-      const sh = Number(String(s).split(':')[0]);
-      const sm = Number(String(s).split(':')[1]);
-      const eh = Number(String(e).split(':')[0]);
-      const em = Number(String(e).split(':')[1]);
-      if (hour == sh) {
-        if (sh == eh) {
-          result = result.concat(makeRange(sm, em));
-        } else {
-          result = result.concat(makeRange(sm, 59));
-        }
-        continue;
-      }
-      if (hour == eh) {
-        result = result.concat(makeRange(0, em));
-      }
-    }
-    // 选开始时间,结束时间被约束
-    if (!isEnd && timeRangeList.value[index].endTime) {
-      const curEnd = timeRangeList.value[index].endTime;
-      const cureh = Number(String(curEnd).split(':')[0]);
-      const curem = Number(String(curEnd).split(':')[1]);
-      if (hour === cureh) {
-        result = result.concat(makeRange(curem, 59));
-      }
-    }
-    // 选结束时间,开始时间被约束
-    if (isEnd && timeRangeList.value[index].startTime) {
-      const curStart = timeRangeList.value[index].startTime;
-      const cursh = Number(String(curStart).split(':')[0]);
-      const cursm = Number(String(curStart).split(':')[1]);
-      if (hour === cursh) {
-        result = result.concat(makeRange(0, cursm));
-      }
-    }
-
-    return result;
-  };
-
-  const getH = (h) => {
-    if (h < 0) {
-      return 0;
-    } else if (h > 23) {
-      return 23;
-    } else {
-      return h;
-    }
-  };
-
-  const getM = (m) => {
-    if (m < 0) {
-      return 0;
-    } else if (m > 59) {
-      return 23;
-    } else {
-      return m;
-    }
-  };
-
-  const compareHM = (s, e) => {
-    const sh = Number(String(s).split(':')[0]);
-    const sm = Number(String(s).split(':')[1]);
-    const eh = Number(String(e).split(':')[0]);
-    const em = Number(String(e).split(':')[1]);
-
-    if (sh === eh) {
-      return sm - em > 0 ? true : false;
-    } else if (sh > eh) {
-      return true;
-    } else {
-      return false;
-    }
-  };
-</script>
-
-<style scoped lang="scss">
-  .periodCard {
-    width: 294px;
-    max-height: 174px;
-    background: #0000000a;
-    border-radius: 5px;
-    border: 1px solid #e8ecf2;
-    padding: 3px;
-    margin: 0 5px 5px 5px;
-  }
-
-  .dayRrange {
-    display: flex;
-    align-items: center;
-  }
-
-  .timeRange {
-    display: flex;
-    align-items: center;
-  }
-
-  .divider {
-    color: #00000040;
-  }
-
-  .daySelect {
-    width: 112px;
-    margin: 5px;
-  }
-
-  .timeAdd {
-    width: 28px;
-    height: 28px;
-    border-radius: 50%;
-    background: #ebebeb;
-    border: 1px dashed #00000026;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-  }
-
-  :deep(.el-date-editor) {
-    width: 112px;
-    margin: 5px;
-
-    .el-input__prefix {
-      order: 1;
-    }
-
-    .el-input__icon {
-      margin: 0;
-    }
-  }
-
-  :deep(.el-time-panel__footer) {
-    display: none;
-  }
-</style>

+ 0 - 349
src/views/cameras/preview/components/AlgorithmsSetting/AlgorithmsSetting.vue

@@ -1,349 +0,0 @@
-<template>
-  <div id="algoSetting">
-    <div style="font-size: 14px; font-weight: bold">算法开关</div>
-    <div>
-      <div class="algoTagWrapper">
-        <AlgoAddBtn @click="showDialog" v-if="hasAddPermission()" />
-        <AddAlgoDialog v-if="algoDialogVisible" @close="closeDialog" />
-
-        <!-- <AlgoTag
-          v-for="item in cameraAlgoList"
-          :key="item.code"
-          :label="item.algoInfo?.name"
-          :algo-id="item.algoInfo.id"
-          :is-active="item.algoId === selectedAlgoId"
-          @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, item)"
-          @toggle-algo="confirmToggleAlgoOpen(item, $event)"
-          :is-fence-open="item.electronicFence === FENCE_ENBALED_STATUS.enabled"
-          @toggle-fence="confirmToggleFence(item, $event)"
-          @toggle-setting="handleToggleSetting(item.algoId)"
-        />
-      </div>
-      <div>
-        <AlgoSettingCard
-          @on-submit="handleSubmit"
-          @on-cancel="handleCancel"
-          v-if="selectedAlgoId && algoSettingIsOpen"
-        />
-        <!-- <div style="color: #ccc; margin-top: 20px" v-else>请选择算法</div> -->
-      </div>
-    </div>
-  </div>
-</template>
-<script lang="ts" setup>
-  import useCameraAlgoStore from '../../store/useCameraAlgoStore';
-  import AlgoSettingCard from './AlgoSettingCard.vue';
-  import { storeToRefs } from 'pinia';
-  import {
-    deleteCameraAlgoApi,
-    updateCameraAlgoApi,
-    FENCE_ENBALED_STATUS,
-    CameraAlgoItem,
-    updateCameraAlgoStatusApi,
-    updateCameraAlgoRelStatus,
-  } from '@/api/camera/camera-preview';
-  import { ElMessage, ElMessageBox } from 'element-plus';
-  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 {
-    getInferCode,
-    getAlgoType,
-    getMetaValues,
-    getExtraCommonInfo,
-    getDetectionTime,
-    getInferParam,
-  } from './utils';
-  import { ALGO_ENABLED_STATUS } from '@/api/camera/camera-preview';
-  import { ref, watchEffect } from 'vue';
-  import { useUserStore } from '@/store/modules/user';
-  import { PERM_ALGO } from '@/types/permission/constants';
-  import AlgoAddBtn from '../AlgoSwitchCard/AlgoAddBtn.vue';
-
-  const cameraAlgoStore = useCameraAlgoStore();
-  const fenceStore = useFenceStore();
-  const presetStore = usePresetListStore();
-
-  const { getCameraAlgoList, getAlgoDetail } = cameraAlgoStore;
-  const { cameraAlgoList, selectedAlgoId, selectedAlgoDetail } = storeToRefs(cameraAlgoStore);
-  const cameraDetailStore = useCameraDetailStore();
-  const userStore = useUserStore();
-
-  const hasAddPermission = () => userStore.checkPermission(PERM_ALGO.CONFIG_ADD);
-  const algoSettingIsOpen = ref(false);
-  const algoDialogVisible = ref(false);
-
-  const handleToggleSetting = (algoId: number) => {
-    // 如果是在当前选中的卡片上切换设置开关,那么反选即可
-    if (selectedAlgoId.value === algoId) {
-      algoSettingIsOpen.value = !algoSettingIsOpen.value;
-      return;
-    }
-    // 如果是在其他卡片上切换设置开关,等同于直接切卡片,并且显示设置。
-    // 如果原先设置开关是打开的,那么要先alert提示,否则就直接切
-    if (selectedAlgoId.value !== algoId && algoSettingIsOpen.value) {
-      confirmSwitchAlgo().then(() => {
-        selectedAlgoId.value = algoId;
-        algoSettingIsOpen.value = true;
-      });
-    } else {
-      selectedAlgoId.value = algoId;
-      algoSettingIsOpen.value = true;
-    }
-  };
-
-  const handleSelectAlgo = (algoId: number) => {
-    if (selectedAlgoId.value === algoId) {
-      return;
-    }
-    if (algoSettingIsOpen.value) {
-      confirmSwitchAlgo()
-        .then(() => {
-          selectedAlgoId.value = algoId;
-          algoSettingIsOpen.value = false;
-        })
-        .catch(() => {});
-    } else {
-      selectedAlgoId.value = algoId;
-      algoSettingIsOpen.value = false;
-    }
-  };
-
-  watchEffect(() => {
-    const algoId = selectedAlgoId.value;
-    if (!algoId) return;
-    const detail = getAlgoDetail(algoId);
-    if (!detail) return;
-    // console.log('detail change', detail);
-    // const detectionJSON = getDetectionJSON(detail?.detectionFrequency);
-    const enableCard = detail?.status === ALGO_ENABLED_STATUS.enabled ? true : false;
-    const electronicFenceBool = detail?.electronicFence === FENCE_ENBALED_STATUS.enabled ? true : false;
-
-    // const timeRangeArr = getDetectionTimeJSON(detail?.detectionTime) || [];
-    const timeRangeArr = getDetectionTime(detail?.detectionTime) || [];
-    const metaValues = getMetaValues(detail?.extra) || [];
-
-    const commonInfo = getExtraCommonInfo(detail);
-
-    selectedAlgoDetail.value = {
-      ...detail,
-      inferCode: getInferCode(detail?.extra),
-      // detectionJSON,
-      enableCardBool: enableCard,
-      electronicFenceBool,
-      timeRangeArr,
-      metaValues,
-      regionJudge: commonInfo.regionJudge || 0,
-      judge: commonInfo.judge || commonInfo.judge == 0 ? commonInfo.judge : 1,
-      eventDurationMinMs:
-        commonInfo.eventDurationMinMs || commonInfo.eventDurationMinMs == 0 ? commonInfo.eventDurationMinMs : 1,
-      eventDurationMinFrames:
-        commonInfo.eventDurationMinFrames || commonInfo.eventDurationMinFrames == 0
-          ? commonInfo.eventDurationMinFrames
-          : 1,
-      eventAlarmIntervalMs:
-        commonInfo.eventAlarmIntervalMs || commonInfo.eventAlarmIntervalMs == 0 ? commonInfo.eventAlarmIntervalMs : 1,
-      eventAlarmIntervalFrames:
-        commonInfo.eventAlarmIntervalFrames || commonInfo.eventAlarmIntervalFrames == 0
-          ? commonInfo.eventAlarmIntervalFrames
-          : 1,
-    };
-    fenceStore.getFence({
-      algoId: algoId,
-      cameraId: cameraDetailStore.cameraId,
-      presetToken: presetStore.currentPresetToken,
-    });
-
-    //是否有窗口时间
-    if (commonInfo.timeWindow) {
-      selectedAlgoDetail.value.timeWindow = commonInfo.timeWindow;
-    }
-  });
-
-  const confirmToggleAlgoOpen = (detail: CameraAlgoItem, algoStatus: boolean) => {
-    if (detail.algoId !== selectedAlgoId.value && algoSettingIsOpen.value) {
-      confirmSwitchAlgo().then(() => {
-        handleToggleAlgoOpen(detail, algoStatus);
-      });
-    } else {
-      handleToggleAlgoOpen(detail, algoStatus);
-    }
-  };
-
-  const handleToggleAlgoOpen = (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;
-    selectedAlgoId.value = algoId;
-    algoSettingIsOpen.value = false;
-    updateCameraAlgoRelStatus(params)
-      .then(() => {
-        ElMessage.success(algoStatus ? '算法已开启' : '算法已关闭');
-      })
-      .catch(() => {
-        detail.status = initialStatus;
-      });
-  };
-
-  const confirmToggleFence = (detail: CameraAlgoItem, fenceStatus: boolean) => {
-    if (detail.algoId !== selectedAlgoId.value && algoSettingIsOpen.value) {
-      confirmSwitchAlgo().then(() => {
-        handleToggleFence(detail, fenceStatus);
-      });
-    } else {
-      handleToggleFence(detail, fenceStatus);
-    }
-  };
-
-  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;
-    selectedAlgoId.value = algoId;
-    algoSettingIsOpen.value = false;
-    updateCameraAlgoApi(params)
-      .then(() => {
-        ElMessage.success(fenceStatus ? '电子围栏已开启' : '电子围栏已关闭');
-      })
-      .catch(() => {
-        detail.electronicFence = initialStatus;
-      });
-  };
-
-  const handleSubmit = (param) => {
-    console.log('submitParam', param);
-    const cameraId = cameraDetailStore.cameraId;
-    const inferParams = getInferParam(selectedAlgoDetail.value.extra);
-    inferParams.metaObjs = param.metaObjs;
-    inferParams.regionJudge = param.regionJudge;
-    inferParams.criticalCounts = param.criticalCounts;
-    inferParams.judge = param.judge;
-    inferParams.eventDurationMinMs = param.eventDurationMinMs;
-    inferParams.eventDurationMinFrames = param.eventDurationMinFrames;
-    inferParams.eventAlarmIntervalMs = param.eventAlarmIntervalMs;
-    inferParams.eventAlarmIntervalFrames = param.eventAlarmIntervalFrames;
-    inferParams.algoCode = selectedAlgoDetail.value.algoInfo.code;
-    inferParams.algoType = getAlgoType(selectedAlgoDetail.value.algoInfo.extra);
-    if (param.timeWindow) {
-      inferParams.timeWindow = param.timeWindow;
-    }
-    const extraValue = {
-      inferCode: param.inferCode,
-      inferParams: [inferParams],
-    } as any;
-    const newParam = {
-      cameraId: cameraId,
-      electronicFence: param.electronicFence,
-      algoId: param.algoId,
-      detectionFrequency: param.detectionFrequency,
-      detectionTime: param.detectionTime,
-      status: param.status,
-      extra: JSON.stringify(extraValue),
-    };
-    if (param.id) {
-      if (param.isSwitch) {
-        updateCameraAlgoStatusApi({ ...newParam, id: param.id }).then(() => {
-          ElMessage.success('更新成功');
-          getCameraAlgoList(cameraId);
-          selectedAlgoId.value = undefined;
-          algoSettingIsOpen.value = false;
-        });
-      } else {
-        updateCameraAlgoApi({ ...newParam, id: param.id }).then(() => {
-          ElMessage.success('更新成功');
-          getCameraAlgoList(cameraId);
-          selectedAlgoId.value = undefined;
-          algoSettingIsOpen.value = false;
-        });
-      }
-    }
-  };
-
-  const confirmRemove = (algoId: number, item) => {
-    ElMessageBox.confirm(`请确定是否删除【${item.algoInfo.name}】算法?`, '提示', {
-      confirmButtonText: '确定',
-      cancelButtonText: '取消',
-      type: 'warning',
-    }).then(() => {
-      handleRemove(algoId);
-    });
-  };
-
-  const confirmSwitchAlgo = () => {
-    return ElMessageBox.confirm('<strong>确认切换算法吗?</strong><br />切换后未保存的算法配置将被丢弃。', '', {
-      confirmButtonText: '确定',
-      cancelButtonText: '取消',
-      type: 'warning',
-      dangerouslyUseHTMLString: true,
-    });
-  };
-
-  const handleRemove = (algoId: number) => {
-    deleteCameraAlgoApi({ algoId, cameraId: cameraDetailStore.cameraId }).then(() => {
-      ElMessage.success('删除成功');
-      getCameraAlgoList(cameraDetailStore.cameraId);
-      selectedAlgoId.value = undefined;
-    });
-  };
-
-  const handleCancel = (algoId: number) => {
-    if (selectedAlgoId.value !== algoId) {
-      return;
-    }
-    selectedAlgoId.value = undefined;
-  };
-
-  const showDialog = () => {
-    algoDialogVisible.value = true;
-  };
-
-  const closeDialog = () => {
-    algoDialogVisible.value = false;
-  };
-</script>
-<style scoped>
-  .algoTagWrapper {
-    min-width: 150px;
-    margin-right: 15px;
-    display: flex;
-    flex-wrap: wrap;
-  }
-
-  :deep(.el-message-box__status.el-icon) {
-    top: 0 !important;
-    transform: none !important;
-  }
-</style>

+ 7 - 0
src/views/cameras/preview/components/CameraConfigGroup/CameraConfigGroup.vue

@@ -0,0 +1,7 @@
+<template>
+  <div> 组相机配置 </div>
+</template>
+
+<script lang="ts" setup></script>
+
+<style lang="scss" scoped></style>

+ 291 - 0
src/views/cameras/preview/components/CameraConfigSingle/CameraConfigSingle.vue

@@ -0,0 +1,291 @@
+<template>
+  <div class="query-box">
+    <div class="query-param">
+      <el-input
+        v-model="tableQueryTypeContent"
+        style="width: 300px; height: 32px; margin-right: 40px; margin-bottom: 10px"
+        :placeholder="tableQueryType ? '请输入' + tableQueryType : '请输入查找内容'"
+        clearable
+        :disabled="!tableQueryType"
+        @input="handleTableQueryTypeContentChange"
+      >
+        <template #prepend>
+          <el-select
+            v-model="tableQueryType"
+            placeholder="选择类型"
+            style="width: 110px"
+            @change="handleTableQueryTypeChange"
+            clearable
+          >
+            <el-option v-for="item in tableQueryTypeOptions" :key="item.value" :label="item.name" :value="item.value" />
+          </el-select>
+        </template>
+      </el-input>
+      <div class="locations-query">
+        <div>地点:</div>
+        <el-cascader
+          v-model="workLocation"
+          :options="locationOptions"
+          :props="locationProp"
+          clearable
+          collapse-tags
+          :show-all-levels="false"
+          placeholder="请选择地点"
+          popper-class="special-cascader"
+          @change="handleCascaderChange"
+        />
+      </div>
+      <div class="algo-query">
+        <div>算法:</div>
+        <el-select v-model="tableQueryParams.queryParam.algoId" placeholder="请选择算法" clearable style="width: 200px">
+          <el-option v-for="item in algoOptions" :key="item.value" :label="item.name" :value="item.value" />
+        </el-select>
+      </div>
+    </div>
+    <div class="query-btn">
+      <el-button type="primary" @click="submitTableQuery">查询</el-button>
+      <el-button @click="resetTable">重置</el-button>
+    </div>
+  </div>
+  <div class="table-box">
+    <el-table ref="multipleTableRef" :data="tableData" style="width: 100%" height="100%">
+      <el-table-column type="selection" width="30" />
+      <el-table-column label="相机名称" prop="cameraName" width="180" />
+      <el-table-column label="设备ID" prop="cameraCode" width="180" />
+      <el-table-column label="地点" prop="location" width="280">
+        <template #default="{ row }">
+          {{ row.workshopName + ' - ' + row.workspaceName }}
+        </template>
+      </el-table-column>
+      <el-table-column label="算法" prop="algoStatusList" min-width="200">
+        <template #default="{ row }">
+          <div v-for="item in row.algoStatusList" :class="item.isDisabled ? 'close-algo' : ''">{{ item.algoName }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="渲染" prop="isRenderDisabled" width="100">
+        <template #default="{ row }">
+          <el-switch :model-value="!row.isRenderDisabled" @change="handleChangeRenderStatus(row)" @click.stop />
+        </template>
+      </el-table-column>
+      <el-table-column label="算法操作" fixed="right" width="300">
+        <template #default="{ row }">
+          <el-button type="primary" text @click="handleSettingConfig(row)">配置</el-button>
+          <el-button type="primary" text @click="batchOpenAlgos(row)">一键开启</el-button>
+          <el-button type="primary" text @click="batchCloseAlgos(row)">一键关闭</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+  <Pagination
+    v-model:page="tableQueryParams.pageNumber"
+    v-model:size="tableQueryParams.pageSize"
+    :total="total"
+    @update:page="handlePageChange"
+    @update:size="handleSizeChange"
+    style="margin-bottom: 0"
+  />
+</template>
+
+<script lang="ts" setup>
+  import { onMounted, ref } from 'vue';
+  import {
+    ElSelect,
+    ElOption,
+    ElInput,
+    ElCascader,
+    ElButton,
+    ElTable,
+    ElTableColumn,
+    ElSwitch,
+    ElMessageBox,
+  } from 'element-plus';
+  import { useWorkLocation } from '@/views/datamanager/alertformdata/hooks/useWorkLocation';
+  import {
+    QueryCameraPageByAlgoParams,
+    QueryCameraPageByAlgoRes,
+    getCameraListByAlgo,
+    updateAlgosStatusByCameraId,
+    updateAlgosStatusByBatch,
+    getAlgosInfo,
+  } from '@/api/camera/camera-config';
+  import { renderCamera } from '@/api/camera/camera-preview';
+  import Pagination from '@/components/Pagination/Pagination.vue';
+  import { mockQueryCameraPageByAlgoRes } from './mockData';
+
+  const { locationOptions, getLocationOptions } = useWorkLocation();
+
+  const tableQueryParams = ref<QueryCameraPageByAlgoParams>({
+    pageNumber: 1,
+    pageSize: 10,
+    queryParam: {},
+  });
+
+  const tableData = ref<QueryCameraPageByAlgoRes[]>([]);
+  // const tableData = ref(mockQueryCameraPageByAlgoRes); // Mock data for testing
+
+  const tableQueryTypeOptions = [
+    { name: '相机名称', value: '相机名称' },
+    { name: '相机ID', value: '相机ID' },
+  ];
+  const algoOptions = ref<{ name: string; value: number }[]>([]); // 算法选择器选项
+  const tableQueryType = ref<string>('');
+  const tableQueryTypeContent = ref<string>('');
+  const workLocation = ref([]); // 级联选择器,为二维数组(提取workspaceId)
+  const locationProp = { multiple: true, expandTrigger: 'hover' as const }; // 级联选择器(打开多选)
+  // 分页
+  const total = ref(0); // 总条数
+
+  const handleTableQueryTypeChange = () => {
+    if (tableQueryType.value === '相机名称') {
+      delete tableQueryParams.value.queryParam.cameraCode;
+    } else if (tableQueryType.value === '相机ID') {
+      delete tableQueryParams.value.queryParam.cameraName;
+    } else {
+      delete tableQueryParams.value.queryParam.cameraCode;
+      delete tableQueryParams.value.queryParam.cameraName;
+    }
+    tableQueryTypeContent.value = '';
+  };
+  const handleTableQueryTypeContentChange = () => {
+    if (tableQueryType.value === '相机名称') {
+      tableQueryParams.value.queryParam.cameraName = tableQueryTypeContent.value;
+    } else if (tableQueryType.value === '相机ID') {
+      tableQueryParams.value.queryParam.cameraCode = tableQueryTypeContent.value;
+    }
+  };
+  const handleCascaderChange = () => {
+    if (workLocation.value.length !== 0) {
+      const arr = [];
+      workLocation.value.forEach((item) => {
+        arr.push(item[1]);
+      });
+      tableQueryParams.value.queryParam.workspaceIdList = arr;
+    } else {
+      Reflect.deleteProperty(tableQueryParams.value.queryParam, 'workspaceIdList');
+    }
+  };
+
+  const submitTableQuery = () => {
+    getTableData();
+  };
+
+  const resetTable = () => {
+    tableQueryType.value = '';
+    tableQueryTypeContent.value = '';
+    workLocation.value = [];
+    tableQueryParams.value.pageNumber = 1;
+    tableQueryParams.value.pageSize = 10;
+    tableQueryParams.value.queryParam = {};
+    getTableData();
+  };
+
+  // 换页,重新获取表格
+  const handlePageChange = (val) => {
+    tableQueryParams.value.pageNumber = val;
+    getTableData();
+  };
+  const handleSizeChange = (val) => {
+    tableQueryParams.value.pageSize = val;
+    getTableData();
+  };
+
+  // 切换渲染状态
+  const handleChangeRenderStatus = (row) => {
+    const tempRenderStatus = row.isRenderDisabled ? 'demo' : 'null';
+    console.log('切换渲染状态', tempRenderStatus);
+    renderCamera({
+      render: tempRenderStatus,
+      cameraId: row.cameraId,
+    })
+      .then(() => {
+        console.log('渲染开启成功');
+        getTableData();
+      })
+      .catch(() => {
+        console.log('渲染开启失败');
+        ElMessageBox.alert('开启数量达到上限,请关闭其他相机渲染后再开启。', '渲染开启失败', {
+          type: 'warning',
+        });
+      });
+  };
+
+  // 配置算法(单相机配置)
+  const handleSettingConfig = (row) => {
+    console.log('配置算法,进入算法设置详情页', row);
+    // TODO: 跳转到算法设置详情页
+  };
+  // 一键开启算法(单相机一键开启)
+  const batchOpenAlgos = (row) => {
+    console.log('开启算法', row);
+    updateAlgosStatusByCameraId({ cameraId: row.cameraId, isDisabled: false }).then(() => {
+      console.log('开启算法成功');
+      getTableData();
+    });
+  };
+  // 一键关闭算法(单相机一键关闭)
+  const batchCloseAlgos = (row) => {
+    console.log('关闭算法', row);
+    updateAlgosStatusByCameraId({ cameraId: row.cameraId, isDisabled: true }).then(() => {
+      console.log('关闭算法成功');
+      getTableData();
+    });
+  };
+
+  const getTableData = async () => {
+    console.log('Query params:', tableQueryParams.value);
+    await getCameraListByAlgo(tableQueryParams.value).then((res) => {
+      console.log('Camera data:', res);
+      tableData.value = res.records;
+      total.value = res.totalRow;
+    });
+  };
+
+  const getAllAlgosInfo = async () => {
+    await getAlgosInfo().then((res) => {
+      algoOptions.value = res.map((item) => ({ name: item.name, value: item.id }));
+    });
+  };
+
+  onMounted(() => {
+    getTableData();
+    getLocationOptions();
+    getAllAlgosInfo();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .query-box {
+    margin-bottom: 10px;
+    display: flex;
+
+    .query-param {
+      display: flex;
+      flex-wrap: wrap;
+      flex-direction: row;
+      align-items: center;
+      flex: 1;
+    }
+
+    .locations-query,
+    .algo-query {
+      display: flex;
+      align-items: center;
+      margin-right: 40px;
+      margin-bottom: 10px;
+    }
+
+    .query-btn {
+      margin-left: auto;
+      margin-bottom: 10px;
+    }
+  }
+
+  .table-box {
+    height: calc(100vh - 280px);
+
+    .close-algo {
+      color: #cccccc;
+      text-decoration: line-through;
+    }
+  }
+</style>

+ 410 - 0
src/views/cameras/preview/components/CameraConfigSingle/mockData.ts

@@ -0,0 +1,410 @@
+export const mockQueryCameraPageByAlgoRes = [
+  {
+    cameraName: 'Camera-1',
+    cameraCode: 'CAM-1',
+    networkingState: 0,
+    integrationState: 1,
+    cameraImgUrl: 'https://example.com/camera1.jpg',
+    workshopName: 'Workshop A',
+    workspaceName: 'Workspace 1',
+    isRenderDisabled: false,
+    algoStatusList: [
+      {
+        algoName: 'Algo Alpha',
+        isDisabled: false,
+      },
+      {
+        algoName: 'Algo Beta',
+        isDisabled: true,
+      },
+      {
+        algoName: 'Algo Alpha2',
+        isDisabled: false,
+      },
+      {
+        algoName: 'Algo Beta2',
+        isDisabled: false,
+      },
+    ],
+  },
+  {
+    cameraName: 'Camera-2',
+    cameraCode: 'CAM-2',
+    networkingState: 1,
+    integrationState: 0,
+    cameraImgUrl: 'https://example.com/camera2.jpg',
+    workshopName: 'Workshop B',
+    workspaceName: 'Workspace 2',
+    isRenderDisabled: true,
+    algoStatusList: [
+      {
+        algoName: 'Algo Gamma',
+        isDisabled: true,
+      },
+      {
+        algoName: 'Algo Delta',
+        isDisabled: false,
+      },
+    ],
+  },
+  {
+    cameraName: 'Camera-3',
+    cameraCode: 'CAM-3',
+    networkingState: 0,
+    integrationState: 1,
+    cameraImgUrl: 'https://example.com/camera3.jpg',
+    workshopName: 'Workshop A',
+    workspaceName: 'Workspace 3',
+    isRenderDisabled: false,
+    algoStatusList: [
+      {
+        algoName: 'Algo Alpha',
+        isDisabled: false,
+      },
+      {
+        algoName: 'Algo Epsilon',
+        isDisabled: true,
+      },
+    ],
+  },
+  {
+    cameraName: 'Camera-4',
+    cameraCode: 'CAM-4',
+    networkingState: 1,
+    integrationState: 0,
+    cameraImgUrl: 'https://example.com/camera4.jpg',
+    workshopName: 'Workshop B',
+    workspaceName: 'Workspace 4',
+    isRenderDisabled: true,
+    algoStatusList: [
+      {
+        algoName: 'Algo Gamma',
+        isDisabled: true,
+      },
+      {
+        algoName: 'Algo Zeta',
+        isDisabled: false,
+      },
+    ],
+  },
+  {
+    cameraName: 'Camera-5',
+    cameraCode: 'CAM-5',
+    networkingState: 0,
+    integrationState: 1,
+    cameraImgUrl: 'https://example.com/camera5.jpg',
+    workshopName: 'Workshop A',
+    workspaceName: 'Workspace 5',
+    isRenderDisabled: false,
+    algoStatusList: [
+      {
+        algoName: 'Algo Alpha',
+        isDisabled: false,
+      },
+      {
+        algoName: 'Algo Eta',
+        isDisabled: true,
+      },
+    ],
+  },
+  // {
+  //   cameraName: 'Camera-6',
+  //   cameraCode: 'CAM-6',
+  //   networkingState: 1,
+  //   integrationState: 0,
+  //   cameraImgUrl: 'https://example.com/camera6.jpg',
+  //   workshopName: 'Workshop B',
+  //   workspaceName: 'Workspace 6',
+  //   isRenderDisabled: true,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Gamma',
+  //       isDisabled: true,
+  //     },
+  //     {
+  //       algoName: 'Algo Theta',
+  //       isDisabled: false,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-7',
+  //   cameraCode: 'CAM-7',
+  //   networkingState: 0,
+  //   integrationState: 1,
+  //   cameraImgUrl: 'https://example.com/camera7.jpg',
+  //   workshopName: 'Workshop A',
+  //   workspaceName: 'Workspace 7',
+  //   isRenderDisabled: false,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Alpha',
+  //       isDisabled: false,
+  //     },
+  //     {
+  //       algoName: 'Algo Iota',
+  //       isDisabled: true,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-8',
+  //   cameraCode: 'CAM-8',
+  //   networkingState: 1,
+  //   integrationState: 0,
+  //   cameraImgUrl: 'https://example.com/camera8.jpg',
+  //   workshopName: 'Workshop B',
+  //   workspaceName: 'Workspace 8',
+  //   isRenderDisabled: true,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Gamma',
+  //       isDisabled: true,
+  //     },
+  //     {
+  //       algoName: 'Algo Kappa',
+  //       isDisabled: false,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-9',
+  //   cameraCode: 'CAM-9',
+  //   networkingState: 0,
+  //   integrationState: 1,
+  //   cameraImgUrl: 'https://example.com/camera9.jpg',
+  //   workshopName: 'Workshop A',
+  //   workspaceName: 'Workspace 9',
+  //   isRenderDisabled: false,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Alpha',
+  //       isDisabled: false,
+  //     },
+  //     {
+  //       algoName: 'Algo Lambda',
+  //       isDisabled: true,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-10',
+  //   cameraCode: 'CAM-10',
+  //   networkingState: 1,
+  //   integrationState: 0,
+  //   cameraImgUrl: 'https://example.com/camera10.jpg',
+  //   workshopName: 'Workshop B',
+  //   workspaceName: 'Workspace 10',
+  //   isRenderDisabled: true,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Gamma',
+  //       isDisabled: true,
+  //     },
+  //     {
+  //       algoName: 'Algo Mu',
+  //       isDisabled: false,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-11',
+  //   cameraCode: 'CAM-11',
+  //   networkingState: 0,
+  //   integrationState: 1,
+  //   cameraImgUrl: 'https://example.com/camera11.jpg',
+  //   workshopName: 'Workshop A',
+  //   workspaceName: 'Workspace 11',
+  //   isRenderDisabled: false,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Alpha',
+  //       isDisabled: false,
+  //     },
+  //     {
+  //       algoName: 'Algo Nu',
+  //       isDisabled: true,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-12',
+  //   cameraCode: 'CAM-12',
+  //   networkingState: 1,
+  //   integrationState: 0,
+  //   cameraImgUrl: 'https://example.com/camera12.jpg',
+  //   workshopName: 'Workshop B',
+  //   workspaceName: 'Workspace 12',
+  //   isRenderDisabled: true,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Gamma',
+  //       isDisabled: true,
+  //     },
+  //     {
+  //       algoName: 'Algo Xi',
+  //       isDisabled: false,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-13',
+  //   cameraCode: 'CAM-13',
+  //   networkingState: 0,
+  //   integrationState: 1,
+  //   cameraImgUrl: 'https://example.com/camera13.jpg',
+  //   workshopName: 'Workshop A',
+  //   workspaceName: 'Workspace 13',
+  //   isRenderDisabled: false,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Alpha',
+  //       isDisabled: false,
+  //     },
+  //     {
+  //       algoName: 'Algo Omicron',
+  //       isDisabled: true,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-14',
+  //   cameraCode: 'CAM-14',
+  //   networkingState: 1,
+  //   integrationState: 0,
+  //   cameraImgUrl: 'https://example.com/camera14.jpg',
+  //   workshopName: 'Workshop B',
+  //   workspaceName: 'Workspace 14',
+  //   isRenderDisabled: true,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Gamma',
+  //       isDisabled: true,
+  //     },
+  //     {
+  //       algoName: 'Algo Pi',
+  //       isDisabled: false,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-15',
+  //   cameraCode: 'CAM-15',
+  //   networkingState: 0,
+  //   integrationState: 1,
+  //   cameraImgUrl: 'https://example.com/camera15.jpg',
+  //   workshopName: 'Workshop A',
+  //   workspaceName: 'Workspace 15',
+  //   isRenderDisabled: false,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Alpha',
+  //       isDisabled: false,
+  //     },
+  //     {
+  //       algoName: 'Algo Rho',
+  //       isDisabled: true,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-16',
+  //   cameraCode: 'CAM-16',
+  //   networkingState: 1,
+  //   integrationState: 0,
+  //   cameraImgUrl: 'https://example.com/camera16.jpg',
+  //   workshopName: 'Workshop B',
+  //   workspaceName: 'Workspace 16',
+  //   isRenderDisabled: true,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Gamma',
+  //       isDisabled: true,
+  //     },
+  //     {
+  //       algoName: 'Algo Sigma',
+  //       isDisabled: false,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-17',
+  //   cameraCode: 'CAM-17',
+  //   networkingState: 0,
+  //   integrationState: 1,
+  //   cameraImgUrl: 'https://example.com/camera17.jpg',
+  //   workshopName: 'Workshop A',
+  //   workspaceName: 'Workspace 17',
+  //   isRenderDisabled: false,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Alpha',
+  //       isDisabled: false,
+  //     },
+  //     {
+  //       algoName: 'Algo Tau',
+  //       isDisabled: true,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-18',
+  //   cameraCode: 'CAM-18',
+  //   networkingState: 1,
+  //   integrationState: 0,
+  //   cameraImgUrl: 'https://example.com/camera18.jpg',
+  //   workshopName: 'Workshop B',
+  //   workspaceName: 'Workspace 18',
+  //   isRenderDisabled: true,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Gamma',
+  //       isDisabled: true,
+  //     },
+  //     {
+  //       algoName: 'Algo Upsilon',
+  //       isDisabled: false,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-19',
+  //   cameraCode: 'CAM-19',
+  //   networkingState: 0,
+  //   integrationState: 1,
+  //   cameraImgUrl: 'https://example.com/camera19.jpg',
+  //   workshopName: 'Workshop A',
+  //   workspaceName: 'Workspace 19',
+  //   isRenderDisabled: false,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Alpha',
+  //       isDisabled: false,
+  //     },
+  //     {
+  //       algoName: 'Algo Phi',
+  //       isDisabled: true,
+  //     },
+  //   ],
+  // },
+  // {
+  //   cameraName: 'Camera-20',
+  //   cameraCode: 'CAM-20',
+  //   networkingState: 1,
+  //   integrationState: 0,
+  //   cameraImgUrl: 'https://example.com/camera20.jpg',
+  //   workshopName: 'Workshop B',
+  //   workspaceName: 'Workspace 20',
+  //   isRenderDisabled: true,
+  //   algoStatusList: [
+  //     {
+  //       algoName: 'Algo Gamma',
+  //       isDisabled: true,
+  //     },
+  //     {
+  //       algoName: 'Algo Chi',
+  //       isDisabled: false,
+  //     },
+  //   ],
+  // },
+];

+ 0 - 41
src/views/cameras/preview/components/FenceEditorV2/utils.ts

@@ -1,41 +0,0 @@
-import { GROUP_NAME, defaultCircleStyle, defaultLineStyle } from './constants';
-import { uid } from 'uid';
-import { FenceGroup } from './types';
-
-export const getCircleConfig = (points: number[], scale: number) => {
-  const circlePoints = [];
-  for (let i = 0; i < points.length - 1; i += 2) {
-    circlePoints.push([points[i], points[i + 1]]);
-  }
-  return circlePoints.map((point, idx) => {
-    return createCircleConfigItem(point as [number, number], idx, scale);
-  });
-};
-
-export const createCircleConfigItem = (point: [number, number], idx: number, scale: number) => {
-  return {
-    ...defaultCircleStyle,
-    radius: defaultCircleStyle.radius / scale,
-    hitStrokeWidth: defaultCircleStyle.hitStrokeWidth / scale,
-    x: point[0],
-    y: point[1],
-    uid: uid(),
-    idx,
-  };
-};
-
-export const createGroupConfig = (points: number[], scale: number): FenceGroup => {
-  const lineConfig = {
-    ...defaultLineStyle,
-    strokeWidth: defaultLineStyle.strokeWidth / scale,
-    points: points,
-  };
-  const circleConfigs = getCircleConfig(points, scale);
-  return {
-    lineConfig,
-    name: GROUP_NAME,
-    circleConfigs,
-    uid: uid(),
-    _temp: { points: [] },
-  };
-};

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

@@ -1,67 +0,0 @@
-<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>
-
-    <ElButton type="primary" size="small" @click="toggleEdit">{{
-      props.isEdit ? '退出编辑' : '编辑电子围栏'
-    }}</ElButton>
-  </div>
-</template>
-<script setup lang="ts">
-  import { defineEmits } from 'vue';
-  import { ElButton } from 'element-plus';
-  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();
-
-  const props = defineProps<{ isEdit: boolean }>();
-
-  const emits = defineEmits<{
-    (e: 'toggleEditable', editState: boolean): unknown;
-    (e: 'toggleRange'): unknown;
-    (e: 'remove'): unknown;
-    (e: 'save'): unknown;
-  }>();
-
-  const toggleEdit = () => {
-    emits('toggleEditable', !props.isEdit);
-  };
-</script>
-
-<style scoped>
-  .toolbar {
-    display: flex;
-
-    align-items: center;
-    z-index: 10;
-    padding: 5px;
-    border-radius: 50px;
-  }
-  .fenceDrawingTip {
-    background: #e8f5ff;
-    border-radius: 6px;
-    border: 1px solid #bae0ff;
-    text-align: center;
-    font-size: 12px;
-    padding: 2px 20px;
-    margin-right: 30px;
-    color: #1890ff;
-  }
-</style>

+ 0 - 76
src/views/cameras/preview/store/useFenceStore.ts

@@ -1,76 +0,0 @@
-import {
-  GetFenceParams,
-  getFenceApi,
-  saveFenceApi,
-  SaveFenceParams,
-  editFenceApi,
-} from '@/api/camera/camera-preview';
-import { defineStore } from 'pinia';
-import { ref } from 'vue';
-import { ServerLines } from '../components/FenceEditor/constants';
-import { ElMessage } from 'element-plus';
-
-/** 当前电子围栏的store */
-export const useFenceStore = defineStore('electronicFencePolygonStore', () => {
-  /** 后端返回的电子围栏点 */
-  const serverFencePoints = ref<ServerLines>([]);
-  /** 当前编辑的电子围栏的点 */
-  const currentFencePoints = ref([]);
-  const currentFenceId = ref<number>();
-  const loading = ref(false);
-
-  /** 获取电子围栏 */
-  const getFence = (param: GetFenceParams) => {
-    loading.value = true;
-    if (!param.algoId || !param.cameraId || !param.presetToken) return;
-    return getFenceApi(param)
-      .then((res) => {
-        currentFenceId.value = res.id;
-        const points = res.electronicFencePolygon
-          ? (JSON.parse(res.electronicFencePolygon) as [])
-          : [];
-        currentFencePoints.value = points;
-        serverFencePoints.value = points;
-      })
-      .catch(() => {
-        currentFenceId.value = undefined;
-        currentFencePoints.value = [];
-        serverFencePoints.value = [];
-      })
-      .finally(() => {
-        loading.value = false;
-      });
-  };
-
-  const saveFence = (param: SaveFenceParams) => {
-    if (!param.cameraId) {
-      ElMessage.error('未选中相机');
-      return;
-    }
-    if (!param.algoId) {
-      ElMessage.error('未选中算法');
-      return;
-    }
-    if (!param.presetToken) {
-      ElMessage.error('未选中预置位');
-      return;
-    }
-    if (currentFenceId.value) {
-      return editFenceApi({ ...param, id: currentFenceId.value });
-    }
-    return saveFenceApi(param).then((res) => {
-      console.log('save success', res);
-      currentFenceId.value = res;
-    });
-  };
-
-  const clear = () => {
-    serverFencePoints.value = [];
-    currentFencePoints.value = [];
-    currentFenceId.value = undefined;
-  };
-
-  return { serverFencePoints, currentFencePoints, currentFenceId, getFence, saveFence, clear };
-});
-
-export default useFenceStore;

+ 1 - 1
src/views/datamanager/alertformdata/components/default-simple/Default.vue

@@ -56,7 +56,7 @@
   import QueryFormSimple from '../common/QueryFormSimple.vue';
   import AlertTableSimple, { DataSourceItem } from '../common/AlertTableSimple.vue';
   import DetailDialog from '../common/DetailDialog.vue';
-  import Pagination from '../common/Pagination.vue';
+  import Pagination from '@/components/Pagination/Pagination.vue';
 
   const { locationOptions, getLocationOptions } = useWorkLocation();
   const { aiMainOptions, getAIMainOptions } = useIssueMainType();

+ 1 - 1
src/views/datamanager/alertformdata/components/default/Default.vue

@@ -101,7 +101,7 @@
   import QueryForm from '../common/QueryForm.vue';
   import AlertTable, { DataSourceItem } from '../common/AlertTable.vue';
   import DetailDialog from '../common/DetailDialog.vue';
-  import Pagination from '../common/Pagination.vue';
+  import Pagination from '@/components/Pagination/Pagination.vue';
 
   const { urlPrefix } = useGlobSetting();
 

+ 1 - 1
src/views/datamanager/alertformdata/components/show/Show.vue

@@ -75,7 +75,7 @@
   import QueryForm from '../common/QueryForm.vue';
   import AlertTable, { DataSourceItem } from '../common/AlertTable.vue';
   import DetailDialog from '../common/DetailDialog.vue';
-  import Pagination from '../common/Pagination.vue';
+  import Pagination from '@/components/Pagination/Pagination.vue';
   import AddDrawer from '../common/AddDrawer.vue';
   import EditDrawer from '../common/EditDrawer.vue';