sunqijun 3 mesi fa
parent
commit
598edd78eb
21 ha cambiato i file con 2019 aggiunte e 59 eliminazioni
  1. 2 1
      src/views/production-safety/implement-safety-duty/components/IssueSafetyResponsibility.vue
  2. 3 3
      src/views/production-safety/implement-safety-duty/create-responsibility-agree.vue
  3. 4 4
      src/views/production-safety/implement-safety-duty/edit-responsibility-agree.vue
  4. 1 0
      src/views/production-safety/implement-safety-duty/non-public-area-responsibilities/add.vue
  5. 104 44
      src/views/production-safety/implement-safety-duty/non-public-list-responsibilities/add.vue
  6. 2 2
      src/views/production-safety/implement-safety-duty/non-public-list-responsibilities/list.vue
  7. 1 1
      src/views/production-safety/implement-safety-duty/sign-agree-dept.vue
  8. 388 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupList/CameraGroup.vue
  9. 200 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupList/CameraGroupList.vue
  10. 198 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupList/CameraListOfGroup.vue
  11. 232 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupList/CameraTreeOfGroupList.vue
  12. 59 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupListAndTree.vue
  13. 258 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/VideosGridBase/CamerasGrid.vue
  14. 334 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/VideosGridBase/ScreenToolbar.vue
  15. 14 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/VideosGridBase/VideosGridBase.vue
  16. 26 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/hooks/parseData.ts
  17. 43 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/hooks/useCameraStatus.ts
  18. 11 3
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/list.vue
  19. 48 1
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/monitor.vue
  20. 65 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/type.ts
  21. 26 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/utils.ts

+ 2 - 1
src/views/production-safety/implement-safety-duty/components/IssueSafetyResponsibility.vue

@@ -20,8 +20,9 @@
       </el-form-item>
       <el-form-item prop="teamName" label="是否推送">
         <el-radio-group v-model="formData.isUrgent">
-          <el-radio :value="1">是</el-radio>
           <el-radio :value="0">否</el-radio>
+
+          <el-radio :value="1">是</el-radio>
         </el-radio-group>
       </el-form-item>
     </el-form>

+ 3 - 3
src/views/production-safety/implement-safety-duty/create-responsibility-agree.vue

@@ -70,8 +70,8 @@
           </el-form-item>
           <el-form-item label="是否推送" prop="isUrgent">
             <el-radio-group v-model="formValue.isUrgent">
-              <el-radio :value="1">是</el-radio>
               <el-radio :value="0">否</el-radio>
+              <el-radio :value="1">是</el-radio>
             </el-radio-group>
           </el-form-item>
         </template>
@@ -110,7 +110,7 @@
     userGroupId: [],
     planStartTime: null,
     planEndTime: null,
-    isUrgent: null,
+    isUrgent: 0,
     executeObject: 0,
     content: '<p></p>',
     attachment: [],
@@ -235,7 +235,7 @@
         userGroupId: [],
         planStartTime: null,
         planEndTime: null,
-        isUrgent: null,
+        isUrgent: 0,
       });
     }
   };

+ 4 - 4
src/views/production-safety/implement-safety-duty/edit-responsibility-agree.vue

@@ -67,8 +67,9 @@
           </el-form-item>
           <el-form-item label="是否推送" prop="isUrgent">
             <el-radio-group v-model="formValue.isUrgent">
-              <el-radio :value="1">是</el-radio>
               <el-radio :value="0">否</el-radio>
+
+              <el-radio :value="1">是</el-radio>
             </el-radio-group>
           </el-form-item>
         </template>
@@ -110,7 +111,7 @@
     userGroupId: [],
     planStartTime: null,
     planEndTime: null,
-    isUrgent: null,
+    isUrgent: 0,
     executeObject: 0,
     content: '<p></p>',
     attachment: [],
@@ -235,7 +236,7 @@
         userGroupId: [],
         planStartTime: null,
         planEndTime: null,
-        isUrgent: null,
+        isUrgent: 0,
       });
     }
   };
@@ -244,7 +245,6 @@
       if (valid) {
         submiting.value = true;
         const attachment = await formatAttachmentList(formValue.attachment);
-        console.log('at:', attachment);
         safetyResponsibilityUpdateSafetyResponsibility({
           ...formValue,
           id: Number(route.query.id),

+ 1 - 0
src/views/production-safety/implement-safety-duty/non-public-area-responsibilities/add.vue

@@ -254,6 +254,7 @@
   const getDeptData = () => {
     getAllDepartments().then((res) => {
       firstLevelDepts.value = formatDeptTree(res);
+      console.log('@res:', res);
     });
   };
 

+ 104 - 44
src/views/production-safety/implement-safety-duty/non-public-list-responsibilities/add.vue

@@ -22,37 +22,41 @@
         </el-form-item>
 
         <el-form-item label="安全责任所/中心" prop="safetyResponsibleCenter">
-          <el-select
-            v-model="formValue.safetyResponsibleCenter"
-            placeholder="请选择"
-            size="large"
+          <el-cascader
+            v-model="formValue.safetyResponsibleCenterId"
             style="width: 50%"
+            size="large"
+            :ref="(el) => (cascaderRef['safetyResponsibleCenter'] = el)"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择安全责任部门"
             filterable
-          >
-            <el-option
-              v-for="item in firstLevelDepts"
-              :key="item.hrIdtOrgId"
-              :label="item.deptName"
-              :value="item.deptName"
-            />
-          </el-select>
+            @change="(val) => handleChangeDept(val, 'safetyResponsibleCenter')"
+          />
         </el-form-item>
 
         <el-form-item label="安全责任所/中心负责人" prop="safetyCenterManager">
           <el-select
+            :disabled="!safetyCenterManagerOptions.length"
             v-model="formValue.safetyCenterManager"
             placeholder="请选择"
             size="large"
             style="width: 50%"
             filterable
-            @change="(val) => syncUserName(val, 'safetyCenterManagerName')"
+            @change="(val) => syncUserName(safetyCenterManagerOptions, val, 'safetyCenterManagerName')"
           >
-            <el-option v-for="item in userOptions" :key="item.id" :label="item.name" :value="item.id" />
+            <el-option
+              v-for="item in safetyCenterManagerOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
           </el-select>
         </el-form-item>
 
         <el-form-item label="安全责任部门" prop="safetyResponsibleDepartment">
-          <el-select
+          <!-- <el-select
             v-model="formValue.safetyResponsibleDepartment"
             placeholder="请选择"
             size="large"
@@ -65,32 +69,46 @@
               :label="item.deptName"
               :value="item.deptName"
             />
-          </el-select>
+          </el-select> -->
+          <el-cascader
+            v-model="formValue.safetyResponsibleDepartmentId"
+            style="width: 50%"
+            size="large"
+            :ref="(el) => (cascaderRef['safetyResponsibleDepartment'] = el)"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择安全责任部门"
+            filterable
+            @change="(val) => handleChangeDept(val, 'safetyResponsibleDepartment')"
+          />
         </el-form-item>
 
         <el-form-item label="安全责任部门负责人" prop="safetyDepartmentManager">
           <el-select
+            :disabled="!userOptions.length"
             v-model="formValue.safetyDepartmentManager"
             placeholder="请选择"
             size="large"
             style="width: 50%"
             filterable
-            @change="(val) => syncUserName(val, 'safetyDepartmentManagerName')"
+            @change="(val) => syncUserName(userOptions, val, 'safetyDepartmentManagerName')"
           >
-            <el-option v-for="item in userOptions" :key="item.id" :label="item.name" :value="item.id" />
+            <el-option v-for="item in userOptions" :key="item.value" :label="item.label" :value="item.value" />
           </el-select>
         </el-form-item>
 
         <el-form-item label="安全具体责任人" prop="safetySpecificPerson">
           <el-select
+            :disabled="!userOptions.length"
             v-model="formValue.safetySpecificPerson"
             placeholder="请选择"
             size="large"
             style="width: 50%"
             filterable
-            @change="handleSpecificPersonChange"
+            @change="(val) => syncUserName(userOptions, val, 'safetySpecificPersonName')"
           >
-            <el-option v-for="item in userOptions" :key="item.id" :label="item.name" :value="item.id" />
+            <el-option v-for="item in userOptions" :key="item.value" :label="item.label" :value="item.value" />
           </el-select>
         </el-form-item>
 
@@ -112,15 +130,12 @@
 </template>
 
 <script lang="ts" setup>
-  import { ref, reactive, onMounted } from 'vue';
+  import { ref, reactive, onMounted, nextTick } from 'vue';
   import { useRouter } from 'vue-router';
   import { ElMessage } from 'element-plus';
   import { getAllDepartments } from '@/api/auth/dept';
   import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
-  import {
-    queryUserPageByUsername,
-    areaCheckListSavaArea,
-  } from '@/api/production-safety/responsibility-implementation';
+  import { areaCheckListSavaArea, queryAvailableUserList } from '@/api/production-safety/responsibility-implementation';
   import { getUserList } from '@/api/system/user-operate';
 
   const router = useRouter();
@@ -129,6 +144,17 @@
 
   const userOptions = ref<any[]>([]);
   const firstLevelDepts = ref<any[]>([]);
+  const cascaderProp = {
+    expandTrigger: 'click',
+    checkStrictly: true,
+    // emitPath: false,
+    value: 'id',
+    label: 'deptName',
+  };
+
+  const cascaderRef = ref({});
+
+  const safetyCenterManagerOptions = ref<any[]>([]);
 
   const formValue = reactive({
     buildingNo: '',
@@ -136,9 +162,11 @@
     floorRoomNo: '',
     nameFunction: '',
     safetyResponsibleCenter: '',
+    safetyResponsibleCenterId: [],
     safetyCenterManager: null as number | null,
     safetyCenterManagerName: '',
     safetyResponsibleDepartment: '',
+    safetyResponsibleDepartmentId: [],
     safetyDepartmentManager: null as number | null,
     safetyDepartmentManagerName: '',
     safetySpecificPerson: null as number | null,
@@ -172,31 +200,68 @@
     });
   };
 
-  const getUserData = () => {
-    getUserList({ pageNumber: 1, pageSize: 200, queryParam: {} }).then((res: any) => {
-      userOptions.value = (res.records || []).map((u: any) => ({
-        id: u.userId || u.id,
-        name: u.realName || u.username,
-      }));
+  const handleChangeDept = (val, prop) => {
+    const cascader = cascaderRef.value?.[prop];
+    const deptInfo = cascader?.getCheckedNodes();
+    formValue[prop] = deptInfo[0].label;
+    formRef.value.validateField(prop);
+    nextTick(() => {
+      handleQueryAvailableUserList(deptInfo[0].label, prop);
     });
   };
 
-  const syncUserName = (id: number, nameField: string) => {
-    const user = userOptions.value.find((u) => u.id === id);
-    if (user) formValue[nameField] = user.name;
+  const handleQueryAvailableUserList = (value, prop) => {
+    switch (prop) {
+      case 'safetyResponsibleCenter':
+        formValue.safetyCenterManager = null;
+        getUserData(safetyCenterManagerOptions, value);
+        break;
+      case 'safetyResponsibleDepartment':
+        formValue.safetySpecificPerson = null;
+        formValue.safetyDepartmentManager = null;
+        getUserData(userOptions, value);
+        break;
+      default:
+        break;
+    }
+  };
+
+  const getUserData = (optionList, deptName, realname = '') => {
+    queryAvailableUserList({
+      pageNumber: 1,
+      pageSize: 200,
+      queryParam: {
+        deptName,
+        realname,
+      },
+    }).then((res: any) => {
+      optionList['value'] = (res.records || []).map((u: any) => ({
+        value: u.id,
+        label: u.realname,
+      }));
+    });
   };
 
-  const handleSpecificPersonChange = (id: number) => {
-    const user = userOptions.value.find((u) => u.id === id);
+  const syncUserName = (optionList, id: number, nameField: string) => {
+    console.log('opt:', optionList, id);
+    const user = optionList?.find((u) => u.value === id);
     if (user) {
-      formValue.safetySpecificPersonName = user.name;
-      formValue.safetyPersonContact = user.mobile;
+      formValue[nameField] = user.value;
+      console.log(formValue[nameField]);
     }
   };
 
+  // const handleSpecificPersonChange = (id: number) => {
+  //   const user = userOptions.value.find((u) => u.id === id);
+  //   if (user) {
+  //     formValue.safetySpecificPersonName = user.name;
+  //     formValue.safetyPersonContact = user.mobile;
+  //   }
+  // };
+
   onMounted(() => {
     getDeptData();
-    getUserData();
+    // getUserData();
   });
 
   const handleSubmit = () => {
@@ -227,11 +292,6 @@
     border-radius: 4px;
     margin-right: 20px;
     overflow: hidden;
-
-    // :deep(.w-e-text-container) {
-    //   min-height: 400px;
-    //   overflow-y: auto;
-    // }
   }
   // :deep(.breadcrumb .title) {
   //   margin-left: 0;

+ 2 - 2
src/views/production-safety/implement-safety-duty/non-public-list-responsibilities/list.vue

@@ -45,7 +45,7 @@
       <div class="table-content">
         <el-table :data="tableData.data">
           <el-table-column type="index" label="序号" width="80" />
-          <el-table-column label="楼号" prop="buildingCode" width="80" />
+          <el-table-column label="楼号" prop="buildingNo" width="80" />
           <el-table-column label="楼层" prop="buildingArea" width="100" />
           <el-table-column label="房间/区域" prop="floorRoomNo" width="180" />
           <el-table-column label="功能名称" prop="nameFunction" width="120" />
@@ -54,7 +54,7 @@
           <el-table-column label="安全责任部门" prop="safetyResponsibleDepartment" width="180" />
           <el-table-column label="安全责任部门负责人" prop="safetyDepartmentManagerName" width="180" />
           <el-table-column label="安全具体责任人" prop="safetySpecificPersonName" width="180" />
-          <el-table-column label="安全具体责任人联系方式" prop="safetyPersonContact;" width="230" />
+          <el-table-column label="安全具体责任人联系方式" prop="safetyPersonContact" width="230" />
           <el-table-column label="变更原因" prop="changeReason" width="170" />
           <el-table-column label="状态" prop="statusName" width="100" />
           <el-table-column fixed="right" min-width="240" label="操作">

+ 1 - 1
src/views/production-safety/implement-safety-duty/sign-agree-dept.vue

@@ -227,7 +227,7 @@
     if (!formData.rejection) {
       return false;
     }
-    return route.query.status === '3' && formData.rejection != null;
+    return route.query.status === '3' && formData.rejection !== null;
   });
   const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
   const isSignsUpload = computed(() => route.query.status === '2');

+ 388 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupList/CameraGroup.vue

@@ -0,0 +1,388 @@
+<template>
+  <div class="cameraGroup">
+    <el-collapse-item :name="cameraGroup.groupName" @keydown.space.stop.prevent @keydown.enter.stop.prevent>
+      <template #title>
+        <div
+          class="cameraGroupTitle"
+          :class="playingGroup?.id === props.cameraGroup.id ? 'playingCameraGroup' : ''"
+          @click.stop=""
+          @dblclick="handleRenameGroup"
+        >
+          <div class="renameGroupInput" v-show="showRenameGroupInput">
+            <img class="folderIcon" src="@/assets/icons/nine-square-grid/folder-white.png" />
+            <el-input
+              v-model="inputNewGroupName"
+              class="GroupNameInput"
+              placeholder="为此区域命名"
+              maxlength="15"
+              show-word-limit
+              ref="groupNameInputRef"
+              @blur="enterNewName"
+              @keyup.enter="$event.target.blur()"
+            />
+          </div>
+
+          <div class="IconAndGroupName" v-show="!showRenameGroupInput">
+            <img class="folderImg" src="@/assets/icons/nine-square-grid/folder.png" alt="" />
+            <div class="groupName" :title="cameraGroup.groupName">
+              {{ cameraGroup.groupName }}
+            </div>
+          </div>
+
+          <div class="groupOperationBar">
+            <img
+              src="@/assets/icons/nine-square-grid/more.png"
+              class="groupOperationIcon"
+              @click="showGroupOperation = !showGroupOperation"
+              @dblclick.stop=""
+            />
+          </div>
+
+          <div class="groupOperation" v-show="showGroupOperation">
+            <div @click="handelStartPlay()" class="groupOperationItem">
+              {{ cameraGroup.id !== playingGroup?.id ? '开始播放' : '停止播放' }}
+            </div>
+            <div @click="handleDelete(cameraGroup)" class="groupOperationItem"> 删除区域 </div>
+          </div>
+        </div>
+      </template>
+
+      <div class="addCamera" @click.stop="addCamera">
+        <div class="addCameraIcon">
+          <img src="@/assets/icons/nine-square-grid/add.png" />
+        </div>
+        <div>添加点位</div>
+      </div>
+
+      <CameraListOfGroup :cameraGroup="cameraGroup" />
+    </el-collapse-item>
+  </div>
+
+  <div>
+    <el-dialog v-model="showCameraTreeDialog" width="500" :title="`添加相机至“${cameraGroup.groupName}”区域`">
+      <CameraTreeOfGroupList :cameraGroup="cameraGroup" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { storeToRefs } from 'pinia';
+  import { ref, watch, nextTick } from 'vue';
+  import CameraTreeOfGroupList from './CameraTreeOfGroupList.vue';
+  import { ElCollapseItem, ElDialog, ElMessageBox, ElInput, ElMessage } from 'element-plus';
+  import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
+  import { userGridType } from '@/store/modules/userGridType';
+  import { GridType } from '@/views/disaster/monitor/splitScreenRetrieval/type';
+  import type { CameraGroupType } from '@/views/disaster/monitor/splitScreenRetrieval/type';
+  import { modifyCameraGroupApi } from '@/api/nine-square-grid';
+  import CameraListOfGroup from './CameraListOfGroup.vue';
+
+  const showGroupOperation = ref(false);
+  const showCameraTreeDialog = ref(false);
+  const props = defineProps<{
+    cameraGroup: CameraGroupType;
+    activeGroup: string[];
+  }>();
+
+  const showRenameGroupInput = ref(false);
+  let groupOriginName = props.cameraGroup.groupName;
+  const inputNewGroupName = ref('');
+  const groupNameInputRef = ref();
+
+  const { cameraGroupList, playingGroup, isPlaying, isPaused, playIntervalTime } = storeToRefs(useCameraGroupList());
+  const { deleteCameraGroup, groupStartPlay, setPlayGroup, stopPlay } = useCameraGroupList();
+  const { changeGridType } = userGridType();
+
+  // 控制播放/删除菜单的隐藏
+  const closeGroupOperation = (event) => {
+    if (event.target.className !== 'groupOperationItem') showGroupOperation.value = false;
+    if (event.target.innerText === '开始播放') {
+      handelStartPlay();
+      showGroupOperation.value = false;
+    }
+  };
+
+  function handleRenameGroup() {
+    showRenameGroupInput.value = true;
+    nextTick(() => {
+      groupNameInputRef.value.focus();
+      inputNewGroupName.value = groupOriginName;
+    });
+  }
+
+  function enterNewName() {
+    if (inputNewGroupName.value === '') {
+      showRenameGroupInput.value = false;
+      return;
+    }
+
+    const camera = cameraGroupList.value.find((x) => x.groupName === inputNewGroupName.value);
+
+    try {
+      if (camera && camera.id === props.cameraGroup.id) return;
+      else if (camera && camera.id !== props.cameraGroup.id)
+        return ElMessage({ message: '已存在同名区域', type: 'error' });
+    } finally {
+      showRenameGroupInput.value = false;
+    }
+
+    props.cameraGroup.groupName = inputNewGroupName.value;
+    groupOriginName = inputNewGroupName.value;
+    showRenameGroupInput.value = false;
+    modifyCameraGroupApi({
+      groupId: props.cameraGroup.id,
+      groupName: inputNewGroupName.value,
+    });
+  }
+
+  function handelStartPlay() {
+    // 如果点击的是其他区域的轮播按钮
+    const changeGroup = props.cameraGroup.id !== playingGroup.value?.id;
+    if (isPlaying.value) {
+      ElMessageBox.confirm(changeGroup ? '是否切换播放区域' : '是否取消当前区域相机播放', '提示', {
+        cancelButtonText: '取消',
+        confirmButtonText: '确定',
+        customClass: 'customMessageBox--warning',
+      })
+        .then(() => {
+          if (changeGroup) {
+            isPlaying.value = false;
+            stopPlay(playingGroup.value?.id!);
+            playClickedGroup();
+          } else {
+            stopPlay(props.cameraGroup.id, true);
+          }
+        })
+        .catch(() => {
+          return;
+        });
+    } else {
+      playClickedGroup();
+    }
+  }
+
+  function playClickedGroup() {
+    isPlaying.value = true;
+    setPlayGroup(props.cameraGroup);
+    playIntervalTime.value = props.cameraGroup.playIntervalSec ? props.cameraGroup.playIntervalSec : 60;
+    isPaused.value = Boolean(props.cameraGroup.isPaused);
+    changeGridType(GridType.nineGrids);
+    groupStartPlay(playIntervalTime.value);
+    modifyCameraGroupApi({
+      groupId: props.cameraGroup.id,
+      isDefault: 1,
+    });
+  }
+
+  function addCamera() {
+    showCameraTreeDialog.value = true;
+  }
+
+  function handleDelete(cameraGroup: CameraGroupType) {
+    const text = '删除后,区域数据不可恢复,需要重新添加,是否确认删除该区域?';
+    ElMessageBox.confirm(text, '提示', {
+      cancelButtonText: '取消',
+      confirmButtonText: '确定',
+      customClass: 'customMessageBox--warning',
+    })
+      .then(() => {
+        deleteCameraGroup(cameraGroup.id);
+      })
+      .catch(() => {
+        return;
+      });
+  }
+
+  watch(
+    () => showGroupOperation.value,
+    (newValue) => {
+      newValue
+        ? document.addEventListener('mousedown', closeGroupOperation)
+        : document.removeEventListener('mousedown', closeGroupOperation);
+    },
+  );
+</script>
+
+<style lang="scss" scoped>
+  .cameraGroup {
+    margin-bottom: 1px;
+    .cameraGroupTitle {
+      height: 38px;
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      color: #333333;
+      border-radius: 4px;
+      position: relative;
+      cursor: default;
+      .arrowIcon {
+        height: 16px;
+        width: 16px;
+        margin-right: 12px;
+        display: flex;
+        align-items: center;
+      }
+      .renameGroupInput {
+        height: 32px;
+        width: 100%;
+        display: flex;
+        align-items: center;
+        background-color: #1777ff;
+        border-radius: 4px;
+        font-size: 14px;
+        line-height: 24px;
+        padding-right: 6px;
+
+        .folderIcon {
+          height: 24px;
+          margin-left: 12px;
+          margin-right: 12px;
+        }
+        .GroupNameInput {
+          height: 24px;
+        }
+      }
+      .IconAndGroupName {
+        display: flex;
+        align-items: center;
+        .folderImg {
+          height: 24px;
+          width: 24px;
+          margin-right: 12px;
+        }
+        .groupName {
+          white-space: nowrap;
+        }
+      }
+      .groupOperationBar {
+        visibility: hidden;
+        margin-left: 10px;
+        margin-right: 10px;
+        .groupOperationIcon {
+          height: 20px;
+          width: 20px;
+          &:hover {
+            background-color: rgba(255, 255, 255, 0.2);
+            border-radius: 4px;
+            cursor: pointer;
+          }
+        }
+      }
+
+      &:hover .groupOperationBar {
+        visibility: visible;
+        display: flex;
+        align-items: center;
+      }
+
+      .groupOperation {
+        display: block;
+        height: 63px;
+        width: 130px;
+        position: absolute;
+        bottom: -63px;
+        right: 5px;
+        box-shadow: 0px 0px 10px 2px rgb(0, 0, 0, 0.3);
+        border-radius: 4px;
+        background: #f4f7ff;
+        outline: 1px solid rgba(245, 248, 255, 0.25);
+        z-index: 999;
+        .groupOperationItem {
+          height: 31.5px;
+          line-height: 31.5px;
+          border-radius: 4px;
+          &:hover {
+            color: #fff;
+            background-color: #1777ff;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+
+    .playingCameraGroup {
+      height: 38px;
+      background-color: #1777ff;
+    }
+
+    .cameraGroupTitle:not(.playingCameraGroup):hover {
+      border-radius: 4px;
+      background-color: #c0c4d2;
+    }
+
+    .addCamera {
+      height: 32px;
+      width: fit-content;
+      display: flex;
+      align-items: center;
+      margin-left: 74px;
+      font-size: 14px;
+      line-height: 32px;
+      color: #333333;
+      text-align: left;
+      white-space: nowrap;
+      cursor: pointer;
+
+      .addCameraIcon {
+        margin-right: 14px;
+        position: relative;
+        display: flex;
+        align-items: center;
+
+        img {
+          height: 20px;
+          width: 20px;
+        }
+
+        &::after {
+          content: '+';
+          position: absolute;
+          left: 0;
+          right: 0;
+          top: -7px;
+          font-size: 20px;
+          color: #1777ff;
+          text-align: center;
+          z-index: 1;
+        }
+      }
+    }
+
+    :deep(.el-collapse-item__header) {
+      height: 38px;
+    }
+  }
+
+  :deep(.el-collapse-item__header:has(.playingCameraGroup)) {
+    height: 38px;
+    background-color: #1777ff;
+    border-radius: 4px;
+
+    .groupName,
+    svg {
+      color: #fff;
+    }
+    .IconAndGroupName img {
+      content: url('@/assets/icons/nine-square-grid/folder-white.png');
+    }
+  }
+
+  :deep(.el-collapse-item__header:not(.el-collapse-item__header:has(.playingCameraGroup)):hover) {
+    background-color: #cdd8ff;
+    border-radius: 4px;
+  }
+
+  :deep(.el-collapse-item__header:not(.el-collapse-item__header:has(.playingCameraGroup)):hover .cameraGroupTitle) {
+    background-color: #cdd8ff;
+    border-radius: 4px;
+  }
+
+  :deep(.el-dialog__header) {
+    text-align: left;
+    font-weight: bold;
+  }
+
+  :deep(.el-dialog__body) {
+  }
+</style>

+ 200 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupList/CameraGroupList.vue

@@ -0,0 +1,200 @@
+<template>
+  <div class="cameraGroups__decoration"></div>
+
+  <div class="cameraGroups">
+    <div class="createGroup" @click="handleCreateGroup">
+      <div class="createGroupIcon">
+        <img src="@/assets/icons/nine-square-grid/add.png" />
+      </div>
+      <div>添加监控区域</div>
+    </div>
+
+    <div class="createGroupInput" v-show="showCreateGroupInput">
+      <img class="folderIcon" src="@/assets/icons/nine-square-grid/folder-white.png" />
+      <el-input
+        v-model="inputNewGroupName"
+        class="GroupNameInput"
+        placeholder="为此区域命名"
+        maxlength="15"
+        show-word-limit
+        ref="groupNameInputRef"
+        @blur="handleEnterGroupName()"
+        @keyup.enter="$event.target.blur()"
+      />
+    </div>
+
+    <div class="groupList">
+      <el-scrollbar>
+        <el-collapse v-model="activeGroup">
+          <div v-for="cameraGroup in cameraGroupList" :key="cameraGroup.id">
+            <CameraGroup :cameraGroup="cameraGroup" :activeGroup="activeGroup" />
+          </div>
+        </el-collapse>
+      </el-scrollbar>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, nextTick } from 'vue';
+  import { ElInput, ElScrollbar, ElCollapse } from 'element-plus';
+  import CameraGroup from './CameraGroup.vue';
+  import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
+  import { storeToRefs } from 'pinia';
+
+  const activeGroup = ref(['']);
+  const showCreateGroupInput = ref(false);
+  const inputNewGroupName = ref('');
+  const groupNameInputRef = ref();
+  const { cameraGroupList } = storeToRefs(useCameraGroupList());
+  const { getCameraGroupList, createCameraGroup } = useCameraGroupList();
+
+  function handleCreateGroup() {
+    showCreateGroupInput.value = !showCreateGroupInput.value;
+    if (showCreateGroupInput.value) {
+      checkGroupName();
+
+      nextTick(() => {
+        groupNameInputRef.value.focus();
+      });
+    }
+  }
+
+  function handleEnterGroupName() {
+    checkGroupName();
+    createCameraGroup(inputNewGroupName.value);
+    activeGroup.value = [inputNewGroupName.value]; // 添加区域后展开区域
+    inputNewGroupName.value = '';
+    showCreateGroupInput.value = false;
+  }
+
+  function checkGroupName() {
+    if (inputNewGroupName.value === '') {
+      let maxSuffix = 0;
+      cameraGroupList.value.map((x) => {
+        if (x.groupName.startsWith('新建区域')) {
+          const suffix = Number(x.groupName.replace('新建区域', ''));
+          // 过滤区域名后缀不是数字的区域
+          if (!isNaN(suffix)) {
+            if (suffix > maxSuffix) {
+              maxSuffix = suffix;
+            }
+          }
+        }
+      });
+
+      if (maxSuffix !== 0) {
+        const newDefaultGroupNameIndex = maxSuffix + 1;
+
+        inputNewGroupName.value = '新建区域' + newDefaultGroupNameIndex;
+      } else {
+        inputNewGroupName.value = '新建区域1';
+      }
+    }
+  }
+
+  onMounted(() => {
+    getCameraGroupList(6);
+  });
+</script>
+
+<style lang="scss" scoped>
+  .cameraGroups__decoration {
+    height: 5px;
+    margin-bottom: 20px;
+    border-radius: 8px 8px 0 0;
+    background-color: #1777ff;
+  }
+
+  .cameraGroups {
+    height: calc(100% - 25px);
+    color: #333333;
+    padding: 0px 16px;
+    padding-right: 4px;
+    transition: all 0.3s ease;
+
+    .createGroup {
+      height: 24px;
+      width: 120px;
+      display: flex;
+      margin-right: 12px;
+      margin-bottom: 12px;
+      color: #333333;
+      font-size: 14px;
+      line-height: 24px;
+      cursor: pointer;
+
+      .createGroupIcon {
+        margin-right: 12px;
+        position: relative;
+
+        img {
+          height: 24px;
+          width: 24px;
+        }
+
+        &::after {
+          content: '+';
+          position: absolute;
+          top: -1px;
+          left: 0;
+          right: 0;
+          font-size: 20px;
+          font-weight: 500;
+          color: #1777ff;
+          text-align: center;
+        }
+      }
+    }
+
+    .createGroupInput {
+      height: 32px;
+      display: flex;
+      align-items: center;
+      background-color: #1677ff;
+      border-radius: 4px;
+      margin-bottom: 12px;
+      padding-right: 6px;
+      color: #333333;
+      font-size: 14px;
+      line-height: 24px;
+      .folderIcon {
+        height: 24px;
+        margin-right: 12px;
+      }
+      .GroupNameInput {
+        height: 24px;
+      }
+    }
+    .groupList {
+      height: 80%;
+
+      padding: 0 0px;
+      overflow: auto;
+    }
+  }
+
+  :deep(.el-collapse) {
+    border: 0px;
+  }
+  :deep(.el-collapse-item__arrow) {
+    margin-right: 12px;
+    color: #333333;
+  }
+
+  :deep(.el-collapse-item__wrap) {
+    background: transparent;
+    border-bottom: transparent;
+  }
+  :deep(.el-collapse-item__content) {
+    padding: 0px;
+    margin-right: 10px;
+  }
+  :deep(.el-collapse-item__header) {
+    background: #f4f7ff;
+    border: 0px;
+    flex-direction: row-reverse;
+    padding-left: 12px;
+    margin-right: 10px;
+  }
+</style>

+ 198 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupList/CameraListOfGroup.vue

@@ -0,0 +1,198 @@
+<template>
+  <div ref="dragRef">
+    <div class="camera" v-for="camera in cameraGroup.children" :key="camera.cameraGroupDetailId">
+      <div class="IconAndCameraName">
+        <div class="cameraIcon">
+          <WarningFilled
+            class="invalidCameraIcon"
+            style="color: red"
+            v-if="!carouselList.includes(camera) && playingGroup?.id === cameraGroup.id"
+          />
+          <img
+            class="cameraImg"
+            src="@/assets/icons//nine-square-grid/camera.png"
+            v-if="!cameraInPlay.includes(camera)"
+          />
+          <img class="cameraImg" src="@/assets/icons/nine-square-grid/playingCamera.png" v-else />
+        </div>
+
+        <div
+          class="cameraName"
+          :class="{
+            playingCamera: cameraInPlay.includes(camera),
+            invalidCameraName: !carouselList.includes(camera) && playingGroup?.id === cameraGroup.id,
+          }"
+        >
+          {{ camera.name }}
+        </div>
+      </div>
+
+      <div class="cameraOperation">
+        <img
+          class="cameraOperationIcon"
+          src="@/assets/icons/nine-square-grid/icon-delete.png"
+          @click.stop="handleDelete(cameraGroup, camera)"
+        />
+      </div>
+
+      <Thumbnail :imageUrl="camera.imageUrl" :code="camera.code" position="right">
+        <div class="mask"></div>
+      </Thumbnail>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import type { Camera, CameraGroupType } from '@/views/disaster/monitor/splitScreenRetrieval/type';
+  import { updateCameraOrderApi } from '@/api/nine-square-grid';
+  import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
+  import { WarningFilled } from '@element-plus/icons-vue';
+  import { ElMessageBox } from 'element-plus';
+  import { useDraggable } from 'vue-draggable-plus';
+  import Thumbnail from '@/components/thumbnail/Thumbnail.vue';
+
+  const props = defineProps<{
+    cameraGroup: CameraGroupType;
+  }>();
+  const dragRef = ref<HTMLElement | null>(null);
+  const list = ref<Camera[]>([]);
+
+  const { carouselList, playingGroup, cameraInPlay } = storeToRefs(useCameraGroupList());
+
+  const { deleteCameraFromGroup, deleteCameraInPlaylist, restartPlay } = useCameraGroupList();
+
+  function handleDelete(cameraGroup: CameraGroupType, camera: Camera) {
+    const text = '删除后,相机数据不可恢复,是否确认删除?';
+    ElMessageBox.confirm(text, '提示', {
+      cancelButtonText: '取消',
+      confirmButtonText: '确定',
+      customClass: 'customMessageBox--warning',
+      lockScroll: false,
+    })
+      .then(() => {
+        if (cameraInPlay.value.includes(camera)) {
+          deleteCameraInPlaylist(cameraGroup, camera);
+        } else {
+          deleteCameraFromGroup(cameraGroup, camera);
+          restartPlay();
+        }
+      })
+      .catch(() => {
+        return;
+      });
+  }
+
+  watch(
+    () => props.cameraGroup.children,
+    (children) => {
+      list.value = children;
+    },
+    { immediate: true, deep: true },
+  );
+
+  useDraggable(dragRef, list, {
+    animation: 150,
+    ghostClass: 'ghost',
+    onUpdate() {
+      const updateIds = list.value.map((x, index) => {
+        return {
+          groupId: props.cameraGroup.id,
+          cameraId: x.id,
+          orderNum: index + 1,
+        };
+      });
+      props.cameraGroup.children = list.value;
+      updateCameraOrderApi(updateIds);
+      restartPlay();
+    },
+  });
+</script>
+
+<style lang="scss" scoped>
+  .camera {
+    height: 32px;
+    width: 100%;
+    white-space: nowrap;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-left: 72px;
+    font-size: 14px;
+    line-height: 32px;
+    color: #333333;
+    text-align: left;
+    background: #f4f7ff;
+    position: relative;
+    &:hover {
+      background-color: #cdd8ff;
+    }
+    &:hover .cameraOperation {
+      visibility: visible;
+      display: flex;
+      align-items: center;
+    }
+
+    .IconAndCameraName {
+      display: flex;
+      align-items: center;
+      .cameraIcon {
+        display: flex;
+        align-items: center;
+        position: relative;
+        .cameraImg {
+          height: 24px;
+          width: 24px;
+          margin-right: 12px;
+        }
+        .invalidCameraIcon {
+          height: 14px;
+          color: #dd5869;
+          position: absolute;
+          right: 6px;
+          top: 0px;
+        }
+      }
+      .cameraName {
+        width: fit-content;
+        position: relative;
+      }
+      .invalidCameraName {
+        color: #999999;
+      }
+    }
+
+    .cameraOperation {
+      visibility: hidden;
+      margin-right: 4px;
+      .cameraOperationIcon {
+        height: 20px;
+        width: 20px;
+        margin-left: 10px;
+        padding: 2px;
+        z-index: 999;
+        &:hover {
+          background-color: rgb(197, 197, 197);
+          border-radius: 4px;
+          cursor: pointer;
+        }
+      }
+    }
+
+    .mask {
+      height: 32px;
+      width: 100%;
+    }
+  }
+
+  :deep(.thumb-nail) {
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 9;
+  }
+</style>

+ 232 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupList/CameraTreeOfGroupList.vue

@@ -0,0 +1,232 @@
+<template>
+  <div class="cameraTreeWrapper">
+    <div class="cameraTreeInputWrapper">
+      <el-input
+        v-model="filterText"
+        placeholder="请输入相机的名称进行搜索"
+        :suffix-icon="Search"
+        class="filterTextInput"
+      />
+    </div>
+    <el-scrollbar class="tree-scroll">
+      <el-tree
+        :data="cameraTree"
+        :props="defaultProps"
+        @node-click="handleClickCamera"
+        node-key="code"
+        :default-expand-all="false"
+        :default-expanded-keys="cameraTree"
+        :filter-node-method="filterNode"
+        ref="treeRef"
+        v-loading="treeLoading"
+        :empty-text="treeEmptyText"
+        element-loading-background="rgba(0, 0, 0, 0)"
+      >
+        <template #default="{ node, data }">
+          <div
+            class="treeNode"
+            :class="{
+              selectedCamera: isSelected(node, data.id),
+            }"
+          >
+            <div v-if="data.nodeType === CameraTreeNodeType.camera" class="icons">
+              <VideoCamera class="cameraIcon" />
+              <WarningFilled v-if="isInvalid(data)" class="invalidCamera" style="color: red" />
+            </div>
+
+            <div class="cameraName">
+              {{ node.label }}
+            </div>
+
+            <Thumbnail
+              v-if="data.nodeType === CameraTreeNodeType.camera"
+              :imageUrl="data.imageUrl"
+              :code="data.code"
+              position="right"
+            >
+              <div class="mask"></div>
+            </Thumbnail>
+          </div>
+        </template>
+      </el-tree>
+    </el-scrollbar>
+  </div>
+</template>
+<script setup lang="ts">
+  import { ElInput, ElTree, ElScrollbar } from 'element-plus';
+  import { onMounted, ref, watch } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { VideoCamera, WarningFilled, Search } from '@element-plus/icons-vue';
+  import { CameraTreeNodeType, getCameraTree } from '@/api/camera/camera-preview';
+  import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
+  import type { CameraGroupType } from '@/views/disaster/monitor/splitScreenRetrieval/type';
+  import Thumbnail from '@/components/thumbnail/Thumbnail.vue';
+
+  interface Tree {
+    [key: string]: any;
+  }
+
+  const defaultProps = {
+    children: 'children',
+    label: 'name',
+  };
+
+  const cameraTree = ref();
+  const filterText = ref('');
+  const treeRef = ref<InstanceType<typeof ElTree>>();
+  const childrenNodeList = ref<string[]>([]);
+  const treeLoading = ref(false);
+  const treeEmptyText = ref('');
+  const props = defineProps<{ cameraGroup: CameraGroupType }>();
+
+  const { cameraInPlay, playingGroup, totalRound } = storeToRefs(useCameraGroupList());
+  const { addCameraIntoGroup, deleteCameraFromGroup, deleteCameraInPlaylist, getValidateCameraNum, restartPlay } =
+    useCameraGroupList();
+
+  const isSelected = (nodeData, id: number) => {
+    return props.cameraGroup.children.find((x) => x.id === id && nodeData.data.nodeType === CameraTreeNodeType.camera);
+  };
+
+  const handleClickCamera = (nodeData) => {
+    // 如果点击的不是摄像头
+    if (nodeData.nodeType !== CameraTreeNodeType.camera) return;
+
+    const cameraInGroup = props.cameraGroup.children.find((x) => x.id === nodeData.id);
+
+    // 如果该摄像头在分组里,则从分组移除该摄像头,否则添加该摄像头进分组
+    if (cameraInGroup) {
+      cameraInPlay.value.includes(cameraInGroup)
+        ? deleteCameraInPlaylist(props.cameraGroup, cameraInGroup)
+        : deleteCameraFromGroup(props.cameraGroup, cameraInGroup);
+    } else {
+      const camera = {
+        code: nodeData.code,
+        name: nodeData.name,
+        url: nodeData.pushStreamDTO.videoUrls?.pushstreamIp || '',
+        id: nodeData.id,
+        cameraGroupDetailId: -1,
+        imageUrl: '',
+      };
+
+      addCameraIntoGroup(props.cameraGroup.id, camera);
+
+      // 如果往正在轮播的分组添加相机,且该分组的有效相机数未能填满宫格数,则刷新当前播放的画面
+      if (props.cameraGroup.id === playingGroup.value?.id && getValidateCameraNum() >= 0 && totalRound.value <= 1) {
+        restartPlay();
+      }
+    }
+  };
+
+  watch(filterText, (val) => {
+    childrenNodeList.value = [];
+    treeRef.value!.filter(val);
+  });
+
+  function extractCodes(data: any[], codes: string[] = []) {
+    data.forEach((item) => {
+      codes.push(item.data.code);
+      if (item.childNodes) {
+        extractCodes(item.childNodes, codes);
+      }
+    });
+    return codes;
+  }
+
+  const filterNode = (value: string, data: Tree, node) => {
+    if (!value) return true;
+    // 检查当前节点的 label 是否包含关键词
+    const labelMatch = data.name.includes(value);
+
+    if (labelMatch) {
+      if (node.childNodes && node.childNodes.length > 0) {
+        childrenNodeList.value = extractCodes(node.childNodes, []);
+      }
+    }
+
+    if (childrenNodeList.value.includes(data.code)) {
+      return true;
+    } else {
+      return labelMatch;
+    }
+  };
+
+  const isInvalid = (data) => {
+    if (data.networkingState !== 0) data.disable = true;
+    return data.networkingState !== 0;
+  };
+
+  onMounted(() => {
+    treeLoading.value = true;
+    getCameraTree()
+      .then((res) => {
+        cameraTree.value = res;
+        if (cameraTree.value.length === 0) treeEmptyText.value = '暂无数据';
+      })
+      .catch(() => {
+        treeEmptyText.value = '暂无数据';
+      })
+      .finally(() => {
+        treeLoading.value = false;
+      });
+  });
+</script>
+<style lang="scss" scoped>
+  .cameraTreeWrapper {
+    .cameraTreeInputWrapper {
+      padding: 8px;
+      .filterTextInput {
+        margin: 8px 0;
+      }
+    }
+
+    .tree-scroll {
+      height: 400px;
+      .selectedCamera {
+        color: #1777ff !important;
+      }
+      .treeNode {
+        display: flex;
+        align-items: center;
+        color: black;
+        // position: relative;
+        .icons {
+          display: flex;
+          align-items: center;
+          position: relative;
+          .cameraIcon {
+            height: 18px;
+            margin-right: 8px;
+            color: black;
+          }
+          .invalidCamera {
+            height: 14px;
+            color: #dd5869;
+            position: absolute;
+            right: 0px;
+            top: -5px;
+          }
+        }
+        .cameraName {
+        }
+      }
+      .mask {
+        height: 26px;
+        width: 100%;
+      }
+    }
+  }
+
+  :deep(.thumb-nail) {
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 99;
+  }
+
+  :deep(.el-tree-node__content) {
+    position: relative;
+  }
+</style>

+ 59 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/CameraGroupListAndTree/CameraGroupListAndTree.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="cameraGroupContainer">
+    <div class="CameraGroupList" :class="{ collapsed: isCollapse }">
+      <CameraGroupList />
+    </div>
+
+    <div class="toggle-button-wrapper" @click="isCollapse = !isCollapse">
+      <div class="collapse-button" :class="{ collapsed: isCollapse === false }"></div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import CameraGroupList from './CameraGroupList/CameraGroupList.vue';
+
+  const isCollapse = ref(false);
+</script>
+
+<style lang="scss" scoped>
+  .cameraGroupContainer {
+    height: 100%;
+    width: 100%;
+    display: flex;
+    align-items: center;
+    .CameraGroupList {
+      height: 100%;
+      width: 100%;
+      background: #f4f7ff;
+      overflow: hidden;
+      transition: all 0.3s ease;
+    }
+
+    .toggle-button-wrapper {
+      @include flex-center;
+      width: 15cpx;
+      height: 75cpx;
+      margin-right: 5px;
+      z-index: 10;
+      clip-path: polygon(0 0, 100% 10cpx, 100% 65cpx, 0 75cpx);
+      background-color: $primary-color;
+      cursor: pointer;
+
+      .collapse-button {
+        border-style: solid;
+        border-width: 5cpx 0 5cpx 8cpx;
+        border-color: transparent transparent transparent #fff;
+        transform: rotate(180deg);
+
+        &.collapsed {
+          transform: rotate(0deg);
+        }
+      }
+    }
+    .collapsed {
+      width: 0;
+    }
+  }
+</style>

+ 258 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/VideosGridBase/CamerasGrid.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="main-grid" id="main-grid">
+    <div v-if="cameraInPlay.some((x) => x.url !== '')" class="video-grid" :class="setGridSize()" id="video-grid">
+      <div :id="`video-${index}`" v-for="(camera, index) in props.cameraInPlay" :key="camera.id" class="video-box">
+        <div class="LiveVideo" v-if="camera.url !== ''">
+          <LiveVideo
+            :url="getWsUrl(camera.url)"
+            :poster="camera.imageUrl"
+            @dblclick="isFullScreen ? exitFullscreen() : fullScreen(`video-${index}`, 'single')"
+          />
+        </div>
+
+        <div class="emptyCameraGrid" v-else>
+          <div class="emptyCameraImgAndText">
+            <img class="emptyCameraGridImg" src="@/assets/icons/nine-square-grid/cameraEmpty.png" />
+            <div>暂未接入相机</div>
+          </div>
+        </div>
+
+        <div class="video-controlBar" v-if="camera.url && !isFullScreen">
+          <div class="cameraName">{{ camera.name }}</div>
+          <div class="controlIconContainer">
+            <Delete class="controlIcon fullScreen" @click="handleDeleteCamera(camera)" />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="allCameraEmpty" v-else>
+      <img class="cameraEmptyImg" src="@/assets/icons/nine-square-grid/cameraEmpty.png" />
+      <div>暂未接入相机</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onUnmounted } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import LiveVideo from '@/components/live/LiveVideoFlv.vue';
+  import screenfull from 'screenfull';
+  import { Delete } from '@element-plus/icons-vue';
+  import urlJoin from 'url-join';
+  import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
+  import { userGridType } from '@/store/modules/userGridType';
+  import { GridType } from '@/views/disaster/monitor/splitScreenRetrieval/type';
+  import { type Camera, CameraInPlay } from '../type';
+  import { userSplitScreenFullScreen } from '@/store/modules/userSplitScreenFullScreen';
+  import { ElMessageBox } from 'element-plus';
+
+  const props = defineProps<{ cameraInPlay: CameraInPlay[] }>();
+  const { currentGrid } = storeToRefs(userGridType());
+  const { isFullScreen, curFullScreenType } = storeToRefs(userSplitScreenFullScreen());
+
+  const { playingGroup } = storeToRefs(useCameraGroupList());
+  const { deleteCameraInPlaylist } = useCameraGroupList();
+  const { fullScreen, exitFullscreen } = userSplitScreenFullScreen();
+
+  function handleDeleteCamera(camera: Camera) {
+    ElMessageBox.confirm('删除后,相机数据不可恢复,是否确认删除?', '提示', {
+      cancelButtonText: '取消',
+      confirmButtonText: '确定',
+      customClass: 'customMessageBox--warning',
+    })
+      .then(() => {
+        deleteCameraInPlaylist(playingGroup.value!, camera);
+      })
+      .catch(() => {
+        return;
+      });
+  }
+
+  const isHttps = () => {
+    return window.location.protocol.startsWith('https');
+  };
+
+  const getWsUrl = (videoUrl: string) => {
+    if (!videoUrl) return '';
+    const protocol = isHttps() ? 'wss' : 'ws';
+    // 如果是绝对地址
+    if (videoUrl.startsWith('http')) {
+      // 如果是https的话,websocket要用wss
+      return videoUrl.replace('http', protocol);
+    }
+    const u = urlJoin(
+      `${protocol}://`,
+      window.location.host,
+      window.location.pathname === '/' ? '' : window.location.pathname,
+      videoUrl,
+    );
+
+    return u;
+  };
+
+  // 根据当前宫格数设置每个宫格宽高
+  const setGridSize = () => {
+    if (currentGrid.value === GridType.oneGrid) return 'oneGrid';
+    if (currentGrid.value === GridType.fourGrids) return 'fourGrid';
+    if (currentGrid.value === GridType.nineGrids) return 'nineGrids';
+    if (currentGrid.value === GridType.sixteenGrids) return 'sixteenGrids';
+  };
+
+  // 全屏之后,无法监听到键盘按键的点击事件,所以只能监听窗口的变化进行判断
+  window.onresize = () => {
+    if (!screenfull.isFullscreen) {
+      isFullScreen.value = false; //判断退出全屏,进行赋值
+      curFullScreenType.value = 'single';
+    }
+  };
+
+  onUnmounted(() => {
+    window.onresize = null;
+  });
+</script>
+
+<style lang="scss" scoped>
+  .main-grid {
+    height: calc(100% - 54px);
+    background: #f4f7ff;
+    padding: 0 10px 10px 10px;
+    border-radius: 0 0 4px 4px;
+    .video-grid {
+      height: 100%;
+      display: grid;
+      gap: 4px;
+
+      .video-box {
+        position: relative;
+        background: #fff;
+        .LiveVideo {
+          width: 100%;
+          height: 100%;
+          video {
+            object-fit: fill;
+          }
+        }
+        &:hover {
+          .video-controlBar {
+            display: flex;
+          }
+        }
+        .emptyCameraGrid {
+          height: 100%;
+          font-size: 12px;
+          color: #ebedf1;
+          margin: auto;
+          text-align: center;
+          .emptyCameraImgAndText {
+            margin: auto;
+            color: #999999;
+            .emptyCameraGridImg {
+              height: 50%;
+              width: 40%;
+            }
+          }
+        }
+        .video-controlBar {
+          position: absolute;
+          display: none;
+          bottom: 10px;
+          left: 0;
+          width: 100%;
+          justify-content: space-between;
+          padding: 0 10px;
+
+          .video-name {
+            background-color: rgba(255, 255, 255, 0.7);
+            padding: 0px 5px;
+            border-radius: 5px;
+            font-size: 12px;
+            color: black;
+          }
+          .controlIconContainer {
+            display: flex;
+            justify-content: space-around;
+            align-items: center;
+
+            border-radius: 16px;
+            padding: 0 16px;
+            background: rgba(0, 0, 0, 0.4);
+            .controlIcon {
+              height: 20px;
+              color: #fff;
+            }
+            .fullScreen {
+              height: 18px;
+              width: 18px;
+            }
+            .controlIcon:hover {
+              color: #1777ff;
+              cursor: pointer;
+            }
+          }
+        }
+        .videoDisconnected {
+          height: 20%;
+          width: 30%;
+          position: absolute;
+          color: white;
+          font-size: 24px;
+          top: 0;
+          bottom: 0;
+          left: 0;
+          right: 0;
+          margin: auto;
+        }
+      }
+      .selectedVideo {
+        outline: 4px;
+        outline-style: solid;
+        outline-color: #a1a2a2;
+      }
+    }
+
+    .oneGrid {
+      grid-template-columns: repeat(1, 100%);
+      grid-template-rows: repeat(1, 100%);
+    }
+
+    .fourGrid {
+      grid-template-columns: repeat(2, calc(50% - 2px));
+      grid-template-rows: repeat(2, calc(50% - 2px));
+    }
+
+    .nineGrids {
+      grid-template-columns: repeat(3, calc(33.3% - 2px));
+      grid-template-rows: repeat(3, calc(33.3% - 2px));
+    }
+
+    .sixteenGrids {
+      gap: 2px;
+      grid-template-columns: repeat(4, calc(25% - 1px));
+      grid-template-rows: repeat(4, calc(25% - 1px));
+    }
+
+    .allCameraEmpty {
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      margin: auto;
+      color: #999999;
+      .cameraEmptyImg {
+        height: 350px;
+        width: 350px;
+      }
+    }
+  }
+
+  .cameraName {
+    border-radius: 16px;
+    height: 32px;
+    line-height: 32px;
+    padding: 0 16px;
+    background: rgba(0, 0, 0, 0.4);
+    color: #fff;
+  }
+</style>

+ 334 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/VideosGridBase/ScreenToolbar.vue

@@ -0,0 +1,334 @@
+<template>
+  <div class="control-panel">
+    <div class="changeGridBar">
+      <div class="changeGrid">
+        <el-tooltip class="box-item" effect="dark" content="一屏" placement="bottom">
+          <img
+            :src="currentGrid === GridType.oneGrid ? oneGrid_selected : oneGrid"
+            class="changeGridIcon"
+            :class="currentGrid === GridType.oneGrid ? 'selectedGridIcon' : ''"
+            @click="changeGridType(GridType.oneGrid)"
+          />
+        </el-tooltip>
+      </div>
+
+      <div class="changeGrid">
+        <el-tooltip effect="dark" content="四屏" placement="bottom">
+          <img
+            :src="currentGrid === GridType.fourGrids ? fourGrids_selected : fourGrids"
+            class="changeGridIcon"
+            :class="currentGrid === GridType.fourGrids ? 'selectedGridIcon' : ''"
+            @click="changeGridType(GridType.fourGrids)"
+          />
+        </el-tooltip>
+      </div>
+
+      <div class="changeGrid">
+        <el-tooltip class="box-item" effect="dark" content="九屏" placement="bottom">
+          <img
+            :src="currentGrid === GridType.nineGrids ? nineGrids_selected : nineGrids"
+            class="changeGridIcon"
+            :class="currentGrid === GridType.nineGrids ? 'selectedGridIcon' : ''"
+            @click="changeGridType(GridType.nineGrids)"
+          />
+        </el-tooltip>
+      </div>
+
+      <div class="changeGrid">
+        <el-tooltip class="box-item" effect="dark" content="十六屏" placement="bottom">
+          <img
+            :src="currentGrid === GridType.sixteenGrids ? sixteenGrids_selected : sixteenGrids"
+            class="changeGridIcon"
+            :class="currentGrid === GridType.sixteenGrids ? 'selectedGridIcon' : ''"
+            @click="changeGridType(GridType.sixteenGrids)"
+          />
+        </el-tooltip>
+      </div>
+
+      <div>|</div>
+    </div>
+
+    <div class="controlBtns">
+      <div class="lockAndSetTime">
+        <div class="pausePlay" @click="handleClickLock">
+          <el-tooltip class="box-item" effect="dark" :content="isPaused ? '恢复轮播' : '取消轮播'" placement="bottom">
+            <div v-if="isPaused">
+              <img src="@/assets/icons/nine-square-grid/lock.png" />
+            </div>
+            <div v-else>
+              <img src="@/assets/icons/nine-square-grid/unlock.png" />
+            </div>
+          </el-tooltip>
+        </div>
+
+        <div class="setIntervalTime" v-show="isPlaying && !isPaused">
+          <div>相机轮播时间间隔:</div>
+          <el-input
+            v-model="playIntervalTime"
+            ref="inputRef"
+            placeholder="默认60"
+            class="intervalTimeInputBar"
+            @blur="handelStartPlay"
+            @keyup.enter="handelStartPlay"
+          >
+            <template #append>
+              <div>秒</div>
+            </template>
+          </el-input>
+        </div>
+      </div>
+
+      <div class="RoundAndFullScreen">
+        <div class="controlRound">
+          <div
+            class="previousRound"
+            :class="currentRound === 1 || currentRound === 0 ? 'disableChangeRound' : ''"
+            @click="playPreviousRound"
+          >
+            <el-tooltip class="box-item" effect="dark" content="上一轮" placement="bottom">
+              <ArrowLeft />
+            </el-tooltip>
+          </div>
+
+          <div class="rounds">
+            <div class="currentRound">{{ currentRound }}</div>
+            <div class="totalRound">/{{ totalRound }}</div>
+          </div>
+
+          <div
+            class="nextRound"
+            @click="playNextRound"
+            :class="currentRound === totalRound ? 'disableChangeRound' : ''"
+          >
+            <el-tooltip class="box-item" effect="dark" content="下一轮" placement="bottom">
+              <ArrowRight />
+            </el-tooltip>
+          </div>
+        </div>
+
+        <el-tooltip class="box-item" effect="dark" content="全屏" placement="bottom">
+          <img
+            src="@/assets/icons/nine-square-grid/fullScreen2.png"
+            class="fullScreenIcon"
+            @click="isFullScreen ? exitFullscreen() : fullScreen('video-grid', 'all')"
+          />
+        </el-tooltip>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { ElInput, ElTooltip } from 'element-plus';
+  import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
+  import { userGridType } from '@/store/modules/userGridType';
+  import { GridType } from '@/views/disaster/monitor/splitScreenRetrieval/type';
+  import { userSplitScreenFullScreen } from '@/store/modules/userSplitScreenFullScreen';
+  import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
+  import { modifyCameraGroupApi } from '@/api/nine-square-grid';
+  import oneGrid from '@/assets/icons/nine-square-grid/oneGrid.png';
+  import oneGrid_selected from '@/assets/icons/nine-square-grid/oneGrid-selected.png';
+  import fourGrids from '@/assets/icons/nine-square-grid/fourGrids.png';
+  import fourGrids_selected from '@/assets/icons/nine-square-grid/fourGrids-selected.png';
+  import nineGrids from '@/assets/icons/nine-square-grid/nineGrids.png';
+  import nineGrids_selected from '@/assets/icons/nine-square-grid/nineGrids-selected.png';
+  import sixteenGrids from '@/assets/icons/nine-square-grid/sixteenGrids.png';
+  import sixteenGrids_selected from '@/assets/icons/nine-square-grid/sixteenGrids-selected.png';
+
+  const inputRef = ref();
+
+  const { currentGrid } = storeToRefs(userGridType());
+  const { changeGridType } = userGridType();
+
+  const { isFullScreen } = storeToRefs(userSplitScreenFullScreen());
+  const { fullScreen, exitFullscreen } = userSplitScreenFullScreen();
+
+  const { playingGroup, isPlaying, isPaused, playIntervalTime, totalRound, currentRound } = storeToRefs(
+    useCameraGroupList(),
+  );
+  const { groupStartPlay, continuePlay, pausePlay, playPreviousRound, playNextRound } = useCameraGroupList();
+
+  function handelStartPlay() {
+    // 如果输入为0或非数字则返回
+    if (playIntervalTime.value === null || playIntervalTime.value === 0 || !Number(playIntervalTime.value)) {
+      inputRef.value.blur();
+      playIntervalTime.value = 60;
+      return;
+    }
+
+    inputRef.value.blur();
+    playingGroup.value!.playIntervalSec = Number(playIntervalTime.value);
+    groupStartPlay(Number(playIntervalTime.value));
+    // inputRef.value.focus();
+    modifyCameraGroupApi({
+      groupId: playingGroup.value?.id!,
+      playIntervalSec: playIntervalTime.value,
+    });
+  }
+
+  function handleClickLock() {
+    isPaused.value = !isPaused.value;
+    if (isPaused.value) {
+      pausePlay();
+    } else {
+      continuePlay();
+    }
+
+    if (isPlaying.value) {
+      playingGroup.value!.isPaused = isPaused.value;
+      modifyCameraGroupApi({
+        groupId: playingGroup.value?.id!,
+        isPaused: Number(isPaused.value),
+      });
+    }
+  }
+
+  watch(
+    () => isPlaying.value,
+    (newValue) => {
+      if (newValue) {
+        inputRef.value.focus();
+        inputRef.value.select();
+      }
+    },
+  );
+</script>
+
+<style lang="scss" scoped>
+  .control-panel {
+    height: 54px;
+    width: 100%;
+    background: #f4f7ff;
+    display: flex;
+    justify-content: space-between;
+    font-size: 12px;
+    border-radius: 4px 4px 0 0;
+
+    .changeGridBar {
+      display: flex;
+      align-items: center;
+      padding-left: 10px;
+      margin-right: 19px;
+      .changeGrid {
+        margin-right: 32px;
+        .changeGridIcon {
+          width: 25px;
+          color: white;
+        }
+        .changeGridIcon:hover {
+          outline: 1px solid #1777ff;
+          color: #1777ff;
+        }
+      }
+    }
+
+    .controlBtns {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      .lockAndSetTime {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .pausePlay {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          margin-right: 19px;
+          img {
+            height: 24px;
+            width: 24px;
+          }
+        }
+        .setIntervalTime {
+          width: 230px;
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          .intervalTimeInputBar {
+            margin-left: 10px;
+            width: 110px;
+          }
+        }
+      }
+
+      .RoundAndFullScreen {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .controlRound {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          margin-right: 69px;
+          .previousRound,
+          .nextRound {
+            width: 15px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            color: #000;
+            border-radius: 4px;
+            margin-right: 14px;
+            cursor: pointer;
+            &:hover {
+              background-color: rgba(255, 255, 255, 0.2);
+            }
+          }
+          .disableChangeRound {
+            color: #909399;
+            pointer-events: none;
+          }
+          .rounds {
+            display: flex;
+            line-height: 59px;
+            color: #ffffff;
+            font-weight: 400;
+            font-size: 20px;
+            margin-right: 14px;
+            .currentRound {
+              color: #1777ff;
+            }
+            .totalRound {
+              color: rgb(0, 0, 0, 0.3);
+            }
+          }
+        }
+
+        .fullScreenIcon {
+          width: 25px;
+          color: white;
+          margin-right: 20px;
+        }
+        .fullScreenIcon:hover {
+          color: #1777ff;
+        }
+      }
+    }
+  }
+
+  :deep(.el-input) {
+    background-color: #f4f7ff;
+  }
+  :deep(.el-input__inner) {
+    color: #333333;
+    border: 0px;
+  }
+  :deep(.el-input__wrapper) {
+    background-color: #f4f7ff;
+  }
+  :deep(.el-input-group__append) {
+    width: 0px;
+    background-color: #f4f7ff;
+  }
+  :deep(.el-input.is-disabled .el-input__wrapper) {
+    background-color: #f4f7ff;
+  }
+  :deep(.el-tooltip__trigger) {
+    outline: none;
+    border: none;
+  }
+</style>

+ 14 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/VideosGridBase/VideosGridBase.vue

@@ -0,0 +1,14 @@
+<template>
+  <ScreenToolbar />
+  <CamerasGrid :cameraInPlay="props.cameraInPlay" />
+</template>
+
+<script setup lang="ts">
+  import ScreenToolbar from './ScreenToolbar.vue';
+  import CamerasGrid from './CamerasGrid.vue';
+  import { type CameraInPlay } from '../type';
+
+  const props = defineProps<{ cameraInPlay: CameraInPlay[] }>();
+</script>
+
+<style lang="scss" scoped></style>

+ 26 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/hooks/parseData.ts

@@ -0,0 +1,26 @@
+import { CameraGroupListTypeBackEnd, Camera } from '../type';
+import { getVideoRenderUrlKey } from '../utils';
+
+export function parseCameraGroupChildren(groupListBackEnd: CameraGroupListTypeBackEnd) {
+  const groupList = groupListBackEnd as any;
+  for (const group of groupList) {
+    const newChildren: Camera[] = [];
+    group.children.map((x) => {
+      const cameraInfo = {} as Camera;
+      cameraInfo['id'] = x['id'];
+      cameraInfo['code'] = x['code'];
+      cameraInfo['name'] = x['name'];
+      cameraInfo.imageUrl = x.pushStreamDTO['imageUrl'];
+      // cameraInfo.tenantId = x.tenantId;
+      try {
+        cameraInfo['url'] = x.pushStreamDTO['videoUrls'][getVideoRenderUrlKey()];
+      } catch (error) {
+        cameraInfo['url'] = '';
+      }
+      newChildren.push(cameraInfo as Camera);
+    });
+    group.children = newChildren;
+  }
+
+  return groupList;
+}

+ 43 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/hooks/useCameraStatus.ts

@@ -0,0 +1,43 @@
+// 查询相机的在线或者离线状态
+
+import { getCameraState } from '@/api/camera/camera';
+import useCameraGroupList from '@/store/modules/useCameraGroupList';
+import { onMounted, onUnmounted, watch } from 'vue';
+
+export const useCameraStatus = () => {
+  const cameraGroupStore = useCameraGroupList();
+  let timer = 0;
+
+  const queryCameraState = () => {
+    const cameraList = cameraGroupStore.playingGroup?.children || [];
+    if (cameraList.length === 0) return;
+    const cameraCodes = cameraList.map((x) => x.code);
+    getCameraState({ cameraCodeList: cameraCodes }).then((res) => {
+      if (res?.length !== cameraCodes.length) return;
+      cameraGroupStore.updateCameraStatus(res);
+    });
+  };
+
+  watch(
+    () => cameraGroupStore.playingGroup?.children || [],
+    (cameraList) => {
+      if (cameraList.length > 0) {
+        queryCameraState();
+      }
+    },
+    {
+      immediate: true,
+      deep: true,
+    },
+  );
+
+  onMounted(() => {
+    timer = window.setInterval(() => {
+      queryCameraState();
+    }, 1000 * 10);
+  });
+
+  onUnmounted(() => {
+    window.clearInterval(timer);
+  });
+};

+ 11 - 3
src/views/production-safety/risk-identification-and-control/construction-safety-manage/list.vue

@@ -63,16 +63,24 @@
               <el-button
                 type="primary"
                 link
-                @click="$router.push({ name: 'hazardManageEdit', query: { id: scope.row.id } })"
+                @click="$router.push({ name: 'constructionSafetyManageEdit', query: { id: scope.row.id } })"
                 >编辑</el-button
               >
+              <el-button
+                type="primary"
+                link
+                @click="$router.push({ name: 'constructionSafetyManageView', query: { id: scope.row.id } })"
+                >查看</el-button
+              >
 
               <el-button type="primary" link @click="handleConfirmDeleteRow(scope)">删除</el-button>
+
+              <el-button type="primary" link>审批</el-button>
               <el-button
                 type="primary"
                 link
-                @click="$router.push({ name: 'hazardManageView', query: { id: scope.row.id } })"
-                >查看</el-button
+                @click="$router.push({ name: 'constructionSafetyManageMonitor', query: { id: scope.row.id } })"
+                >视频监控</el-button
               >
             </template>
           </el-table-column>

+ 48 - 1
src/views/production-safety/risk-identification-and-control/construction-safety-manage/monitor.vue

@@ -1 +1,48 @@
-<template>monitor</template>
+<template>
+  <div class="nine-square-grid">
+    <div class="leftSideBar">
+      <CameraGroupListAndTree />
+    </div>
+
+    <div class="toolbarAndCamerasGrid">
+      <VideosGridBase :cameraInPlay="cameraInPlay" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted } from 'vue';
+  import CameraGroupListAndTree from './CameraGroupListAndTree/CameraGroupListAndTree.vue';
+  import VideosGridBase from './VideosGridBase/VideosGridBase.vue';
+  import { useCameraGroupList } from '@/store/modules/useCameraGroupList';
+  import { storeToRefs } from 'pinia';
+  import { useTargetTenantIdStore } from '@/store/modules/useTargetTenantIdStore';
+  import { useCameraStatus } from './hooks/useCameraStatus';
+
+  const { cameraInPlay } = storeToRefs(useCameraGroupList());
+
+  const { clear } = useTargetTenantIdStore();
+  useCameraStatus();
+
+  onMounted(() => {
+    clear();
+  });
+</script>
+
+<style lang="scss" scoped>
+  .nine-square-grid {
+    height: 100%;
+    padding: 14px 10px;
+    display: flex;
+    border-radius: 4px;
+    background: #fff;
+
+    .leftSideBar {
+      height: 100%;
+    }
+
+    .toolbarAndCamerasGrid {
+      width: 100%;
+    }
+  }
+</style>

+ 65 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/type.ts

@@ -0,0 +1,65 @@
+export enum GridType {
+  oneGrid = 1,
+  fourGrids = 4,
+  nineGrids = 9,
+  sixteenGrids = 16,
+}
+// 轮播中的摄像头
+export type CameraInPlay = {
+  id: number;
+  cameraGroupDetailId: number; // 相机所属分组id
+  url: string;
+  name: string;
+  code: string;
+  // 相机的缩略图
+  imageUrl: string;
+};
+
+export type Camera = {
+  code: string;
+  name: string;
+  url: string;
+  id: number; // 相机id
+  cameraGroupDetailId: number; // 相机所属分组id
+  /** 相机的缩略图 */
+  imageUrl: string;
+  // /** 租户id */
+  // tenantId: number;
+};
+
+export type CameraGroupType = {
+  id: number;
+  groupName: string;
+  children: Camera[];
+  isDefault: number;
+  playIntervalSec: number;
+  isPaused: boolean;
+};
+
+// 后端返回的数据
+export type cameraWithDetailBackEnd = {
+  code: string;
+  name: string;
+  pushstreamIp: string;
+  pushstreamIpAbs: string;
+  id: number;
+};
+
+// 后端返回的数据
+export type CameraBackEnd = {
+  cameraGroupDetailId: number;
+  cameraWithDetail: cameraWithDetailBackEnd;
+};
+
+// 后端返回的数据
+export type CameraGroupTypeBackEnd = {
+  id: number;
+  groupName: string;
+  children: CameraBackEnd[];
+  isDefault: number;
+  playIntervalSec: number;
+  isPaused: number;
+};
+
+export type CameraGroupListType = CameraGroupType[];
+export type CameraGroupListTypeBackEnd = CameraGroupTypeBackEnd[];

+ 26 - 0
src/views/production-safety/risk-identification-and-control/construction-safety-manage/utils.ts

@@ -0,0 +1,26 @@
+export const SKYEYE_VIDEO_PATH = 'skyeyeVideoPath';
+export const SKYEYE_VIDEO_PATH_MAP = {
+  // 看视频用绝对地址
+  abs: 'abs',
+  // 看视频用相对地址
+  relative: '',
+};
+
+const videoPath = window.localStorage.getItem(SKYEYE_VIDEO_PATH);
+
+export const getVideoRenderUrlKey = () => {
+  // // 总部内网地址的话,用绝对地址去访问
+  // if (isPrivateIp(location.host)) return "pushstreamIpAbs";
+  // // 其他用相对地址
+  // return "pushstreamIp";
+
+  if (videoPath === SKYEYE_VIDEO_PATH_MAP.abs) {
+    return 'pushstreamIpAbs';
+  } else {
+    return 'pushstreamIp';
+  }
+};
+
+// export const isPrivateIp = (host: string) => {
+//   return host.startsWith("10.11");
+// };