Просмотр исходного кода

Merge branch 'feat/production-safety' into 'dev'

Feat/production safety

See merge request product-group-fe/sfy-safety-group/sfy-safety!339
ai0197(吴云丰) 2 месяцев назад
Родитель
Сommit
6e8784e556
54 измененных файлов с 2899 добавлено и 129 удалено
  1. 15 1
      src/api/drawLessons/index.ts
  2. 4 3
      src/api/production-safety-system/index.ts
  3. 46 1
      src/api/production-safety/responsibility-implementation/index.ts
  4. 12 0
      src/api/safety-system-construction-work-plan/index.ts
  5. 33 2
      src/router/routers/production-safety-router/risk-identification-and-control.ts
  6. 8 8
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagement/areaCheckPlanManagement.vue
  7. 2 2
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagement/components/areaCheckPlanManagementDetail.vue
  8. 1 1
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagement/configs/status.ts
  9. 23 23
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagement/configs/tables.ts
  10. 2 2
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagementDept/areaCheckPlanManagementDept.vue
  11. 3 2
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagementDept/components/areaCheckPlanManagementDeptDetail.vue
  12. 4 2
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/components/OneByOneAuditDetail.vue
  13. 3 2
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/components/OneByOneNotifyTarget.vue
  14. 3 3
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/oneByOneManagement.vue
  15. 4 0
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/oneByOneManagementItem.vue
  16. 1 0
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagementDept/components/oneByOneManagementDeptDetail.vue
  17. 5 7
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagementDept/oneByOneManagementDept.vue
  18. 1 0
      src/views/production-safety/implement-safety-duty/create-responsibility-agree.vue
  19. 10 10
      src/views/production-safety/implement-safety-duty/non-public-list-responsibilities/list.vue
  20. 9 1
      src/views/production-safety/implement-safety-duty/public-list-responsibilities/list.vue
  21. 0 1
      src/views/production-safety/implement-safety-duty/responsibility-agree-manage-dept.vue
  22. 15 3
      src/views/production-safety/implement-safety-duty/responsibility-notice-manage-admin/list.vue
  23. 29 11
      src/views/production-safety/implement-safety-duty/responsibility-notice-manage-admin/notice-view.vue
  24. 14 0
      src/views/production-safety/implement-safety-duty/responsibility-notice-manage-admin/review.vue
  25. 1 0
      src/views/production-safety/implement-safety-duty/responsibility-notice-manage-dept/feedback.vue
  26. 2 2
      src/views/production-safety/implement-safety-duty/responsibility-notice-manage-dept/list.vue
  27. 17 7
      src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagement/components/safetySystemConstructionWorkPlanManagementDetail.vue
  28. 2 2
      src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagement/configs/tables.ts
  29. 1 2
      src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagement/safetySystemConstructionWorkPlanManagement.vue
  30. 4 1
      src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagement/safetySystemConstructionWorkPlanManagementViewSender.vue
  31. 40 13
      src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagementDept/components/safetySystemConstructionWorkPlanManagementDeptDetail.vue
  32. 55 3
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/list.vue
  33. 74 0
      src/views/production-safety/risk-identification-and-control/equipment-high-alert/configs/form.ts
  34. 93 0
      src/views/production-safety/risk-identification-and-control/equipment-high-alert/configs/tables.ts
  35. 374 0
      src/views/production-safety/risk-identification-and-control/equipment-high-alert/list.vue
  36. 19 1
      src/views/production-safety/risk-identification-and-control/hazard-approval-manage/list.vue
  37. 51 1
      src/views/production-safety/risk-identification-and-control/hazard-manage/list.vue
  38. 388 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraGroup.vue
  39. 201 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraGroupList.vue
  40. 198 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraListOfGroup.vue
  41. 232 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraTreeOfGroupList.vue
  42. 59 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupListAndTree.vue
  43. 258 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/CamerasGrid.vue
  44. 334 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/ScreenToolbar.vue
  45. 14 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/VideosGridBase/VideosGridBase.vue
  46. 26 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/hooks/parseData.ts
  47. 43 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/hooks/useCameraStatus.ts
  48. 55 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/index.vue
  49. 65 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/type.ts
  50. 26 0
      src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/utils.ts
  51. 4 4
      src/views/production-safety/risk-identification-and-control/risk-manage/list.vue
  52. 1 1
      src/views/production-safety/risk-identification-and-control/work-injury-apply-manage/Item.vue
  53. 7 7
      src/views/production-safety/safetyTrainingAndEducation/educationTrainingPlanManagement/educationTrainingPlanManagement.vue
  54. 8 0
      src/views/production-safety/safetyTrainingAndEducation/educationTrainingPlanManagementDept/educationTrainingPlanManagementDept.vue

+ 15 - 1
src/api/drawLessons/index.ts

@@ -392,7 +392,7 @@ export function exportTheDocumentPost() {
 }
 
 /**
- * 导出举一反三子记录
+ * 管理导出举一反三子记录
  */
 export function exportDrawLessonsAdminPage() {
   return http.request({
@@ -403,3 +403,17 @@ export function exportDrawLessonsAdminPage() {
     isTransformResponse: false,
   });
 }
+
+/**
+ * 部门导出举一反三子记录
+ */
+export function exportDrawLessonsAdminPageDept() {
+  return http.request({
+    url: '/drawLessons/dept/export',
+    method: 'post',
+    responseType: 'blob',
+  }, {
+    isTransformResponse: false,
+  });
+}
+

+ 4 - 3
src/api/production-safety-system/index.ts

@@ -858,7 +858,7 @@ export interface AreaCheckPlanRecord {
   categoryCode?: string;
   checklistTemplateName?: string;
   needOverallDesc?: boolean;
-  needInspectedSign?: boolean;
+  needSigneeSign?: boolean;
   planStartTime?: string;
   planEndTime?: string;
   isDeleted?: number | string;
@@ -880,6 +880,7 @@ export function mapAreaCheckPlanApiRecordToUi(api: AreaCheckPlanManageApiRecord
     id: api.id,
     planName: api.areaCheckPlanName,
     checkVenue: api.checkPlace,
+    createdPersonName: api.createdPersonName,
     venueCategoryName: api.checkPlaceCategory ?? api.checkCategory,
     status: api.status as AreaCheckPlanStatus | undefined,
     primaryResponsibleDeptName: api.primaryResponsibleDeptName ?? api.responsibleDeptName ?? api.responsibleDept,
@@ -908,7 +909,7 @@ export function mapAreaCheckPlanApiRecordToUi(api: AreaCheckPlanManageApiRecord
     checklistTemplateName: api.checklistTemplateName,
     checkKeyContent: api.checkKeyContent,
     needOverallDesc: toBool(api.needOverallDesc),
-    needInspectedSign: toBool(api.needSigneeSign),
+    needSigneeSign: toBool(api.needSigneeSign),
     planStartTime: api.planStartTime,
     planEndTime: api.planEndTime,
     isDeleted: api.isDeleted,
@@ -958,7 +959,7 @@ function uiRecordToApi(ui: AreaCheckPlanRecord & { id?: number }): AreaCheckPlan
     checklistTemplateName: ui.checklistTemplateName,
     checkKeyContent: ui.checkKeyContent,
     needOverallDesc: toNum(ui.needOverallDesc),
-    needSigneeSign: toNum(ui.needInspectedSign),
+    needSigneeSign: toNum(ui.needSigneeSign),
     planStartTime: ui.planStartTime,
     planEndTime: ui.planEndTime,
     status: ui.status as number | undefined,

+ 46 - 1
src/api/production-safety/responsibility-implementation/index.ts

@@ -978,8 +978,53 @@ export function dangerWorkSaveApproval(params) {
  */
 export function exportRiskList () {
   return http.request({
-    url: `/safetyRisk/list/exportRiskList`,
+    url: '/safetyRisk/list/exportRiskList',
     method: 'post',
+    responseType: 'blob',
+  }, {
+    isTransformResponse: false,
   });
 }
 
+/**
+ * 导出危险清单管理
+ * @returns Promise<any> 导出的危险清单数据
+ */
+export function exportHazardList () {
+  return http.request({
+    url: '/safetyHazardInventory/exportHazard',
+    method: 'post',
+    responseType: 'blob',
+  }, {
+    isTransformResponse: false,
+  });
+}
+
+/**
+ * 导出施工安全管理
+ * @returns Promise<any> 导出的施工安全管理数据
+ */
+export function exportConstructionSafetyList () {
+  return http.request({
+    url: '/constructionSafety/exportConstruction',
+    method: 'post',
+    responseType: 'blob',
+  }, {
+    isTransformResponse: false,
+  });
+}
+
+
+/**
+ * 导出危险作业审批管理
+ * @returns Promise<any> 导出的危险作业审批管理数据
+ */
+export function exportHazardApprovalList () {
+  return http.request({
+    url: '/dangerWork/exportDangerWorkApproval',
+    method: 'post',
+    responseType: 'blob',
+  }, {
+    isTransformResponse: false,
+  });
+}

+ 12 - 0
src/api/safety-system-construction-work-plan/index.ts

@@ -113,6 +113,7 @@ export interface ViewSenderQueryPageResponse {
   planId: number;
   plannedComplateTime: string;
   responsibleDeptName: string;
+  responsibleDeptNames: string;
   status: number;
   statusName?: any;
   trainingPlanName: string;
@@ -164,11 +165,22 @@ export function updateWorkPlan(data: UpdateWorkPlanRequest) {
 export function queryWorkPlanDetail(id: number) {
   return http.request<SaveWorkPlanRequest>({
     url: `${ADMIN_BASE}/queryDetail`,
+    // url: `/safetyWorkPlan/workPlanSendObj/queryDetail?id=${id}`,
     method: 'get',
     params: { id },
   });
 }
 
+/**
+ * 查询部门安全体系建设工作计划详情
+ */
+export function queryWorkPlanDepartmentDetail(id: number) {
+  return http.request<SaveWorkPlanRequest>({
+    url: `/safetyWorkPlan/workPlanSendObj/queryDetail?id=${id}`,
+    method: 'get',
+  });
+}
+
 /**
  * 删除安全体系建设工作计划
  */

+ 33 - 2
src/router/routers/production-safety-router/risk-identification-and-control.ts

@@ -181,7 +181,7 @@ const riskIdentificationAndControlRoutes: RouteComponent[] = [{
         noCache: false,
       }
     },
-
+    
     // 施工作业安全管理
     {
       id: 93008,
@@ -382,6 +382,22 @@ const riskIdentificationAndControlRoutes: RouteComponent[] = [{
         noCache: false,
       }
     },
+    // 设备告警管理
+    {
+      id: 93036,
+      parentId: 90014,
+      name: 'equipmentHighAlert',
+      path: 'equipment-high-alert',
+      component: '/production-safety/risk-identification-and-control/equipment-high-alert/list',
+      meta: {
+        title: '设备告警管理',
+        icon: 'OverviewIcon',
+        activeMenu: '/work-safety/risk-identification-and-control/equipment-high-alert',
+        isRoot: false,
+        hidden: false,
+        noCache: false,
+      }
+    },
     {
       id: 93019,
       parentId: 90014,
@@ -587,6 +603,7 @@ const riskIdentificationAndControlRoutes: RouteComponent[] = [{
       component: '/production-safety/risk-identification-and-control/work-injury-apply-manage/list',
       meta: {
         title: '工伤认定申请',
+        activeMenu: '/work-safety/risk-identification-and-control/work-injury-apply-manage',
         icon: 'OverviewIcon',
         isRoot: false,
         hidden: false,
@@ -638,7 +655,21 @@ const riskIdentificationAndControlRoutes: RouteComponent[] = [{
         noCache: false,
       }
     },
+    {
+      id: 930038,
+      parentId: 90014,
+      name: 'keySiteSensorManage',
+      path: 'key-site-sensor-manage',
+      component: '/production-safety/risk-identification-and-control/key-site-sensor-manage/index',
+      meta: {
+        title: '重点部位传感器管理',
+        activeMenu: '/work-safety//risk-identification-and-control/key-site-sensor-manage',
+        icon: 'OverviewIcon',
+        isRoot: false,
+        hidden: false,
+        noCache: false,
+      }
+    },
   ],
 }];
-
 export default riskIdentificationAndControlRoutes; 

+ 8 - 8
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagement/areaCheckPlanManagement.vue

@@ -88,8 +88,8 @@
             <template #needOverallDesc="scope">
               <span>{{ scope.row.needOverallDesc === true ? '是' : scope.row.needOverallDesc === false ? '否' : '-' }}</span>
             </template>
-            <template #needInspectedSign="scope">
-              <span>{{ scope.row.needInspectedSign === true ? '是' : scope.row.needInspectedSign === false ? '否' : '-' }}</span>
+            <template #needSigneeSign="scope">
+              <span>{{ scope.row.needSigneeSign === true ? '是' : scope.row.needSigneeSign === false ? '否' : '-' }}</span>
             </template>
             <template #action="scope">
               <div class="action-container--div" style="justify-content: left">
@@ -258,8 +258,8 @@
             <el-radio :value="false">否</el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label="是否需要被检查人签字:" prop="needInspectedSign">
-          <el-radio-group v-model="issueForm.needInspectedSign">
+        <el-form-item label="是否需要被检查人签字:" prop="needSigneeSign">
+          <el-radio-group v-model="issueForm.needSigneeSign">
             <el-radio :value="true">是</el-radio>
             <el-radio :value="false">否</el-radio>
           </el-radio-group>
@@ -321,7 +321,7 @@
     planStartTime: '',
     planEndTime: '',
     needOverallDesc: false,
-    needInspectedSign: false,
+    needSigneeSign: false,
   });
   const issueRules = {
     responsibleDeptIds: [{ required: true, message: '请选择责任部门', trigger: 'change', type: 'array', min: 1 }],
@@ -333,7 +333,7 @@
     planStartTime: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
     planEndTime: [{ required: true, message: '请选择计划结束时间', trigger: 'change' }],
     needOverallDesc: [{ required: true, message: '请选择是否需要整体检查情况描述', trigger: 'change' }],
-    needInspectedSign: [{ required: true, message: '请选择是否需要被检查人签字', trigger: 'change' }],
+    needSigneeSign: [{ required: true, message: '请选择是否需要被检查人签字', trigger: 'change' }],
   };
   // 下发弹窗:责任部门/安全应急部门/院领导部门复用新增区域检查计划的主责部门下拉(getAllDepartments)
   const issueDeptTree = ref<DeptTree[]>([]);
@@ -415,7 +415,7 @@
     issueForm.planStartTime = '';
     issueForm.planEndTime = '';
     issueForm.needOverallDesc = false;
-    issueForm.needInspectedSign = false;
+    issueForm.needSigneeSign = false;
     issueFormRef.value?.resetFields?.();
   };
 
@@ -572,7 +572,7 @@
         planStartTime: issueForm.planStartTime,
         planEndTime: issueForm.planEndTime,
         needOverallDesc: issueForm.needOverallDesc,
-        needInspectedSign: issueForm.needInspectedSign,
+        needSigneeSign: issueForm.needSigneeSign,
       } as AreaCheckPlanRecord & { id: number } & Record<string, unknown>;
 
       await sendAreaCheckPlanToDep(payload);

+ 2 - 2
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagement/components/areaCheckPlanManagementDetail.vue

@@ -103,7 +103,7 @@
           </div>
           <div class="col">
             <div class="label">是否需要被检查人签字:</div>
-            <div class="value">{{ viewDetail.needInspectedSign === true ? '是' : viewDetail.needInspectedSign === false ? '否' : '-' }}</div>
+            <div class="value">{{ viewDetail.needSigneeSign === true ? '是' : viewDetail.needSigneeSign === false ? '否' : '-' }}</div>
           </div>
         </div>
         <div class="row">
@@ -648,7 +648,7 @@
       categoryName: d?.categoryName ?? ruleFormData.categoryName ?? '-',
       checklistTemplateName: d?.checklistTemplateName ?? ruleFormData.checklistTemplateName ?? '-',
       needOverallDesc: d?.needOverallDesc ?? ruleFormData.needOverallDesc,
-      needInspectedSign: d?.needInspectedSign ?? ruleFormData.needInspectedSign,
+      needSigneeSign: d?.needSigneeSign ?? ruleFormData.needSigneeSign,
       planStartTime: d?.planStartTime ?? ruleFormData.planStartTime ?? '-',
       planEndTime: d?.planEndTime ?? ruleFormData.planEndTime ?? '-',
        createdPersonName: d?. createdPersonName ?? '-',

+ 1 - 1
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagement/configs/status.ts

@@ -1,7 +1,7 @@
 // 区域检查计划状态配置(管理员与部门一致):0=未下发 1=进行中 2=已完成 3=已作废
 export const AREA_CHECK_PLAN_STATUS_OPTIONS = [
   { label: '全部', value: '' as const },
-  { label: '未下发', value: 0 },
+//   { label: '未下发', value: 0 },
   { label: '进行中', value: 1 },
   { label: '已完成', value: 2 },
   { label: '已作废', value: 3 },

+ 23 - 23
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagement/configs/tables.ts

@@ -29,104 +29,104 @@ export const AREA_CHECK_PLAN_TABLE_COLUMNS: TableColumnProps[] = [
     prop: 'status',
     slot: 'status',
     align: 'center',
-    minWidth: '90px',
+    minWidth: '120px',
   },
   {
     label: '检查场所所属类别',
     prop: 'venueCategoryName',
     align: 'left',
-    minWidth: '140px',
+    minWidth: '180px',
     showOverflowTooltip: true,
   },
   {
     label: '区域检查计划名称',
     prop: 'planName',
     align: 'left',
-    minWidth: '160px',
+    minWidth: '180px',
     showOverflowTooltip: true,
   },
   {
     label: '主责部门',
     prop: 'primaryResponsibleDeptName',
     align: 'left',
-    minWidth: '120px',
+    minWidth: '140px',
     showOverflowTooltip: true,
   },
   {
     label: '自查频次',
     prop: 'selfCheckFrequency',
     align: 'left',
-    minWidth: '100px',
+    minWidth: '140px',
     showOverflowTooltip: true,
   },
   {
     label: '主责部门执行人所属分组名称',
     prop: 'mainDeptExecutorGroupName',
     align: 'left',
-    minWidth: '180px',
+    minWidth: '240px',
     showOverflowTooltip: true,
   },
   {
     label: '主责部门责任人',
     prop: 'mainDeptResponsiblePerson',
     align: 'left',
-    minWidth: '120px',
+    minWidth: '160px',
     showOverflowTooltip: true,
   },
   {
     label: '安全应急部门名称',
     prop: 'safetyEmergencyDeptName',
     align: 'left',
-    minWidth: '140px',
+    minWidth: '180px',
     showOverflowTooltip: true,
   },
   {
     label: '安全应急部检查频次',
     prop: 'safetyEmergencyCheckFrequency',
     align: 'left',
-    minWidth: '150px',
+    minWidth: '180px',
     showOverflowTooltip: true,
   },
   {
     label: '安全应急部执行人所属分组名称',
     prop: 'safetyEmergencyExecutorGroupName',
     align: 'left',
-    minWidth: '200px',
+    minWidth: '260px',
     showOverflowTooltip: true,
   },
   {
     label: '安全应急部责任人',
     prop: 'safetyEmergencyResponsiblePerson',
     align: 'left',
-    minWidth: '130px',
+    minWidth: '180px',
     showOverflowTooltip: true,
   },
   {
     label: '院领导部门名称',
     prop: 'hospitalLeaderDeptName',
     align: 'left',
-    minWidth: '130px',
+    minWidth: '180px',
     showOverflowTooltip: true,
   },
   {
     label: '院领导检查频次',
     prop: 'hospitalLeaderCheckFrequency',
     align: 'left',
-    minWidth: '140px',
+    minWidth: '160px',
     showOverflowTooltip: true,
   },
   {
     label: '院领导执行人所属分组名称',
     prop: 'hospitalLeaderExecutorGroupName',
     align: 'left',
-    minWidth: '200px',
+    minWidth: '220px',
     showOverflowTooltip: true,
   },
   {
     label: '院领导责任人',
     prop: 'hospitalLeaderResponsiblePerson',
     align: 'left',
-    minWidth: '120px',
+    minWidth: '140px',
     showOverflowTooltip: true,
   },
   {
@@ -140,14 +140,14 @@ export const AREA_CHECK_PLAN_TABLE_COLUMNS: TableColumnProps[] = [
     label: '检查单所属类别名称',
     prop: 'categoryName',
     align: 'left',
-    minWidth: '150px',
+    minWidth: '180px',
     showOverflowTooltip: true,
   },
   {
     label: '检查单模版名称',
     prop: 'checklistTemplateName',
     align: 'left',
-    minWidth: '140px',
+    minWidth: '160px',
     showOverflowTooltip: true,
   },
   {
@@ -155,26 +155,26 @@ export const AREA_CHECK_PLAN_TABLE_COLUMNS: TableColumnProps[] = [
     prop: 'needOverallDesc',
     slot: 'needOverallDesc',
     align: 'center',
-    minWidth: '180px',
+    minWidth: '240px',
   },
   {
     label: '是否需要被检查人签字',
-    prop: 'needInspectedSign',
-    slot: 'needInspectedSign',
+    prop: 'needSigneeSign',
+    slot: 'needSigneeSign',
     align: 'center',
-    minWidth: '160px',
+    minWidth: '200px',
   },
   {
     label: '计划开始时间',
     prop: 'planStartTime',
     align: 'left',
-    minWidth: '120px',
+    minWidth: '160px',
   },
   {
     label: '计划结束时间',
     prop: 'planEndTime',
     align: 'left',
-    minWidth: '120px',
+    minWidth: '160px',
   },
   {
     label: '操作',

+ 2 - 2
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagementDept/areaCheckPlanManagementDept.vue

@@ -84,8 +84,8 @@
             <template #needOverallDesc="scope">
               <span>{{ scope.row.needOverallDesc === true ? '是' : scope.row.needOverallDesc === false ? '否' : '-' }}</span>
             </template>
-            <template #needInspectedSign="scope">
-              <span>{{ scope.row.needInspectedSign === true ? '是' : scope.row.needInspectedSign === false ? '否' : '-' }}</span>
+            <template #needSigneeSign="scope">
+              <span>{{ scope.row.needSigneeSign === true ? '是' : scope.row.needSigneeSign === false ? '否' : '-' }}</span>
             </template>
             <template #action="scope">
               <div class="action-container--div" style="justify-content: left">

+ 3 - 2
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/areaCheckPlanManagementDept/components/areaCheckPlanManagementDeptDetail.vue

@@ -103,7 +103,7 @@
           </div>
           <div class="col">
             <div class="label">是否需要被检查人签字:</div>
-            <div class="value">{{ viewDetail.needInspectedSign === true ? '是' : viewDetail.needInspectedSign === false ? '否' : '-' }}</div>
+            <div class="value">{{ viewDetail.needSigneeSign === true ? '是' : viewDetail.needSigneeSign === false ? '否' : '-' }}</div>
           </div>
         </div>
         <div class="row">
@@ -357,6 +357,7 @@
   const viewDetail = computed(() => {
     const d = viewDetailData.value;
     const status = d?.status as number | undefined;
+    console.log(d,'d')
     return {
       ...d,
       statusName: status != null ? AREA_CHECK_PLAN_STATUS_LABEL[String(status)] ?? '-' : '-',
@@ -380,7 +381,7 @@
       categoryName: (d?.categoryName ?? d?.checklistCategoryName) ?? '-',
       checklistTemplateName: d?.checklistTemplateName ?? '-',
       needOverallDesc: d?.needOverallDesc,
-      needInspectedSign: d?.needInspectedSign,
+      needSigneeSign: d?.needSigneeSign,
       planStartTime: d?.planStartTime ?? '-',
       planEndTime: d?.planEndTime ?? '-',
       createdPersonName: (d?.createdPersonName ?? '') || '-',

+ 4 - 2
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/components/OneByOneAuditDetail.vue

@@ -96,8 +96,8 @@
     </main>
     <footer class="safety-platform-container__footer">
       <el-button @click="router.back()">返回</el-button>
-      <el-button type="primary" @click="handleReject">审核不通过</el-button>
-      <el-button type="primary" @click="handleApprove">审核通过</el-button>
+      <el-button type="primary" @click="handleReject" v-if="!isViewMode">审核不通过</el-button>
+      <el-button type="primary" @click="handleApprove" v-if="!isViewMode">审核通过</el-button>
     </footer>
 
     <el-dialog
@@ -139,6 +139,8 @@
   const router = useRouter();
   const route = useRoute();
   const id = computed(() => Number(route.query.id));
+  const operate = computed(() => (route.query.operate as string) || 'one-by-one-audit-detail');
+  const isViewMode = computed(() => operate.value === 'one-by-one-audit-view');
 
   const detailData = ref<{
     problem?: string;

+ 3 - 2
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/components/OneByOneNotifyTarget.vue

@@ -333,8 +333,9 @@
     router.push({
       name: 'oneByOneManagementItem',
       query: {
-        id: String(row.associationOtId),
-        operate: 'one-by-one-view',
+        id: String(row.id),
+        // operate: 'one-by-one-view',
+        operate: 'one-by-one-audit-view',
       },
     });
   };

+ 3 - 3
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/oneByOneManagement.vue

@@ -10,9 +10,6 @@
             <el-button type="primary" class="search-table-container--button" @click="handleCreate">
               新增举一反三
             </el-button>
-            <el-button plain class="search-table-container--button" @click="handleDownload">
-              导出
-            </el-button>
           </div>
 
           <div class="act-search">
@@ -56,6 +53,9 @@
             <section class="search-btn">
               <el-button type="primary" @click="handleSearch">查询</el-button>
               <el-button @click="handleReset">重置</el-button>
+              <el-button plain @click="handleDownload">
+                导出
+              </el-button>
             </section>
           </div>
         </header>

+ 4 - 0
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/oneByOneManagementItem.vue

@@ -28,6 +28,8 @@
         return '通知对象';
       case 'one-by-one-audit-detail':
         return '审核';
+      case 'one-by-one-audit-view':
+        return '审核查看';
       default:
         return '未知操作';
     }
@@ -39,6 +41,8 @@
         return defineAsyncComponent(() => import('./components/OneByOneNotifyTarget.vue'));
       case 'one-by-one-audit-detail':
         return defineAsyncComponent(() => import('./components/OneByOneAuditDetail.vue'));
+      case 'one-by-one-audit-view':
+        return defineAsyncComponent(() => import('./components/OneByOneAuditDetail.vue'));
       default:
         return defineAsyncComponent(() => import('./components/oneByOneManagementDetail.vue'));
     }

+ 1 - 0
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagementDept/components/oneByOneManagementDeptDetail.vue

@@ -344,6 +344,7 @@
       if (data && typeof data === 'object') {
         detailData.value = data;
         hasProblem.value = Number(data.feedbackHasIssue) === 1 ? true : false;
+        materialAttachmentList.value = JSON.parse(data.attachments || '[]');
       }
     } catch (e) {
       console.error('获取举一反三详情失败:', e);

+ 5 - 7
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagementDept/oneByOneManagementDept.vue

@@ -6,11 +6,6 @@
     <main class="safety-platform-container__main">
       <div class="search-table-container">
         <header>
-          <div style="position: relative">
-            <el-button plain class="search-table-container--button" @click="handleDownload">
-              导出
-            </el-button>
-          </div>
 
           <div class="act-search">
             <section class="select-box">
@@ -52,6 +47,9 @@
             <section class="search-btn">
               <el-button type="primary" @click="handleSearch">查询</el-button>
               <el-button @click="handleReset">重置</el-button>
+              <el-button plain @click="handleDownload">
+                导出
+              </el-button>
             </section>
           </div>
         </header>
@@ -108,7 +106,7 @@
   import type { QueryPageRequest } from '@/types/basic-query';
   import {
     queryDrawLessonsAdminDeptPage,
-    exportDrawLessonsAdminPage,
+    exportDrawLessonsAdminPageDept,
     type DrawLessonsQueryParam,
   } from '@/api/drawLessons';
   import { downloadByData } from '@/utils/file/download';
@@ -209,7 +207,7 @@
 
   const handleDownload = async () => {
     try {
-      const response = await exportDrawLessonsAdminPage();
+      const response = await exportDrawLessonsAdminPageDept();
       if (response) {
         const fileName = `举一反三_${new Date().toISOString().split('T')[0]}.xlsx`;
         downloadByData(response, fileName);

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

@@ -32,6 +32,7 @@
             :maxCount="1"
             @uploadSuccess="handleUploadSuccess"
             :fileList="formValue.attachment"
+            :desc="'支持格式:.rar .zip .doc .docx .doc .pdf .png .jpg .jpeg .mp4 .xlsx .pptx,单个文件不能超过20MB'"
           />
         </el-form-item>
 

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

@@ -18,6 +18,7 @@
           "
           >添加
         </el-button>
+        <el-button @click="batchImportVisible = true">导入</el-button>
       </div>
       <div class="search-form">
         <el-form :inline="true">
@@ -57,11 +58,9 @@
         </el-form>
 
         <div>
-          <!-- <el-button @click="batchImportVisible = true">导入</el-button>
-          <el-button type="primary" @click="handleDownload">导出</el-button> -->
-
           <el-button type="primary" @click="queryTableList">查询</el-button>
           <el-button @click="handleRestParams">重置</el-button>
+          <el-button @click="handleDownload">导出</el-button>
         </div>
       </div>
 
@@ -166,7 +165,7 @@
     :visible="batchImportVisible"
     :importApiUrl="importApiUrl"
     :templateUrl="templateUrl"
-    :templateName="'责任清单-批量导入模版'"
+    :templateName="'责任清单(非区域管理员)-批量导入模版'"
     @close="() => (batchImportVisible = false)"
     @update="handleUpdate"
   />
@@ -191,6 +190,7 @@
   import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
   import { getAllDepartments } from '@/api/auth/dept';
   import { useUserInfoHook } from '@/views/disaster/hooks';
+  import { downloadByData } from '@/utils/file/download';
 
   const { id } = useUserInfoHook();
   const router = useRouter();
@@ -259,13 +259,13 @@
   };
 
   async function handleDownload() {
-    // getQuery();
     try {
-      const res = await areaCheckListExportArea(queryParams.queryParam);
-      if (res.size === 0) return;
-      const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
-      const url = window.URL.createObjectURL(blob);
-      downloadFile(url, '责任清单.xlsx');
+      const response = await areaCheckListExportArea(queryParams.queryParam);
+        if (response) {
+            const fileName = `责任清单(非区域管理员)_${new Date().toISOString().split('T')[0]}.xlsx`;
+            downloadByData(response, fileName);
+            ElMessage.success('导出成功');
+        }
     } catch (e) {
       ElMessage.error('下载失败');
       console.log(e);

+ 9 - 1
src/views/production-safety/implement-safety-duty/public-list-responsibilities/list.vue

@@ -314,7 +314,15 @@
     });
   };
   const queryTableList = () => {
-    areaCheckListQueryPage(queryParams).then((res) => {
+    let params = {
+        pageNumber: queryParams.pageNumber,
+        pageSize: queryParams.pageSize,
+        queryParam:{
+            ...queryParams.queryParam,
+            safetyResponsibleDepartmentId: JSON.stringify(queryParams.queryParam.safetyResponsibleDepartmentId)
+        }
+    }
+    areaCheckListQueryPage(params).then((res) => {
       tableData.data = res.records;
       tableData.total = res.totalRow;
     });

+ 0 - 1
src/views/production-safety/implement-safety-duty/responsibility-agree-manage-dept.vue

@@ -23,7 +23,6 @@
           </el-form-item>
           <el-form-item label="状态">
             <el-select v-model="queryParams.queryParam.status" clearable placeholder="状态" style="width: 170px">
-              <el-option :value="1" label="未下发" />
               <el-option :value="2" label="待签署" />
               <el-option :value="3" label="待反馈材料" />
               <el-option :value="4" label="待审核" />

+ 15 - 3
src/views/production-safety/implement-safety-duty/responsibility-notice-manage-admin/list.vue

@@ -70,9 +70,21 @@
           <el-table-column label="责任通知名称" prop="responsibilityName" width="180" />
           <el-table-column label="状态" prop="statusName" width="100" />
           <!-- <el-table-column label="类型" prop="safetyAreaName" width="180" /> -->
-          <el-table-column label="下发数" prop="issuedQuantity" width="120" />
-          <el-table-column label="反馈人数" prop="signedQuantity" width="120" />
-          <el-table-column label="反馈比例" prop="signedRatio" width="120" />
+          <el-table-column label="下发数" prop="issuedQuantity" width="120">
+            <template #default="scope">
+              {{ scope.row.status === 1 ? '-' : scope.row.issuedQuantity }}
+            </template>
+          </el-table-column>
+          <el-table-column label="反馈人数" prop="signedQuantity" width="120">
+            <template #default="scope">
+              {{ scope.row.status === 1 ? '-' : scope.row.signedQuantity }}
+            </template>
+          </el-table-column>
+          <el-table-column label="反馈比例" prop="signedRatio" width="120">
+            <template #default="scope">
+              {{ scope.row.status === 1 ? '-' : scope.row.signedRatio }}
+            </template>
+          </el-table-column>
           <el-table-column label="责任通知文档" prop="attachment" width="250">
             <template #default="scope">
               <!-- <div

+ 29 - 11
src/views/production-safety/implement-safety-duty/responsibility-notice-manage-admin/notice-view.vue

@@ -31,16 +31,9 @@
               <el-option :value="6" label="已作废" />
             </el-select>
           </el-form-item> -->
-          <el-form-item label="区域类型">
-            <el-select v-model="queryParams.queryParam.type" clearable placeholder="选择区域类型" style="width: 170px">
-              <el-option :value="1" label="公共区域" />
-              <el-option :value="2" label="非公共区域" />
-            </el-select>
-          </el-form-item>
-
-          <el-form-item label="分组名称">
-            <el-select v-model="queryParams.queryParam.userGroupId" placeholder="分组名称" style="width: 170px">
-              <el-option v-for="group in groupList" :key="group.id" :label="group.name" :value="group.id" />
+          <el-form-item label="通知区域">
+            <el-select v-model="queryParams.queryParam.safetyAreaId" clearable placeholder="选择通知区域" style="width: 170px">
+                <el-option v-for="group in areaCheckListQueryList" :key="group.id" :label="group.nameFunction" :value="group.id" />
             </el-select>
           </el-form-item>
           <el-form-item label="计划日期">
@@ -53,6 +46,9 @@
               style="width: 230px"
             />
           </el-form-item>
+            <el-form-item label="关键词">
+            <el-input v-model="queryParams.queryParam.keyWord" placeholder="关键词内容" style="width: 170px"></el-input>
+          </el-form-item>
         </el-form>
 
         <div>
@@ -66,7 +62,7 @@
         <el-table :data="tableData.data">
           <el-table-column label="责任通知名称" prop="responsibilityName" width="180" />
           <el-table-column label="状态" prop="statusName" width="100" />
-          <el-table-column label="类别名称" prop="safetyAreaTypeName" />
+          <el-table-column label="类" prop="safetyAreaTypeName" />
 
           <el-table-column label="通知区域" prop="safetyAreaName" />
           <el-table-column label="安全责任人" prop="responsiblePersonName" />
@@ -130,6 +126,7 @@
     safetyNoticeAdminExportIssuedObject,
     safetyResponsibilityAdminDeleteIssuedObject,
     safetyNoticeAdminScrap,
+    areaCheckListQueryPage,
   } from '@/api/production-safety/responsibility-implementation';
   import { downloadByData } from '@/utils/file/download';
   import { unformatAttachment } from '@/components/UploadFiles/utils';
@@ -148,9 +145,17 @@
       status: '',
       date: '',
       type: route.query.type || '',
+      safetyAreaId: '',
+      keyWord: '',
     },
   });
 
+  const queryParamsTow = reactive<any>({
+    pageNumber: 1,
+    pageSize: 9999,
+    queryParam: {},
+  });
+
   const detailData = reactive({
     createdAt: '',
     createdByName: '',
@@ -163,6 +168,8 @@
     total: 0,
   });
 
+  const areaCheckListQueryList = ref<any>([]);
+
   const handleQueryUserGroupPage = () => {
     queryUserGroupPage({
       pageNumber: 1,
@@ -255,11 +262,19 @@
         status: '',
         date: '',
         userGroupId: '',
+        safetyAreaId: '',
+        keyWord: '',
       },
     });
     queryTableList();
   };
 
+  const areaCheckListPost = () => {
+    areaCheckListQueryPage(queryParamsTow).then((res) => {
+      areaCheckListQueryList.value = res.records;
+    });
+  };
+
   watch(
     () => activeTab.value,
     (a) => {
@@ -280,6 +295,9 @@
     await handleQueryUserGroupPage();
 
     queryTableList();
+
+    areaCheckListPost();
+
   });
 </script>
 

+ 14 - 0
src/views/production-safety/implement-safety-duty/responsibility-notice-manage-admin/review.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="safety-platform-container">
     <header class="safety-platform-container__header">
+      <div class="back"><BreadcrumbBack /> 安全责任通知管理审核 </div>
       <div class="detail-content">
         <span>类型:{{ formData?.departmentName }} </span>
         <span>创建人:{{ formData?.createdByName }} </span>
@@ -67,6 +68,18 @@
               </div>
             </div>
           </div>
+          <div class="row">
+            <div class="col">
+              <div class="label">责任通知内容:</div>
+              <div class="value value-s1">
+                <div class="file-list">
+                  <div class="file-item">
+                    <div v-html="formData.content"></div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
 
         <h4>材料上传</h4>
@@ -206,6 +219,7 @@
             attachment: unformatAttachment(res.attachment),
             signsUpload: unformatAttachment(res.signsUpload),
             feedback: unformatAttachment(res.feedback),
+            content: res.content,
           });
         }
       });

+ 1 - 0
src/views/production-safety/implement-safety-duty/responsibility-notice-manage-dept/feedback.vue

@@ -280,6 +280,7 @@
             attachment: unformatAttachment(res.attachment),
             signsUpload: unformatAttachment(res.signsUpload),
             feedback: unformatAttachment(res.feedback),
+            content: res.content,
           });
         }
       });

+ 2 - 2
src/views/production-safety/implement-safety-duty/responsibility-notice-manage-dept/list.vue

@@ -23,7 +23,7 @@
           </el-form-item>
           <el-form-item label="状态">
             <el-select v-model="queryParams.queryParam.status" clearable placeholder="状态" style="width: 170px">
-              <el-option :value="1" label="未下发" />
+              <!-- <el-option :value="1" label="未下发" /> -->
               <el-option :value="2" label="待反馈" />
               <el-option :value="3" label="待审核" />
               <el-option :value="4" label="已完成" />
@@ -66,7 +66,7 @@
         <el-table :data="tableData.data">
           <el-table-column label="责任通知名称" prop="responsibilityName" />
           <el-table-column label="状态" prop="statusName" width="100" />
-          <!-- <el-table-column label="类型" prop="safetyAreaName" width="130" /> -->
+          <el-table-column label="类型" prop="safetyAreaName" width="130" />
           <!-- <el-table-column label="下发数" prop="issuedQuantity" width="120" />
           <el-table-column label="反馈人数" prop="signedQuantity" width="120" />
           <el-table-column label="反馈比例" prop="signedRatio" width="120" /> -->

+ 17 - 7
src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagement/components/safetySystemConstructionWorkPlanManagementDetail.vue

@@ -70,13 +70,22 @@
         />
       </el-form-item>
       <el-form-item label="附件上传:" v-if="isViewMode" prop="fileUrl" :required="isViewMode">
-        <UploadFiles
-          label="上传附件"
-          :file-list="attachmentFileList"
-          :maxCount="1"
-          @uploadSuccess="handleAttachmentUploadSuccess"
-          @preview="handlePreview"
-        />
+        <div class="file-list" v-if="form.fileUrl && form.fileUrl.length != 0">
+          <div class="file-item" v-for="file in form.fileUrl" :key="file.fileId">
+            <span class="file-item--name">{{ file.fileName }}</span>
+            <div class="file-item--footer">
+              <el-button link type="primary" @click="previewOnline(file.fileUrl, file.fileType)"
+                >预览</el-button
+              >
+              <el-button link type="primary" @click.stop="downloadFile(file.fileUrl, file.fileName)"
+                >下载</el-button
+              >
+            </div>
+          </div>
+        </div>
+        <div v-else>
+          暂无附件
+        </div>
       </el-form-item>
     </el-form>
   </main>
@@ -261,6 +270,7 @@
           ...res,
           cooperateDeptIds: cooperateDeptIdsArray,
           responsibleDeptIds: responsibleDeptIdsArray,
+          fileUrl: JSON.parse(res.fileUrl || '[]'),
         });
       }
       // cloneRuleFormData();

+ 2 - 2
src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagement/configs/tables.ts

@@ -74,7 +74,7 @@ export const WORK_PLAN_TABLE_COLUMNS: TableColumnProps[] = [
   },
   {
     label: '责任部门',
-    prop: 'responsibleDeptName',
+    prop: 'responsibleDeptNames',
     align: 'left',
     minWidth: '120px',
   },
@@ -101,7 +101,7 @@ export const WORK_PLAN_TABLE_COLUMNS: TableColumnProps[] = [
   },
   {
     label: '计划完成时间',
-    prop: 'plannedComplateTime',
+    prop: 'plannedEndTime',
     align: 'left',
     minWidth: '160px',
   },

+ 1 - 2
src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagement/safetySystemConstructionWorkPlanManagement.vue

@@ -85,6 +85,7 @@
                 <!-- 1-未下发、2-待反馈、3-已完成 4- 已作废 -->
                 <!-- 未下发(1):编辑、删除、下发 -->
                 <template v-if="Number(scope.row.status) === 1">
+                  <ActionButton text="查看" @click="handleView(scope.row.id)" />
                   <ActionButton text="编辑" @click="handleEdit(scope.row.id)" />
                   <ActionButton
                     text="删除"
@@ -98,7 +99,6 @@
 
                 <!-- (2)待反馈-->
                 <template v-else-if="Number(scope.row.status) === 2">
-                  <ActionButton text="查看" @click="handleView(scope.row.id)" />
                   <ActionButton text="查看发送对象" @click="handleViewRecipients(scope.row.id)" />
                   <ActionButton
                     text="作废"
@@ -118,7 +118,6 @@
                     }"
                     @confirm="handleDelete(scope.row.id)"
                   />
-                  <ActionButton text="查看" @click="handleView(scope.row.id)" />
                   <ActionButton text="发送查看对象" @click="handleViewRecipients(scope.row.id)" />
                 </template>
               </div>

+ 4 - 1
src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagement/safetySystemConstructionWorkPlanManagementViewSender.vue

@@ -219,6 +219,9 @@ import BreadcrumbBack from '@/components/BreadcrumbBack.vue';
       }
       const res = await queryViewSender(tableQuery);
       if (res) {
+        res.records.forEach((item) => {
+          item.responsibleDeptNames = item.responsibleDeptName;
+        });
         tableData.value = res.records;
         pagination.total = res.totalRow;
       }
@@ -274,7 +277,7 @@ import BreadcrumbBack from '@/components/BreadcrumbBack.vue';
     router.push({
       name: 'SafetySystemConstructionWorkPlanManagementDeptItem',
       query: {
-        id: currentId.value,
+        id: row.id,
         planId: row.planId,
         operate: 'work-plan-dept-view',
       },

+ 40 - 13
src/views/production-safety/productionSafetySystem/safetySystemConstructionWorkPlanManagementDept/components/safetySystemConstructionWorkPlanManagementDeptDetail.vue

@@ -69,8 +69,9 @@
           :disabled="true"
         />
       </el-form-item>
-      <el-form-item label="附件上传:" prop="fileUrl" :required="isViewMode">
+      <el-form-item label="附件上传:" prop="fileUrl" required >
         <UploadFiles
+          v-if="isEditMode"
           label="上传附件"
           :file-list="attachmentFileList"
           :disabled="isViewMode"
@@ -78,8 +79,22 @@
           @uploadSuccess="handleAttachmentUploadSuccess"
           @preview="handlePreview"
         />
+        <div class="file-list" v-else>
+          <div class="file-item" v-for="file in form.fileUrl" :key="file.fileId">
+            <span class="file-item--name">{{ file.fileName }}</span>
+            <div class="file-item--footer">
+              <el-button link type="primary" @click="previewOnline(file.fileUrl, file.fileType)"
+                >预览</el-button
+              >
+              <el-button link type="primary" @click.stop="downloadFile(file.fileUrl, file.fileName)"
+                >下载</el-button
+              >
+            </div>
+          </div>
+        </div>
       </el-form-item>
     </el-form>
+    <PreviewOnline ref="previewOnlineRef" />
   </main>
   <footer class="safety-platform-container__footer">
     <el-button @click="router.back()">返回</el-button>
@@ -97,7 +112,7 @@
   import { FORM_RULES } from '../configs/form';
   import {
     issueWorkPlanDept,
-    queryWorkPlanDetail,
+    queryWorkPlanDepartmentDetail,
     type SaveWorkPlanRequest,
   } from '@/api/safety-system-construction-work-plan';
   import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
@@ -106,6 +121,8 @@
   import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
   import { DeptTree } from '@/types/dept/type';
   import { getAllDepartments } from '@/api/auth/dept';
+  import { downloadFile } from '@/views/disaster/utils';
+
   const router = useRouter();
   const route = useRoute();
 
@@ -183,13 +200,14 @@
     }
     try {
       const list = await formatAttachmentList(files);
-      const jsonArr = (list || [])
-        .map((r) => ({
-          file_name: r.fileName,
-          url: r.fileUrl || '',
-        }))
-        .filter((x) => x.url);
-      form.fileUrl = JSON.stringify(jsonArr);
+      // debugger
+      // const jsonArr = (list || [])
+      //   .map((r) => ({
+      //     fileName: r.fileName,
+      //     fileUrl: r.fileUrl || '',
+      //   }))
+      //   .filter((x) => x.fileName);
+      form.fileUrl = JSON.stringify(list);
     } catch (e) {
       console.error('附件上传失败:', e);
       ElMessage.error('附件上传失败,请重试');
@@ -211,6 +229,13 @@
       previewOnlineRef.value?.open(url, fileType);
     }
   };
+
+  const previewOnline = (url: string | undefined, type) => {
+    if (url) {
+      previewOnlineRef.value?.open(url, type);
+    }
+  };
+
   //  时间格式化
   const formatDate = (date) => {
     if (!date) return '';
@@ -248,10 +273,10 @@
   };
 
   const getDetail = async () => {
-    const id = currentPlanId.value;
+    const id = currentId.value;
     if (!id) return;
     try {
-      const res = await queryWorkPlanDetail(id);
+      const res = await queryWorkPlanDepartmentDetail(id);
       if (res) {
         // 映射接口字段到表单字段
         const cooperateDeptIdsArray = parseDeptIds(res.cooperateDeptIds);
@@ -260,6 +285,7 @@
           ...res,
           cooperateDeptIds: cooperateDeptIdsArray,
           responsibleDeptIds: responsibleDeptIdsArray,
+          fileUrl: JSON.parse(res.fileUrl || '[]'),
         });
       }
       // cloneRuleFormData();
@@ -275,12 +301,13 @@
     try {
       if (isViewMode && form.fileUrl) {
         // 处理附件格式
-        form.fileUrl = JSON.parse(form.fileUrl)[0].url;
+        // debugger
+        // form.fileUrl = JSON.parse(form.fileUrl)[0].url;
       }
       if (isEditMode.value && currentId.value) {
         await issueWorkPlanDept({
           id: currentId.value,
-          fileUrl: form.fileUrl,
+          fileUrl: JSON.parse(form.fileUrl),
         });
         ElMessage.success('保存成功');
       }

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

@@ -6,6 +6,9 @@
     <main class="safety-platform-container__main">
         <div style="margin-bottom:20px">
             <el-button type="primary" @click="$router.push({ name: 'constructionSafetyManageAdd' })">添加 </el-button>
+            <!-- <el-button plain class="search-table-container--button" @click="handleImport">
+              导入
+            </el-button> -->
         </div>
       <div class="search-form">
         <el-form :inline="true">
@@ -43,9 +46,11 @@
         </el-form>
 
         <div>
-          
           <el-button type="primary" @click="queryTableList">查询</el-button>
           <el-button @click="handleRestParams">重置</el-button>
+          <el-button plain  @click="handleDownload">
+            导出
+          </el-button>
         </div>
       </div>
 
@@ -107,6 +112,16 @@
           @current-change="handleCurrentChange"
         />
       </div>
+      <BatchImport
+        v-if="batchImportVisible"
+        :visible="batchImportVisible"
+        :import-api-url="importApiUrl"
+        :template-url="templateUrl"
+        template-name="下载模板"
+        :show-template="false"
+        @close="batchImportVisible = false"
+        @update="handleUpdate"
+      />
     </main>
   </div>
   <BasicDialog
@@ -177,6 +192,7 @@
     constructionSafetyQueryPageConstruction,
     constructionSafetyDeleteConstructionById,
     constructionSafetyUpdateApply,
+    exportConstructionSafetyList,
   } from '@/api/production-safety/responsibility-implementation';
   import BasicDialog from '@/components/BasicDialog.vue';
   import { getApprovalNodeInstanceList } from '@/api/approval/approval';
@@ -188,7 +204,10 @@
   import { getAllDepartments } from '@/api/auth/dept';
   import { APPROVAL_TYPE_MAP, APPROVER_TYPE } from '@/views/emergency/emergency-plan/src/constant';
   import { useEmergencySuppliesHook } from '@/views/emergency/emergency-supplies/src/hook';
-
+  import BatchImport from '@/components/batch-import/BatchImport.vue';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
+  import { downloadByData } from '@/utils/file/download';
   const router = useRouter();
   const { id } = useUserInfoHook();
   const firstLevelDepts = ref<any[]>([]);
@@ -339,6 +358,36 @@
     });
   };
 
+  // 批量导入
+  const batchImportVisible = ref(false);
+  const { urlPrefix } = useGlobSetting();
+  // /productionSafety/academyFile/import
+  const importApiUrl = ref(urlJoin(urlPrefix, '/safetyRisk/list/importRiskList'));
+  const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-academy-file-template.xlsx');
+
+  const handleImport = () => {
+    batchImportVisible.value = true;
+  };
+
+  const handleUpdate = () => {
+    batchImportVisible.value = false;
+    // getTableData();
+  };
+
+  const handleDownload = async () => {
+    try {
+      const response = await exportConstructionSafetyList();
+      if (response) {
+        const fileName = `施工作业安全管理_${new Date().toISOString().split('T')[0]}.xlsx`;
+        downloadByData(response, fileName);
+        ElMessage.success('导出成功');
+      }
+    } catch (e) {
+      console.error('导出院级文件失败:', e);
+      ElMessage.error('导出失败,请重试');
+    }
+  };
+
   onMounted(async () => {
     await getDeptData();
     queryTableList();
@@ -362,7 +411,10 @@
   :deep(.breadcrumb .title) {
     margin-left: 0;
   }
-
+  :deep(main) {
+    display: flex;
+    flex-direction: column;
+  }
   .search-form {
     min-width: 800px;
     display: flex;

+ 74 - 0
src/views/production-safety/risk-identification-and-control/equipment-high-alert/configs/form.ts

@@ -0,0 +1,74 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-03-05 10:11:20
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-03-10 14:44:38
+ * @Describe: file describe
+ */
+import type { FormConfig } from '@/types/basic-form';
+
+/** 设备类别选项(设备类别名称) */
+export const DEVICE_CATEGORY_OPTIONS = [
+  { label: 'GC2', value: 1 },
+  { label: '机动工艺车辆', value: 2 },
+];
+
+/** 设备种类选项(设备种类名称) */
+export const DEVICE_TYPE_OPTIONS = [
+  { label: '压力容器', value: 1 },
+  { label: '固定式压力容器', value: 2 },
+  { label: '压力管道', value: 3 },
+];
+
+
+export const SPECIAL_EQUIPMENT_FORM_DATA = {
+  deviceId: '',
+  assetId: '',
+  deviceName: '',
+  useUnit: '',
+  categoryId: undefined as number | undefined,
+  typeId: undefined as number | undefined,
+  registerCode: '',
+  licenseNo: '',
+  deviceCode: '',
+  safeLocation: '',
+  useDepartment: '',
+  responsibilityDeptId: undefined as number | undefined,
+  responsiblePerson: '',
+  jobNo: '',
+  factoryNo: '',
+  productionDate: '',
+  startUseDate: '',
+  inspectionCycle: undefined as number | undefined,
+  useYears: undefined as number | undefined,
+  inspectionTime: '',
+  deviceStatus: 1,
+  nextInspectionDate: '',
+};
+
+export const SPECIAL_EQUIPMENT_FORM_RULES = {
+  deviceId: [{ required: true, message: '设备ID不能为空', trigger: 'blur' }],
+  assetId: [{ required: true, message: '设备固资ID不能为空', trigger: 'blur' }],
+  deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
+  useUnit: [{ required: true, message: '使用单位不能为空', trigger: 'blur' }],
+  categoryId: [{ required: true, message: '请选择设备类别', trigger: 'change' }],
+  typeId: [{ required: true, message: '请选择设备种类', trigger: 'change' }],
+  registerCode: [{ required: true, message: '注册代码不能为空', trigger: 'blur' }],
+  licenseNo: [{ required: true, message: '使用证号不能为空', trigger: 'blur' }],
+  // deviceCode: [{ required: true, message: '设备编码不能为空', trigger: 'blur' }],
+  safeLocation: [{ required: true, message: '安全地点不能为空', trigger: 'blur' }],
+  useDepartment: [{ required: true, message: '请选择使用部门', trigger: 'change' }],
+  responsibilityDeptId: [{ required: true, message: '请选择责任部门', trigger: 'change' }],
+  responsiblePerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+  jobNo: [{ required: true, message: '工号不能为空', trigger: 'blur' }],
+  // factoryNo: [{ required: true, message: '出厂编号不能为空', trigger: 'blur' }],
+  // productionDate: [{ required: true, message: '请选择生产日期', trigger: 'change' }],
+  startUseDate: [{ required: true, message: '请选择启用日期', trigger: 'change' }],
+  inspectionCycle: [{ required: true, message: '检测周期不能为空', trigger: 'blur' }],
+  useYears: [{ required: true, message: '使用年限不能为空', trigger: 'blur' }],
+  inspectionTime: [{ required: true, message: '请选择检测时间', trigger: 'change' }],
+  deviceStatus: [{ required: true, message: '请选择设备状态', trigger: 'change' }],
+  // nextInspectionDate: [{ required: true, message: '请选择下次检测时间', trigger: 'change' }],
+};
+
+

+ 93 - 0
src/views/production-safety/risk-identification-and-control/equipment-high-alert/configs/tables.ts

@@ -0,0 +1,93 @@
+/*
+ * @Author: liuJie
+ * @Date: 2026-02-26 13:57:24
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-03-10 14:43:50
+ * @Describe: file describe
+ */
+import type { TableColumnProps } from '@/types/basic-table';
+
+// 特种设备设施管理 - 表格基础配置
+export const TABLE_OPTIONS = {
+  emptyText: '暂无数据',
+  loading: true,
+  maxHeight: 'calc(70vh - 150px)',
+};
+
+export const SPECIAL_EQUIPMENT_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '序号',
+    type: 'index',
+    align: 'center',
+    width: '80px',
+  },
+  {
+    label: '设备编号',
+    prop: 'deviceNo',
+    align: 'left',
+    minWidth: '160px',
+  },
+  {
+    label: '设备类型',
+    prop: 'deviceType',
+    align: 'left',
+    minWidth: '160px',
+  },
+  {
+    label: '所属部门',
+    prop: 'deptName',
+    align: 'left',
+    minWidth: '140px',
+  },
+  {
+    label: '所属区域',
+    prop: 'regionName',
+    align: 'left',
+    minWidth: '140px',
+  },
+  {
+    label: '告警名称',
+    prop: 'alarmTitle',
+    align: 'left',
+    minWidth: '180px',
+  },
+  {
+    label: '告警内容',
+    prop: 'alarmContent',
+    align: 'left',
+    minWidth: '140px',
+  },
+  {
+    label: '告警类型',
+    prop: 'alarmType',
+    align: 'left',
+    minWidth: '140px',
+  },
+  {
+    label: '告警时刻',
+    prop: 'firstAlarmTime',
+    align: 'center',
+    minWidth: '140px',
+  },
+  {
+    label: '告警次数',
+    prop: 'alarmCount',
+    align: 'center',
+    minWidth: '140px',
+  },
+  {
+    label: '告警来源',
+    prop: 'location',
+    align: 'center',
+    minWidth: '140px',
+  },
+  {
+    label: '操作',
+    prop: 'action',
+    slot: 'action',
+    fixed: 'right',
+    width: '220px',
+    align: 'left',
+  },
+];
+

+ 374 - 0
src/views/production-safety/risk-identification-and-control/equipment-high-alert/list.vue

@@ -0,0 +1,374 @@
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <div class="breadcrumb-title">告警列表</div>
+    </header>
+    <main class="safety-platform-container__main">
+      <div class="search-table-container">
+        <header>
+          <div class="act-search">
+            <section class="select-box">
+                <div class="select-box--item">
+                <span>搜索设备编号:</span>
+                <el-input
+                  v-model="queryParams.queryParam.deviceNo"
+                  placeholder="请输入设备编号"
+                  class="act-search-input"
+                  clearable
+                />
+              </div>
+              <div class="select-box--item">
+                <span>搜索设备名称:</span>
+                <el-input
+                  v-model="queryParams.queryParam.deviceName"
+                  placeholder="请输入设备名称"
+                  class="act-search-input"
+                  clearable
+                />
+              </div>
+              <div class="select-box--item">
+                <span>设备类型:</span>
+                <el-select
+                  v-model="queryParams.queryParam.deviceType"
+                  placeholder="请选择设备类型"
+                  clearable
+                  class="act-search-input"
+                >
+                  <el-option
+                    v-for="opt in DEVICE_CATEGORY_OPTIONS"
+                    :key="opt.value"
+                    :label="opt.label"
+                    :value="opt.value"
+                  />
+                </el-select>
+              </div>
+              <div class="select-box--item">
+                <span>所属部门:</span>
+                <el-cascader
+                  v-model="useDeptPath"
+                  :options="deptOptions"
+                  :props="deptCascaderProps"
+                  :show-all-levels="false"
+                  placeholder="请选择责任部门"
+                  class="act-search-input"
+                  filterable
+                  clearable
+                  @change="handleUseDeptChange"
+                />
+              </div>
+              <div>
+                <span>计划日期范围:</span>
+                <el-date-picker
+                  v-model="dateRange"
+                  type="daterange"
+                  range-separator="至"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  value-format="YYYY-MM-DD"
+                  format="YYYY-MM-DD"
+                />
+              </div>
+            </section>
+            <section class="search-btn">
+              <el-button type="primary" @click="queryTableList">查询</el-button>
+              <el-button @click="handleRestParams">重置</el-button>
+              <el-button plain  @click="handleDownload">
+               导出
+              </el-button>
+            </section>
+          </div>
+        </header>
+
+        <div class="batch-table">
+          <BasicTable
+            ref="basicTableRef"
+            :tableData="tableData"
+            :tableConfig="tableConfig"
+            @update:pageSize="handleSizeChange"
+            @update:pageNumber="handleCurrentChange"
+          >
+            <template #action="scope">
+              <div class="action-container--div" style="justify-content: left">
+                <ActionButton
+                  text="查看"
+                  @click="handleView(scope.row)"
+                />
+              </div>
+            </template>
+          </BasicTable>
+        </div>
+      </div>
+    </main>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import {
+    querySpecialEquipmentPage,
+    deleteSpecialEquipment,
+    exportSpecialEquipment,
+    type SpecialEquipment,
+    type SpecialEquipmentQueryParam,
+  } from '@/api/production-safety/special-equipment';
+  import { getAllDepartments } from '@/api/auth/dept';
+  import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
+  import BasicTable from '@/components/BasicTable.vue';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import ActionButton from '@/components/ActionButton.vue';
+  import { TABLE_OPTIONS, SPECIAL_EQUIPMENT_TABLE_COLUMNS } from './configs/tables';
+  import { DEVICE_CATEGORY_OPTIONS, DEVICE_TYPE_OPTIONS } from './configs/form';
+  import { downloadByData } from '@/utils/file/download';
+import { http } from '@/utils/http/axios';
+  import dayjs from 'dayjs';
+  const loading = ref(false);
+  const dateRange = ref<[string, string] | null>(null);
+  interface  QueryParamType {
+    pageNumber: number;
+    pageSize: number;
+    queryParam: {
+      //设备编码
+      deviceNo?: string | undefined;
+      //设备名称
+      deviceName?: string | undefined;
+      // 设备类型
+      deviceType?: string | number | undefined;
+      // 所属部门
+      deptName?: string | undefined;
+      // 开始时间
+      startTime: string | undefined,
+      // 结束
+      endTime: string | undefined,
+    };
+  }
+  // 列表数据
+const equipmentHighAlertList = (query: QueryParamType)=>{
+  return http.request({
+    url: '/prodAlarm/queryProdAlarm',
+    method: 'post',
+    data: query,
+  });
+}
+// 导出接口
+const exportTableData = (params)=>{
+    return http.request({
+        url: '/prodAlarm/export',
+        method: 'post',
+        data: params,
+        responseType: 'blob',
+    }, {
+        isTransformResponse: false,
+    });
+}
+
+  // BasicTable
+  const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+  const { tableConfig, pagination } = useTableConfig(SPECIAL_EQUIPMENT_TABLE_COLUMNS, TABLE_OPTIONS);
+
+  const queryParams = reactive<QueryParamType>({
+    pageNumber: 1,
+    pageSize: 10,
+    queryParam: {
+      //设备编码
+      deviceNo: undefined,
+      //设备名称
+      deviceName: undefined,
+      // 设备类型
+      deviceType: undefined,
+      // 所属部门
+      deptName: undefined,
+      // 开始时间
+      startTime: undefined,
+      // 结束
+      endTime: undefined,
+    },
+  });
+
+  // 部门树(queryAllDeptTree 样式)
+  const deptOptions = ref<any[]>([]);
+  const useDeptPath = ref<number[]>([]);
+  const deptCascaderProps = {
+    expandTrigger: 'click',
+    checkStrictly: true,
+    value: 'id',
+    label: 'deptName',
+  };
+
+  const tableData = ref([]);
+
+  const handleSizeChange = (value: number) => {
+    pagination.pageSize = value;
+    queryParams.pageSize = value;
+    queryParams.pageNumber = 1;
+    queryTableList();
+  };
+
+  const handleCurrentChange = (value: number) => {
+    pagination.pageNumber = value;
+    queryParams.pageNumber = value;
+    queryTableList();
+  };
+
+  const getStatusText = (status?: number, statusName?: string) => {
+    if (statusName) return statusName;
+    if (status === 1) return '在用';
+    if (status === 2) return '停用';
+    if (status === 3) return '报废';
+    return '-';
+  };
+
+  const loadDeptTree = async () => {
+    try {
+      const res = await getAllDepartments();
+      deptOptions.value = formatDeptTree(res);
+    } catch (e) {
+      console.error('获取部门树失败:', e);
+      deptOptions.value = [];
+    }
+  };
+
+  const handleUseDeptChange = (val: number[]) => {
+    if (Array.isArray(val) && val.length) {
+      queryParams.queryParam.deptName = JSON.stringify(val[val.length - 1]);
+    } else {
+      queryParams.queryParam.deptName = undefined;
+    }
+  };
+
+  const handleView = (row)=>{
+    ElMessage.warning('请给跳转地址')
+  }
+
+
+  const queryTableList = () => {
+    loading.value = true;
+    tableConfig.loading = true;
+    let data = {
+        pageNumber: queryParams.pageNumber,
+        pageSize: queryParams.pageSize,
+        queryParam:{
+            ...queryParams.queryParam,
+            startTime: dateRange?.[0]
+                ? dayjs(dateRange?.[0]).format('YYYY-MM-DD')
+                : undefined,
+            endTime: dateRange?.[1]
+                ? dayjs(dateRange?.[1]).format('YYYY-MM-DD')
+                : undefined,
+        }
+    }
+    equipmentHighAlertList({
+        ...queryParams
+
+    })
+      .then((res: any) => {
+        tableData.value = res?.records;
+        pagination.total = res?.totalRow ?? res?.total ?? 0;
+      })
+      .finally(() => {
+        loading.value = false;
+        tableConfig.loading = false;
+      });
+  };
+// 导出相关
+  const handleDownload = async () => {
+    try {
+      const response = await exportTableData(queryParams.queryParam);
+      if (response) {
+        const fileName = `告警列表_${new Date().toISOString().split('T')[0]}.xlsx`;
+        downloadByData(response, fileName);
+        ElMessage.success('导出成功');
+      }
+    } catch (e) {
+      console.error('导出告警列表失败:', e);
+      ElMessage.error('导出失败,请重试');
+    }
+  };
+
+  const handleRestParams = () => {
+    pagination.pageNumber = 1;
+    pagination.pageSize = 10;
+    queryParams.pageNumber = 1;
+    queryParams.pageSize = 10;
+    queryParams.queryParam = {
+      //设备编码
+      deviceNo: undefined,
+      //设备名称
+      deviceName: undefined,
+      // 设备类型
+      deviceType: undefined,
+      // 所属部门
+      deptName: undefined,
+      // 开始时间
+      startTime: undefined,
+      // 结束
+      endTime: undefined,
+    };
+    useDeptPath.value = [];
+    dateRange.value = null
+    queryTableList();
+  };
+
+  onMounted(() => {
+    loadDeptTree();
+    queryTableList();
+  });
+</script>
+
+<style lang="scss" scoped>
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+  @use '@/views/traffic/violation/style/act-search-table.scss' as *;
+
+  :deep(.el-tabs__header) {
+    margin: 0;
+  }
+  :deep(.el-tabs__item) {
+    font-size: 14px !important;
+  }
+  :deep(.flexContent) {
+    display: flex;
+  }
+  :deep(.breadcrumb .title) {
+    margin-left: 0;
+  }
+
+  :deep(.el-form) {
+    flex: 1;
+    display: flex;
+    row-gap: 15px;
+    flex-wrap: wrap;
+  }
+  :deep(.el-form-item) {
+    margin-bottom: 0;
+  }
+  :deep(main) {
+    display: flex;
+    flex-direction: column;
+  }
+  .search-form {
+    min-width: 800px;
+    display: flex;
+
+    justify-content: space-between;
+    align-items: flex-end;
+    margin-bottom: 20px;
+  }
+
+  .button-content {
+    margin-bottom: 20px;
+  }
+  .table-content {
+    flex: 1;
+    overflow: hidden;
+    overflow-y: auto;
+  }
+  .page-content {
+    display: flex;
+    justify-content: flex-end;
+  }
+  .dateRange {
+    align-items: flex-end;
+  }
+</style>

+ 19 - 1
src/views/production-safety/risk-identification-and-control/hazard-approval-manage/list.vue

@@ -46,6 +46,9 @@
           
           <el-button type="primary" @click="queryTableList">查询</el-button>
           <el-button @click="handleRestParams">重置</el-button>
+          <el-button plain  @click="handleDownload">
+            导出
+          </el-button>
         </div>
       </div>
 
@@ -183,6 +186,7 @@
     dangerWorkSubmit,
     dangerWorkQueryPage,
     dangerWorkDeleteDangerWork,
+    exportHazardApprovalList,
   } from '@/api/production-safety/responsibility-implementation';
   import BasicDialog from '@/components/BasicDialog.vue';
   import { getApprovalNodeInstanceList } from '@/api/approval/approval';
@@ -190,7 +194,7 @@
   import { omit } from 'lodash-es';
   import { useUserInfoHook } from '@/hooks/useUserInfoHook';
   import type { ApprovalNodeInstanceType } from '@/views/system/approval/types';
-
+  import { downloadByData } from '@/utils/file/download';
   import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
   import { getAllDepartments } from '@/api/auth/dept';
   import { APPROVAL_TYPE_MAP, APPROVER_TYPE } from '@/views/emergency/emergency-plan/src/constant';
@@ -340,6 +344,20 @@
     });
   };
 
+    const handleDownload = async () => {
+    try {
+      const response = await exportHazardApprovalList();
+      if (response) {
+        const fileName = `危险作业审批管理_${new Date().toISOString().split('T')[0]}.xlsx`;
+        downloadByData(response, fileName);
+        ElMessage.success('导出成功');
+      }
+    } catch (e) {
+      console.error('导出院级文件失败:', e);
+      ElMessage.error('导出失败,请重试');
+    }
+  };
+
   onMounted(async () => {
     await getDeptData();
     queryTableList();

+ 51 - 1
src/views/production-safety/risk-identification-and-control/hazard-manage/list.vue

@@ -6,6 +6,9 @@
     <main class="safety-platform-container__main">
       <div style="margin-bottom: 20px">
         <el-button type="primary" @click="$router.push({ name: 'hazardManageAdd' })">添加 </el-button>
+        <el-button plain @click="handleImport">
+          导入
+        </el-button>
       </div>
       <div class="search-form">
         <el-form :inline="true">
@@ -63,6 +66,9 @@
         <div>
           <el-button type="primary" @click="queryTableList">查询</el-button>
           <el-button @click="handleRestParams">重置</el-button>
+          <el-button plain  @click="handleDownload">
+            导出
+          </el-button>
         </div>
       </div>
 
@@ -114,6 +120,16 @@
           @current-change="handleCurrentChange"
         />
       </div>
+      <BatchImport
+        v-if="batchImportVisible"
+        :visible="batchImportVisible"
+        :import-api-url="importApiUrl"
+        :template-url="templateUrl"
+        template-name="下载模板"
+        :show-template="false"
+        @close="batchImportVisible = false"
+        @update="handleUpdate"
+      />
     </main>
   </div>
 </template>
@@ -125,6 +141,7 @@
     safetyHazardInventoryQueryPage,
     safetyHazardInventoryDelete,
     safetyRiskListApprove,
+    exportHazardList,
   } from '@/api/production-safety/responsibility-implementation';
   import { omit } from 'lodash-es';
   import { useUserInfoHook } from '@/hooks/useUserInfoHook';
@@ -132,7 +149,10 @@
   import { downloadFile } from '@/views/disaster/utils';
   import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
   import { getAllDepartments } from '@/api/auth/dept';
-
+  import BatchImport from '@/components/batch-import/BatchImport.vue';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
+  import { downloadByData } from '@/utils/file/download';
   const router = useRouter();
   const { id } = useUserInfoHook();
   const firstLevelDepts = ref<any[]>([]);
@@ -233,6 +253,36 @@
     queryTableList();
   };
 
+  // 批量导入
+  const batchImportVisible = ref(false);
+  const { urlPrefix } = useGlobSetting();
+  // /productionSafety/academyFile/import
+  const importApiUrl = ref(urlJoin(urlPrefix, '/safetyHazardInventory/importHazard'));
+  const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-academy-file-template.xlsx');
+
+  const handleImport = () => {
+    batchImportVisible.value = true;
+  };
+
+  const handleUpdate = () => {
+    batchImportVisible.value = false;
+    // getTableData();
+  };
+
+  const handleDownload = async () => {
+    try {
+      const response = await exportHazardList();
+      if (response) {
+        const fileName = `危险清单管理_${new Date().toISOString().split('T')[0]}.xlsx`;
+        downloadByData(response, fileName);
+        ElMessage.success('导出成功');
+      }
+    } catch (e) {
+      console.error('导出危险清单失败:', e);
+      ElMessage.error('导出失败,请重试');
+    }
+  };
+
   onMounted(async () => {
     await getDeptData();
     queryTableList();

+ 388 - 0
src/views/production-safety/risk-identification-and-control/key-site-sensor-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>

+ 201 - 0
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/CameraGroupListAndTree/CameraGroupList/CameraGroupList.vue

@@ -0,0 +1,201 @@
+<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, saveOrUpdateCameraGroupAll, queryCameraGroupListByTypeAndSourceId } =
+    useCameraGroupList();
+
+  function handleCreateGroup() {
+    showCreateGroupInput.value = !showCreateGroupInput.value;
+    if (showCreateGroupInput.value) {
+      checkGroupName();
+
+      nextTick(() => {
+        groupNameInputRef.value.focus();
+      });
+    }
+  }
+
+  function handleEnterGroupName() {
+    checkGroupName();
+    saveOrUpdateCameraGroupAll(inputNewGroupName.value, 6);
+    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(() => {
+    queryCameraGroupListByTypeAndSourceId(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/key-site-sensor-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/key-site-sensor-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/key-site-sensor-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/key-site-sensor-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/key-site-sensor-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/key-site-sensor-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/key-site-sensor-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/key-site-sensor-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);
+  });
+};

+ 55 - 0
src/views/production-safety/risk-identification-and-control/key-site-sensor-manage/index.vue

@@ -0,0 +1,55 @@
+<!--
+ * @Author: liuJie
+ * @Date: 2026-02-26 13:57:24
+ * @LastEditors: liuJie
+ * @LastEditTime: 2026-03-10 15:05:42
+ * @Describe: 传感器
+-->
+<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/key-site-sensor-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/key-site-sensor-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");
+// };

+ 4 - 4
src/views/production-safety/risk-identification-and-control/risk-manage/list.vue

@@ -9,9 +9,6 @@
             <el-button plain class="search-table-container--button" @click="handleImport">
               导入
             </el-button>
-            <el-button plain class="search-table-container--button" @click="handleDownload">
-              导出
-            </el-button>
         </div>
       <div class="search-form">
         <el-form :inline="true">
@@ -71,6 +68,9 @@
         <div>
           <el-button type="primary" @click="queryTableList">查询</el-button>
           <el-button @click="handleRestParams">重置</el-button>
+          <el-button plain  @click="handleDownload">
+            导出
+          </el-button>
         </div>
       </div>
 
@@ -259,7 +259,7 @@
   const batchImportVisible = ref(false);
   const { urlPrefix } = useGlobSetting();
   // /productionSafety/academyFile/import
-  const importApiUrl = ref(urlJoin(urlPrefix, '/admin/prod/academyFile/importAcademyFile'));
+  const importApiUrl = ref(urlJoin(urlPrefix, '/safetyRisk/list/importRiskList'));
   const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-academy-file-template.xlsx');
 
   const handleImport = () => {

+ 1 - 1
src/views/production-safety/risk-identification-and-control/work-injury-apply-manage/Item.vue

@@ -2,7 +2,7 @@
   <div class="safety-platform-container">
     <header class="safety-platform-container__header">
       <BreadcrumbBack />
-      <span class="breadcrumb-title">{{ headerTitle }}</span>
+      <span class="breadcrumb-title">{{ headerTitle }} 1212121</span>
     </header>
     <Detail />
   </div>

+ 7 - 7
src/views/production-safety/safetyTrainingAndEducation/educationTrainingPlanManagement/educationTrainingPlanManagement.vue

@@ -185,11 +185,11 @@
     pageNumber: pagination.pageNumber,
     pageSize: pagination.pageSize,
     queryParam: {
-      keyword: '',
-      status: '',
-      categoryName: '',
-      startDate: '',
-      endDate: '',
+      keyword: undefined,
+      status: undefined,
+      categoryName: undefined,
+      startDate: undefined,
+      endDate: undefined,
     },
   });
 
@@ -208,8 +208,8 @@
   async function getTableData() {
     tableConfig.loading = true;
     try {
-      tableQuery.queryParam.startDate = dateRange.value ? dateRange.value[0] : '';
-      tableQuery.queryParam.endDate = dateRange.value ? dateRange.value[1] : '';
+      tableQuery.queryParam.startDate = dateRange.value ? dateRange.value[0] : undefined;
+      tableQuery.queryParam.endDate = dateRange.value ? dateRange.value[1] : undefined;
       const res = await getEducationAndTrainingProgramList(tableQuery);
       if (res) {
         // 映射返回数据字段到表格字段

+ 8 - 0
src/views/production-safety/safetyTrainingAndEducation/educationTrainingPlanManagementDept/educationTrainingPlanManagementDept.vue

@@ -276,6 +276,14 @@
 
   };
   const saveSummary = async()=>{
+    if(!fileList.value.length){
+      ElMessage.error('请先上传材料');
+      return;
+    }
+    if(!form.trainingSummary){
+      ElMessage.error('请输入小结内容');
+      return;
+    }
     try {
       await updateEducationTrainingPlanCourseSummary(form);
       ElMessage.success('更新小结成功');