Procházet zdrojové kódy

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

Feat/production 安全责任相关功能

See merge request product-group-fe/sfy-safety-group/sfy-safety!304
ai0197(吴云丰) před 4 měsíci
rodič
revize
5b1eb414c9
70 změnil soubory, kde provedl 5770 přidání a 675 odebrání
  1. 1 1
      src/api/nine-square-grid/index.ts
  2. 1 1
      src/api/production-safety-system/index.ts
  3. 186 154
      src/api/production-safety/index.ts
  4. 131 4
      src/api/production-safety/responsibility-implementation/index.ts
  5. 62 0
      src/api/receiptRecord/index.ts
  6. 2 2
      src/components/UploadFiles/UploadFiles.vue
  7. 93 1
      src/router/routers/production-safety-router/risk-identification-and-control.ts
  8. 14 0
      src/router/routers/production-safety-router/safetyAssessment.ts
  9. 2 2
      src/store/modules/useCameraGroupList.ts
  10. 136 84
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/components/employeeReportHiddenTroubleManagementDetail.vue
  11. 41 81
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/configs/form.ts
  12. 21 21
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/configs/tables.ts
  13. 89 78
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/employeeReportHiddenTroubleManagement.vue
  14. 3 1
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/employeeReportHiddenTroubleManagementItem.vue
  15. 4 6
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleCategoryManagement/components/hiddenTroubleCategoryManagementDetail.vue
  16. 12 18
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleCategoryManagement/configs/form.ts
  17. 11 16
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleCategoryManagement/configs/tables.ts
  18. 33 6
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleCategoryManagement/hiddenTroubleCategoryManagement.vue
  19. 0 1
      src/views/production-safety/implement-safety-duty/non-public-area-responsibilities/add.vue
  20. 1 2
      src/views/production-safety/implement-safety-duty/non-public-list-responsibilities/edit.vue
  21. 1 1
      src/views/production-safety/implement-safety-duty/public-area-responsibilities/edit.vue
  22. 1 1
      src/views/production-safety/productionSafetySystem/collegeFileManagement/collegeFileManagement.vue
  23. 78 24
      src/views/production-safety/productionSafetySystem/collegeFileManagement/components/collegeFileManagementDetail.vue
  24. 1 0
      src/views/production-safety/productionSafetySystem/collegeFileManagement/configs/form.ts
  25. 78 24
      src/views/production-safety/productionSafetySystem/doubleSystemManagement/components/doubleSystemManagementDetail.vue
  26. 1 0
      src/views/production-safety/productionSafetySystem/doubleSystemManagement/configs/form.ts
  27. 1 1
      src/views/production-safety/productionSafetySystem/doubleSystemManagement/doubleSystemManagement.vue
  28. 79 25
      src/views/production-safety/productionSafetySystem/lawManagement/components/lawManagementDetail.vue
  29. 2 0
      src/views/production-safety/productionSafetySystem/lawManagement/configs/form.ts
  30. 1 1
      src/views/production-safety/productionSafetySystem/lawManagement/lawManagement.vue
  31. 78 24
      src/views/production-safety/productionSafetySystem/safetyStandardizationSystemManagement/components/safetyStandardizationSystemManagementDetail.vue
  32. 1 0
      src/views/production-safety/productionSafetySystem/safetyStandardizationSystemManagement/configs/form.ts
  33. 1 1
      src/views/production-safety/productionSafetySystem/safetyStandardizationSystemManagement/safetyStandardizationSystemManagement.vue
  34. 78 24
      src/views/production-safety/productionSafetySystem/safetyTraining/components/safetyTrainingDetail.vue
  35. 1 0
      src/views/production-safety/productionSafetySystem/safetyTraining/configs/form.ts
  36. 1 1
      src/views/production-safety/productionSafetySystem/safetyTraining/safetyTraining.vue
  37. 1 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/add.vue
  38. 1 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/edit.vue
  39. 249 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/list.vue
  40. 1 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/monitor.vue
  41. 1 0
      src/views/production-safety/risk-identification-and-control/construction-safety-manage/view.vue
  42. 55 39
      src/views/production-safety/risk-identification-and-control/hazard-manage/add.vue
  43. 274 0
      src/views/production-safety/risk-identification-and-control/hazard-manage/components/PlansAndProgramsDialog.vue
  44. 328 1
      src/views/production-safety/risk-identification-and-control/hazard-manage/create-plan.vue
  45. 471 1
      src/views/production-safety/risk-identification-and-control/hazard-manage/edit.vue
  46. 9 10
      src/views/production-safety/risk-identification-and-control/hazard-manage/list.vue
  47. 568 1
      src/views/production-safety/risk-identification-and-control/hazard-manage/view.vue
  48. 388 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/CameraGroupListAndTree/CameraGroupList/CameraGroup.vue
  49. 200 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/CameraGroupListAndTree/CameraGroupList/CameraGroupList.vue
  50. 198 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/CameraGroupListAndTree/CameraGroupList/CameraListOfGroup.vue
  51. 232 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/CameraGroupListAndTree/CameraGroupList/CameraTreeOfGroupList.vue
  52. 59 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/CameraGroupListAndTree/CameraGroupListAndTree.vue
  53. 258 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/VideosGridBase/CamerasGrid.vue
  54. 334 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/VideosGridBase/ScreenToolbar.vue
  55. 14 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/VideosGridBase/VideosGridBase.vue
  56. 26 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/hooks/parseData.ts
  57. 43 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/hooks/useCameraStatus.ts
  58. 48 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/index.vue
  59. 65 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/type.ts
  60. 26 0
      src/views/production-safety/risk-identification-and-control/key-site-monitor-manage/utils.ts
  61. 1 0
      src/views/production-safety/safetyAssessment/evaluationSystem/components/EvaluationTarget.vue
  62. 6 0
      src/views/production-safety/safetyAssessment/evaluationSystem/configs/targetTables.ts
  63. 0 1
      src/views/production-safety/safetyAssessment/pointDeduction/configs/form.ts
  64. 1 1
      src/views/production-safety/safetyAssessment/pointDeduction/configs/tables.ts
  65. 218 11
      src/views/production-safety/safetyAssessment/receiptRecord/components/ReceiptRecordDetail.vue
  66. 25 3
      src/views/production-safety/safetyAssessment/receiptRecord/configs/form.ts
  67. 16 0
      src/views/production-safety/safetyAssessment/receiptRecord/configs/status.ts
  68. 59 0
      src/views/production-safety/safetyAssessment/receiptRecord/configs/tables.ts
  69. 2 1
      src/views/production-safety/safetyAssessment/receiptRecord/receiptRecord.vue
  70. 347 0
      src/views/production-safety/safetyAssessment/receiptRecord/receiptRecordAdministratorReview.vue

+ 1 - 1
src/api/nine-square-grid/index.ts

@@ -8,7 +8,7 @@ export function editTenant(data) {
   });
 }
 
-export function queryCameraGroupList(type: 0 | 1) {
+export function queryCameraGroupList(type: number) {
   //是否主页展示:0-否 1-是
   return http.request({
     url: `/cameraGroup/queryCameraGroupList?type=${type}`,

+ 1 - 1
src/api/production-safety-system/index.ts

@@ -38,7 +38,7 @@ export interface ProductionSafetyFileQuery {
 export interface ProductionSafetyFilePageQuery {
   pageNumber: number; // 页码,从1开始
   pageSize: number; // 每页数量
-  data?: ProductionSafetyFileQuery; // 查询条件对象
+  queryParam?: ProductionSafetyFileQuery; // 查询条件对象
 }
 
 /**

+ 186 - 154
src/api/production-safety/index.ts

@@ -2,131 +2,164 @@ import { http } from '@/utils/http/axios';
 import type { QueryPageRequest, QueryPageResponse } from '@/types/basic-query';
 
 /**
- * ==================== 员工上报隐患管理 hiddenDanger ====================
+ * ==================== 员工上报隐患管理 employeeHazardReport ====================
  */
 
-// 隐患上报数据对象(详情 / 列表返回)
-export interface HiddenDanger {
+// 员工上报隐患数据对象(详情 / 列表返回)
+export interface EmployeeHazardReportDTO {
   id: number;
-  dangerName: string;
-  dangerType: number;
-  dangerTypeName?: string;
-  dangerLevel: number;
-  dangerLevelName?: string;
-  location: string;
-  description: string;
-  reporterName: string;
-  reporterPhone: string;
-  reporterDept: string;
-  reportTime: string;
-  images?: string[];
-  status: number;
-  statusName?: string;
-  handlerName?: string;
-  handleTime?: string;
-  handleResult?: string;
-  createdAt?: string;
-  updatedAt?: string;
+  hazardNo: string; // 隐患编号
+  categoryId: number; // 隐患类别 ID
+  categoryName: string; // 隐患类别名称
+  hazardDesc: string; // 隐患问题描述
+  location: string; // 隐患地点
+  reportTime: string; // 上报时间
+  sourceType: number; // 提交类型:1-员工,2-供应商,3-第三方
+  sourceTypeName: string; // 提交类型名称
+  reporterName: string; // 上报人姓名
+  reporterJobNo: string; // 上报人工号
+  reporterMobile: string; // 联系电话
+  attachment: string; // 附件 JSON
+  status: number; // 状态:1-待审核,2-需求部门通过,3-需求部门驳回,4-安全部门通过,5-安全部门驳回,6-已入账,7-已关闭
+  statusName: string; // 状态名称
+  statusType?: number; // 状态类型:1-待审核,2-审核通过,3-审核不通过
+  isRewardApplied: number; // 是否申请奖品:1-是,0-否
+  createdById: number; // 创建人 ID
+  createdByName: string; // 创建人姓名
+  createdAt: string; // 创建时间
+  updatedAt: string; // 更新时间
 }
 
-// 新增隐患上报
-export interface SaveHiddenDangerReq {
-  dangerName: string;
-  dangerType: number;
-  dangerLevel: number;
-  location: string;
-  description: string;
-  reporterName: string;
-  reporterPhone: string;
-  reporterDept: string;
-  reportTime: string;
-  images?: string[];
-  status?: number;
+// 新增员工上报隐患
+export interface SaveEmployeeHazardReportReq {
+  categoryId: number; // 隐患类别 ID
+  hazardDesc: string; // 隐患问题描述
+  location: string; // 隐患地点
+  reportTime: string; // 上报时间
+  sourceType: number; // 提交类型:1-员工,2-供应商,3-第三方
+  reporterName: string; // 上报人姓名
+  reporterJobNo: string; // 上报人工号
+  reporterMobile: string; // 联系电话
+  attachment?: string; // 附件 JSON,格式 [{file_name, url}]
 }
 
-// 编辑隐患上报
-export interface UpdateHiddenDangerReq extends SaveHiddenDangerReq {
-  id: number;
+// 编辑员工上报隐患
+export interface UpdateEmployeeHazardReportReq {
+  id: number; // 主键 ID
+  categoryId?: number; // 隐患类别 ID
+  hazardDesc?: string; // 隐患问题描述
+  location?: string; // 隐患地点
+  reportTime?: string; // 上报时间
+  sourceType?: number; // 提交类型:1-员工,2-供应商,3-第三方
+  reporterName?: string; // 上报人姓名
+  reporterJobNo?: string; // 上报人工号
+  reporterMobile?: string; // 联系电话
+  attachment?: string; // 附件 JSON
 }
 
 // 查询条件
-export interface QueryHiddenDangerReq {
-  keyword?: string;
-  dangerType?: number;
-  dangerLevel?: number;
-  status?: number;
-  startDate?: string;
-  endDate?: string;
+export interface QueryEmployeeHazardReportReq {
+  keyword?: string; // 搜索隐患问题(支持全部非格式化字段关键词:隐患问题描述、隐患地点、姓名、工号、联系电话)
+  statusType?: number; // 隐患状态下拉:0或null=全部,1=待审核,2=审核通过,3=审核不通过
+  status?: number; // 状态精确值(1~7),与 statusType 二选一
+  sourceTypeName?: string; // 搜索任务来源(模糊查询)
+  startDate?: string; // 上报时间范围-开始日期(格式:YYYY-MM-DD)
+  endDate?: string; // 上报时间范围-结束日期(格式:YYYY-MM-DD)
+  categoryId?: number; // 隐患类别 ID
+  reporterJobNo?: string; // 上报人工号
+  hazardDesc?: string; // 仅按隐患问题描述查询(兼容用,建议使用 keyword)
 }
 
-// 处理隐患
-export interface HandleHiddenDangerReq {
-  id: number;
-  status: number;
-  handlerName: string;
-  handleTime: string;
-  handleResult?: string;
+// 审核员工上报隐患
+export interface ApproveEmployeeHazardReportReq {
+  hazardId: number; // 隐患主表 ID
+  node: number; // 审批节点:1-需求部门,2-安全部门
+  approvalStatus: number; // 审批状态:2-通过,3-驳回
+  approvalContent?: string; // 审批意见
 }
 
-// 新增隐患上报
-export function saveHiddenDanger(data: SaveHiddenDangerReq) {
+// 兼容旧接口名称(向后兼容)
+export type HiddenDanger = EmployeeHazardReportDTO;
+export type SaveHiddenDangerReq = SaveEmployeeHazardReportReq;
+export type UpdateHiddenDangerReq = UpdateEmployeeHazardReportReq;
+export type QueryHiddenDangerReq = QueryEmployeeHazardReportReq;
+
+// 新增员工上报隐患
+export function saveEmployeeHazardReport(data: SaveEmployeeHazardReportReq) {
   return http.request<number>({
-    url: '/admin/prod/hiddenDanger/saveHiddenDanger',
+    url: '/employeeHazardReport/saveEmployeeHazardReport',
     method: 'post',
     data,
   });
 }
 
-// 编辑隐患上报
-export function updateHiddenDanger(data: UpdateHiddenDangerReq) {
+// 编辑员工上报隐患
+export function updateEmployeeHazardReport(data: UpdateEmployeeHazardReportReq) {
   return http.request({
-    url: '/admin/prod/hiddenDanger/updateHiddenDanger',
+    url: '/employeeHazardReport/updateEmployeeHazardReport',
     method: 'put',
     data,
   });
 }
 
-// 删除隐患上报
-export function deleteHiddenDanger(id: number) {
+// 删除员工上报隐患
+export function deleteEmployeeHazardReport(id: number) {
   return http.request({
-    url: `/admin/prod/hiddenDanger/deleteHiddenDanger?id=${id}`,
+    url: `/employeeHazardReport/deleteEmployeeHazardReport?id=${id}`,
     method: 'delete',
   });
 }
 
-// 根据 ID 查询隐患详情
-export function queryHiddenDangerById(id: number) {
-  return http.request<HiddenDanger>({
-    url: `/admin/prod/hiddenDanger/queryHiddenDangerById?id=${id}`,
+// 根据 ID 查询员工上报隐患详情
+export function queryEmployeeHazardReportById(id: number) {
+  return http.request<EmployeeHazardReportDTO>({
+    url: `/employeeHazardReport/queryEmployeeHazardReportById?id=${id}`,
     method: 'get',
   });
 }
 
-// 查询隐患上报列表(不分页)
-export function queryHiddenDangerList(params?: QueryHiddenDangerReq) {
-  return http.request<HiddenDanger[]>({
-    url: '/admin/prod/hiddenDanger/queryHiddenDangerList',
+// 查看员工上报隐患列表(不分页)
+export function queryEmployeeHazardReportList(params?: QueryEmployeeHazardReportReq) {
+  return http.request<EmployeeHazardReportDTO[]>({
+    url: '/employeeHazardReport/queryEmployeeHazardReportList',
     method: 'post',
-    data: params,
+    data: params || {},
   });
 }
 
-// 分页查询隐患上报
-export function queryHiddenDangerPage(query: QueryPageRequest<QueryHiddenDangerReq>) {
-  return http.request<QueryPageResponse<HiddenDanger>>({
-    url: '/admin/prod/hiddenDanger/queryHiddenDangerPage',
+// 分页查询员工上报隐患
+export function queryEmployeeHazardReportPage(query: QueryPageRequest<QueryEmployeeHazardReportReq>) {
+  return http.request<QueryPageResponse<EmployeeHazardReportDTO>>({
+    url: '/employeeHazardReport/queryEmployeeHazardReportPage',
     method: 'post',
     data: query,
   });
 }
 
-// 导出隐患上报
-export function exportHiddenDanger(params?: QueryHiddenDangerReq) {
+// 审核员工上报隐患
+export function approveEmployeeHazardReport(data: ApproveEmployeeHazardReportReq) {
+  return http.request({
+    url: '/employeeHazardReport/approveEmployeeHazardReport',
+    method: 'post',
+    data,
+  });
+}
+
+// 入账员工上报隐患
+export function accountEmployeeHazardReport(hazardId: number) {
+  return http.request({
+    url: `/employeeHazardReport/accountEmployeeHazardReport?hazardId=${hazardId}`,
+    method: 'post',
+  });
+}
+
+// 导出员工上报隐患
+export function exportEmployeeHazardReport(params?: QueryEmployeeHazardReportReq) {
   return http.request(
     {
-      url: '/admin/prod/hiddenDanger/exportHiddenDanger',
+      url: '/employeeHazardReport/exportEmployeeHazardReport',
       method: 'post',
-      data: params,
+      data: params || {},
       responseType: 'blob',
     },
     {
@@ -135,137 +168,126 @@ export function exportHiddenDanger(params?: QueryHiddenDangerReq) {
   );
 }
 
-// 导入隐患上报
+// 导入结果
 export interface ImportRes {
   successCount: number;
   failCount: number;
-  failMessages: string[];
+  failInfoList?: Array<{
+    rowNum: number;
+    failReason: string;
+  }>;
+  failMessages?: string[]; // 兼容旧版本
 }
 
-export function importHiddenDanger(file: File) {
-  const formData = new FormData();
-  formData.append('file', file);
-  return http.request<ImportRes>({
-    url: '/admin/prod/hiddenDanger/importHiddenDanger',
-    method: 'post',
-    data: formData,
-    headers: {
-      'Content-Type': 'multipart/form-data',
-    },
-  });
-}
-
-// 处理隐患
-export function handleHiddenDanger(data: HandleHiddenDangerReq) {
-  return http.request({
-    url: '/admin/prod/hiddenDanger/handleHiddenDanger',
-    method: 'put',
-    data,
-  });
-}
+// 向后兼容的接口函数(使用旧接口名称)
+export const saveHiddenDanger = saveEmployeeHazardReport;
+export const updateHiddenDanger = updateEmployeeHazardReport;
+export const deleteHiddenDanger = deleteEmployeeHazardReport;
+export const queryHiddenDangerById = queryEmployeeHazardReportById;
+export const queryHiddenDangerList = queryEmployeeHazardReportList;
+export const queryHiddenDangerPage = queryEmployeeHazardReportPage;
+export const exportHiddenDanger = exportEmployeeHazardReport;
 
 /**
- * ==================== 隐患类别管理 dangerType ====================
+ * ==================== 隐患类别管理 hazardCategory ====================
  */
 
-export interface DangerType {
+export interface HazardCategoryDTO {
   id: number;
-  typeName: string;
-  parentId: number | null;
-  parentName?: string | null;
-  sort?: number;
-  status: number;
-  statusName?: string;
-  createdAt?: string;
-  updatedAt?: string;
-  children?: DangerType[];
+  categoryName: string; // 类别名称
+  description: string; // 类别描述
+  status: number; // 状态:1-启用,0-禁用
+  parentId: number; // 父级 ID,一级为 0
+  sortOrder: number; // 排序
+  createdAt: string; // 创建时间
+  updatedAt: string; // 更新时间
 }
 
-export interface SaveDangerTypeReq {
-  typeName: string;
-  parentId?: number | null;
-  sort?: number;
-  status?: number;
+export interface SaveHazardCategoryReq {
+  categoryName: string; // 类别名称
+  description?: string; // 类别描述,最多 300 字
+  status: number; // 启用状态:1-启用,0-禁用
 }
 
-export interface UpdateDangerTypeReq extends SaveDangerTypeReq {
-  id: number;
+export interface UpdateHazardCategoryReq {
+  id: number; // 主键 ID
+  categoryName?: string; // 类别名称
+  description?: string; // 类别描述,最多 300 字
+  status?: number; // 启用状态:1-启用,0-禁用
 }
 
-export interface QueryDangerTypeReq {
-  keyword?: string;
-  parentId?: number | null;
-  status?: number;
+export interface QueryHazardCategoryReq {
+  keyword?: string; // 关键词,模糊查询类别名称、描述等
+  status?: number; // 状态:1-启用,0-禁用
+  startDate?: string; // 创建时间范围-开始
+  endDate?: string; // 创建时间范围-结束
 }
 
+// 兼容旧接口名称(向后兼容)
+export type DangerType = HazardCategoryDTO;
+export type SaveDangerTypeReq = SaveHazardCategoryReq;
+export type UpdateDangerTypeReq = UpdateHazardCategoryReq;
+export type QueryDangerTypeReq = QueryHazardCategoryReq;
+
 // 新增隐患类别
-export function saveDangerType(data: SaveDangerTypeReq) {
+export function saveHazardCategory(data: SaveHazardCategoryReq) {
   return http.request<number>({
-    url: '/admin/prod/dangerType/saveDangerType',
+    url: '/hazardCategory/saveHazardCategory',
     method: 'post',
     data,
   });
 }
 
 // 编辑隐患类别
-export function updateDangerType(data: UpdateDangerTypeReq) {
+export function updateHazardCategory(data: UpdateHazardCategoryReq) {
   return http.request({
-    url: '/admin/prod/dangerType/updateDangerType',
+    url: '/hazardCategory/updateHazardCategory',
     method: 'put',
     data,
   });
 }
 
 // 删除隐患类别
-export function deleteDangerType(id: number) {
+export function deleteHazardCategory(id: number) {
   return http.request({
-    url: `/admin/prod/dangerType/deleteDangerType?id=${id}`,
+    url: `/hazardCategory/deleteHazardCategory?id=${id}`,
     method: 'delete',
   });
 }
 
 // 根据 ID 查询隐患类别详情
-export function queryDangerTypeById(id: number) {
-  return http.request<DangerType>({
-    url: `/admin/prod/dangerType/queryDangerTypeById?id=${id}`,
+export function queryHazardCategoryById(id: number) {
+  return http.request<HazardCategoryDTO>({
+    url: `/hazardCategory/queryHazardCategoryById?id=${id}`,
     method: 'get',
   });
 }
 
-// 查隐患类别列表(不分页)
-export function queryDangerTypeList(params?: QueryDangerTypeReq) {
-  return http.request<DangerType[]>({
-    url: '/admin/prod/dangerType/queryDangerTypeList',
+// 查隐患类别列表(不分页)
+export function queryHazardCategoryList(params?: QueryHazardCategoryReq) {
+  return http.request<HazardCategoryDTO[]>({
+    url: '/hazardCategory/queryHazardCategoryList',
     method: 'post',
-    data: params,
+    data: params || {},
   });
 }
 
 // 分页查询隐患类别
-export function queryDangerTypePage(query: QueryPageRequest<QueryDangerTypeReq>) {
-  return http.request<QueryPageResponse<DangerType>>({
-    url: '/admin/prod/dangerType/queryDangerTypePage',
+export function queryHazardCategoryPage(query: QueryPageRequest<QueryHazardCategoryReq>) {
+  return http.request<QueryPageResponse<HazardCategoryDTO>>({
+    url: '/hazardCategory/queryHazardCategoryPage',
     method: 'post',
     data: query,
   });
 }
 
-// 获取隐患类别树形结构
-export function getDangerTypeTree(status?: number) {
-  return http.request<DangerType[]>({
-    url: '/admin/prod/dangerType/getDangerTypeTree',
-    method: 'get',
-    params: typeof status === 'number' ? { status } : undefined,
-  });
-}
-
 // 导出隐患类别
-export function exportDangerType(params?: QueryDangerTypeReq) {
+export function exportHazardCategory(params?: QueryHazardCategoryReq) {
   return http.request(
     {
-      url: '/admin/prod/dangerType/exportDangerType',
+      url: '/hazardCategory/exportHazardCategory',
       method: 'post',
-      data: params,
+      data: params || {},
       responseType: 'blob',
     },
     {
@@ -275,11 +297,11 @@ export function exportDangerType(params?: QueryDangerTypeReq) {
 }
 
 // 导入隐患类别
-export function importDangerType(file: File) {
+export function importHazardCategory(file: File) {
   const formData = new FormData();
   formData.append('file', file);
   return http.request<ImportRes>({
-    url: '/admin/prod/dangerType/importDangerType',
+    url: '/hazardCategory/importHazardCategory',
     method: 'post',
     data: formData,
     headers: {
@@ -287,3 +309,13 @@ export function importDangerType(file: File) {
     },
   });
 }
+
+// 向后兼容的接口函数(使用旧接口名称)
+export const saveDangerType = saveHazardCategory;
+export const updateDangerType = updateHazardCategory;
+export const deleteDangerType = deleteHazardCategory;
+export const queryDangerTypeById = queryHazardCategoryById;
+export const queryDangerTypeList = queryHazardCategoryList;
+export const queryDangerTypePage = queryHazardCategoryPage;
+export const exportDangerType = exportHazardCategory;
+export const importDangerType = importHazardCategory;

+ 131 - 4
src/api/production-safety/responsibility-implementation/index.ts

@@ -6,7 +6,6 @@ import type { QueryPageRequest, QueryPageResponse } from '@/types/basic-query';
  * @param params - 分页查询参数,通常包含 username、pageNum、pageSize 等
  * @returns Promise<QueryPageResponse>
  */
-queryAvailableUserList
 export function queryAvailableUserList(params) {
   return http.request({
     url: `/admin/user/queryAvailableUserList`,
@@ -375,7 +374,7 @@ export function safetyNoticeAdminDelete(id) {
  * @returns Promise<any> 安全通知的完整详细信息,包括发布内容、接收部门、反馈状态等
  */
 export function safetyNoticeAdminQueryDetail(params) {
-   return http.request({
+  return http.request({
     url: `/safetyNotice/admin/queryDetail`,
     method: 'get',
     params
@@ -570,7 +569,7 @@ export function safetyRiskListChange(params) {
  * 查询安全风险清单详情           
  *  @param id - 安全风险清单 ID
  * @returns Promise<any> 安全风险清单详细信息
- */       
+ */
 export function safetyRiskListQueryDetail(id) {
   return http.request({
     url: `/safetyRisk/list/queryDetail?id=${id}`,
@@ -602,4 +601,132 @@ export function safetyHazardInventoryDelete(id) {
     url: `/safetyHazardInventory/delete?id=${id}`,
     method: 'delete',
   });
-}
+}
+
+/**
+  * 保存安全危险源清单     
+  * @param params - 安全危险源清单数据
+  * @returns Promise<void>
+  */
+export function safetyHazardInventorySaveHazard(params) {
+  return http.request({
+    url: `/safetyHazardInventory/saveHazard`,
+    method: 'post',
+    params
+  });
+}
+
+// safetyHazardInventory/queryDetail
+/**
+ * 查询安全危险源清单详情 
+ * @param id - 安全危险源清单 ID
+ *  @returns Promise<any> 安全危险源清单详细信息
+ */
+export function safetyHazardInventoryQueryDetail(id) {
+  return http.request({
+    url: `/safetyHazardInventory/queryDetail?id=${id}`,
+    method: 'get',
+  });
+}
+
+/**
+ * 更新安全危险源清单 
+ * @param params - 更新后的安全危险源清单数据(需包含 ID)
+ * @returns Promise<void>
+ */
+export function safetyHazardInventoryUpdateHazard(params) {
+  return http.request({
+    url: `/safetyHazardInventory/updateHazard`,
+    method: 'put',
+    params
+  });
+}
+
+/**
+ * 分页查询安全整改计划与方案列表 
+ * @param params - 分页及查询条件
+ * @returns Promise<QueryPageResponse> 安全整改计划与方案分页结果
+ */
+
+export function safetyHazardInventoryQueryPlanAndSchemePage(params) {
+  return http.request({
+    url: '/safetyHazardInventory/queryPlanAndSchemePage',
+    method: 'post',
+    params
+  });
+}
+
+/**
+ * 删除安全整改计划与方案 
+ * @param id - 安全整改计划与方案 ID
+ * @returns Promise<void>
+ */
+export function safetyHazardInventoryDeletePlan(id) {
+  return http.request({
+    url: `/safetyHazardInventory/deletePlan?id=${id}`,
+    method: 'delete',
+  });
+}
+
+/**
+ * 查询安全整改计划与方案详情 
+ * @param id - 安全整改计划与方案 ID    
+ * @returns Promise<any> 安全整改计划与方案详细信息
+ */
+export function safetyHazardInventoryQueryPlanDetail(id) {
+  return http.request({
+    url: `/safetyHazardInventory/queryPlanDetail?id=${id}`,
+    method: 'get',
+  });
+}
+
+/**
+ * 保存安全整改计划与方案 
+ * @param params - 安全整改计划与方案数据
+ * @returns Promise<void>
+ */
+export function safetyHazardInventorySavePlan(params) {
+  return http.request({
+    url: `/safetyHazardInventory/savePlan`,
+    method: 'post',
+    params
+  });
+}
+
+/**
+ * 更新安全整改计划与方案 
+ * @param params - 更新后的安全整改计划与方案数据(需包含 ID)
+ * @returns Promise<void>
+ */
+export function safetyHazardInventoryUpdatePlan(params) {
+  return http.request({
+    url: `/safetyHazardInventory/updatePlan`,
+    method: 'put',
+    params
+  });
+}
+
+
+/**
+ * 分页查询施工安全数据(施工侧)
+ * @param params - 查询参数
+ * @returns Promise<void>
+ */
+export function constructionSafetyQueryPageConstruction(params) {
+  return http.request({
+    url: `/constructionSafety/queryPageConstruction`,
+    method: 'post',
+    params
+  });
+}
+/**
+ * 根据 ID 删除施工安全记录
+ * @param id - 施工安全记录 ID
+ * @returns Promise<void>
+ */
+export function constructionSafetyDeleteConstructionById(id) {
+  return http.request({
+    url: `/constructionSafety/deleteConstructionById/${id}`,
+    method: 'delete',
+  });
+}

+ 62 - 0
src/api/receiptRecord/index.ts

@@ -21,6 +21,9 @@ export interface ReceiptRecordItem {
   createdAt: string; // 创建时间
   updatedAt: string; // 更新时间
   isDeleted: number; // 0-未删除,大于0(时间戳)-已删除
+  templateId?: number; // 审批流程模板ID(可选,后端字段为 templateId)
+  approvalOrder?: number; // 审批顺序(管理员审核页使用)
+  rejectReson?: string; // 审核不通过原因(后端字段为 rejectReson)
 }
 
 /**
@@ -42,6 +45,16 @@ export function queryClaimItemsLogs(query: QueryPageRequest<ReceiptRecordQueryPa
     data: query,
   });
 }
+/**
+ * 查询物品领取记录审批列表
+ */
+export function queryClaimItemsApprovalPage(query: QueryPageRequest<ReceiptRecordQueryParam>) {
+  return http.request<QueryPageResponse<ReceiptRecordItem>>({
+    url: '/claimItemsLog/queryClaimItemsApprovalPage',
+    method: 'post',
+    data: query,
+  });
+}
 
 /**
  * 保存物品领取记录请求参数
@@ -57,6 +70,7 @@ export interface SaveClaimItemsLogRequest {
   userName: string; // 领取人
   userId?: number; // 领取人ID
   remark: string; // 备注
+  templateId?: number; // 审批流程模板ID(后端字段为 templateId)
 }
 
 /**
@@ -77,6 +91,54 @@ export interface UpdateClaimItemsLogRequest extends SaveClaimItemsLogRequest {
   id: number; // 记录ID
 }
 
+/**
+ * 物品领取记录审批流程提交参数
+ * 参考应急预案审批流程参数结构
+ */
+export interface ReceiptRecordApprovalProcessParam {
+  claimItemsLogId: number; // 物品领取记录ID
+  approvalDescription: string; // 审批说明
+  approvalInfoList: {
+    approvalOrder: number; // 审批顺序
+    approverIdList: number[]; // 当前节点审批人ID列表
+  }[];
+}
+
+/**
+ * 提交物品领取记录审批流程
+ * (需要后端提供对应接口:/claimItemsLog/saveApprovalProcess)
+ */
+export function submitReceiptRecordApprovalProcess(data: ReceiptRecordApprovalProcessParam) {
+  return http.request({
+    url: '/claimItemsLog/saveApprovalProcess',
+    method: 'post',
+    data,
+  });
+}
+
+/**
+ * 物品领取记录单条审核入参
+ * 复用预案审批的审批入参结构
+ */
+export interface ReceiptRecordApprovalParam {
+  planId: number; // 预案id,这里复用为物品领取记录ID
+  approvalOrder: number; // 审批顺序
+  approvalStatus: number; // 审批状态: 1-待审批,2-已审批,3-退回
+  returnReason?: string; // 退回原因(请求字段为 returnReason)
+}
+
+/**
+ * 物品领取记录单条审核
+ * 接口:/claimItemsLog/saveApproval
+ */
+export function submitReceiptRecordApproval(data: ReceiptRecordApprovalParam) {
+  return http.request({
+    url: '/claimItemsLog/saveApproval',
+    method: 'post',
+    data,
+  });
+}
+
 /**
  * 更新物品领取记录(编辑)
  */

+ 2 - 2
src/components/UploadFiles/UploadFiles.vue

@@ -41,7 +41,7 @@
           <span class="file-name">{{ file.fileName }}</span>
         </div>
         <el-icon 
-          v-if="!isUploadDisabled"
+          v-if="!props.disabled"
           class="delete-button" 
           @click="removeFile(file.fileId)"
         >
@@ -86,7 +86,7 @@
   });
 
   const isUploadDisabled = computed(() => {
-    return props.disabled || fileList.value.length >= MAX_COUNT.value;
+    return  fileList.value.length >= MAX_COUNT.value;
   });
 
   // 检查文件是否已存在

+ 93 - 1
src/router/routers/production-safety-router/risk-identification-and-control.ts

@@ -166,7 +166,99 @@
             hidden: false,
             noCache: false,
           }
-        }],
+        },
+        // 重点部位监控管理
+        {
+          id: 93007,
+          parentId: 90014,
+          name: 'keySiteMonitorManage',
+          path: 'key-site-monitor-manage',
+          component: '/production-safety/risk-identification-and-control/key-site-monitor-manage',
+          meta: {
+            title: '重点部位监控管理',
+            icon: 'OverviewIcon',
+            isRoot: false,
+            hidden: false,
+            noCache: false,
+          }
+        },
+      
+        // 施工作业安全管理
+        {
+          id: 93008,
+          parentId: 90014,
+          name: 'constructionSafetyManage',
+          path: 'construction-safety-manage',
+          component: '/production-safety/risk-identification-and-control/construction-safety-manage/list',
+          meta: {
+            title: '施工作业安全管理',
+            icon: 'OverviewIcon',
+            isRoot: false,
+            hidden: false,
+            noCache: false,
+          }
+        },
+        {
+          id: 93009,
+          parentId: 90014,
+          name: 'constructionSafetyManageView',
+          path: 'construction-safety-manage-view',
+          component: '/production-safety/risk-identification-and-control/construction-safety-manage/view',
+          meta: {
+            title: '查看施工作业安全',
+            activeMenu: '/work-safety/risk-identification-and-control/construction-safety-manage',
+            icon: 'OverviewIcon',
+            isRoot: false,
+            hidden: false,
+            noCache: false,
+          }
+        },
+        {
+          id: 93010,
+          parentId: 90014,
+          name: 'constructionSafetyManageAdd',
+          path: 'construction-safety-manage-add',
+          component: '/production-safety/risk-identification-and-control/construction-safety-manage/add',
+          meta: {
+            title: '新增施工作业安全',
+            activeMenu: '/work-safety/risk-identification-and-control/construction-safety-manage',
+            icon: 'OverviewIcon',
+            isRoot: false,
+            hidden: false,
+            noCache: false,
+          }
+        },
+        {
+          id: 93011,
+          parentId: 90014,
+          name: 'constructionSafetyManageEdit',
+          path: 'construction-safety-manage-edit',
+          component: '/production-safety/risk-identification-and-control/construction-safety-manage/edit',
+          meta: {
+            title: '编辑施工作业安全',
+            activeMenu: '/work-safety/risk-identification-and-control/construction-safety-manage',
+            icon: 'OverviewIcon',
+            isRoot: false,
+            hidden: false,
+            noCache: false,
+          }
+        },
+        {
+          id: 93012,
+          parentId: 90014,
+          name: 'constructionSafetyManageMonitor',
+          path: 'construction-safety-manage-monitor',
+          component: '/production-safety/risk-identification-and-control/construction-safety-manage/monitor',
+          meta: {
+            title: '视频监控',
+            activeMenu: '/work-safety/risk-identification-and-control/construction-safety-manage',
+            icon: 'OverviewIcon',
+            isRoot: false,
+            hidden: false,
+            noCache: false,
+          }
+        },
+      ],
     }];
      
      export default riskIdentificationAndControlRoutes; 

+ 14 - 0
src/router/routers/production-safety-router/safetyAssessment.ts

@@ -56,6 +56,20 @@
           noCache: false,
         },
       },
+      {
+        id: 9002102,
+        parentId: 90021,
+        name: 'ReceiptRecordAdministratorReview',
+        path: 'receipt-record-administrator-review',
+        component: '/production-safety/safetyAssessment/receiptRecord/receiptRecordAdministratorReview',
+        meta: {
+          title: '物品领取记录管理(管理员审核)',
+          icon: 'OverviewIcon',
+          isRoot: false,
+          hidden: true,
+          noCache: false,
+        },
+      },
       {
         id: 9002101,
         parentId: 90021,

+ 2 - 2
src/store/modules/useCameraGroupList.ts

@@ -80,8 +80,8 @@ export const useCameraGroupList = defineStore('useCameraGroupList', () => {
     cameraInPlay.value = tempList;
   }
 
-  function getCameraGroupList() {
-    queryCameraGroupList(0).then((res) => {
+  function getCameraGroupList(type: number = 0) {
+    queryCameraGroupList(type).then((res) => {
       console.log('res', res);
 
       cameraGroupList.value = parseCameraGroupChildren(res);

+ 136 - 84
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/components/employeeReportHiddenTroubleManagementDetail.vue

@@ -3,24 +3,66 @@
     <BasicForm
       ref="basicFormRef"
       :formData="ruleFormData"
-      :formRules="isViewMode ? undefined : formRules"
-      :formConfig="computedFormConfig"
+      :formRules="formRules"
+      :formConfig="formConfig"
     >
-      <template #status>
-        <el-radio-group v-model="ruleFormData.status" :disabled="isViewMode">
-          <el-radio :value="1">待处理</el-radio>
-          <el-radio :value="2">处理中</el-radio>
-          <el-radio :value="3">已处理</el-radio>
-          <el-radio :value="4">已驳回</el-radio>
-        </el-radio-group>
+      <template #attachment>
+        <div v-if="attachmentList.length > 0">
+          <div
+            v-for="(item, index) in attachmentList"
+            :key="index"
+            class="file-item"
+            style="display: flex; align-items: center; margin-bottom: 8px;"
+          >
+            <span>{{ item.file_name || item.fileName || `附件${index + 1}` }}</span>
+            <el-button
+              type="primary"
+              link
+              size="small"
+              style="margin-left: 8px;"
+              @click="handlePreview(item.url || item.fileUrl)"
+            >
+              预览
+            </el-button>
+          </div>
+        </div>
+        <span v-else>无附件</span>
       </template>
     </BasicForm>
+    <!-- 审核弹窗 -->
+    <el-dialog v-model="approveDialogVisible" title="审核" width="500px" @close="handleApproveDialogClose">
+      <el-form :model="approveForm" label-width="100px">
+        <el-form-item label="审批节点:">
+          <el-select v-model="approveForm.node" placeholder="请选择审批节点">
+            <el-option label="需求部门" :value="1" />
+            <el-option label="安全部门" :value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="审批状态:">
+          <el-radio-group v-model="approveForm.approvalStatus">
+            <el-radio :value="2">通过</el-radio>
+            <el-radio :value="3">驳回</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="审批意见:">
+          <el-input
+            v-model="approveForm.approvalContent"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入审批意见"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="approveDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleApproveSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+    <PreviewOnline ref="previewOnlineRef" />
   </main>
   <footer class="safety-platform-container__footer">
     <el-button @click="router.back()">返回</el-button>
-    <el-button v-if="!isViewMode" type="primary" @click="handleSubmit">
-      {{ isCreateMode ? '提交' : '保存' }}
-    </el-button>
+    <el-button v-if="isApproveMode" type="primary" @click="handleApproveClick">审核</el-button>
   </footer>
 </template>
 
@@ -33,49 +75,40 @@
   import { ACADEMY_FILE_FORM_CONFIG, ACADEMY_FILE_FORM_DATA, ACADEMY_FILE_FORM_RULES } from '../configs/form';
   import {
     queryHiddenDangerById,
-    saveHiddenDanger,
-    updateHiddenDanger,
+    approveEmployeeHazardReport,
     type HiddenDanger,
+    type ApproveEmployeeHazardReportReq,
   } from '@/api/production-safety';
+  import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
 
   const router = useRouter();
   const route = useRoute();
 
-  const operate = computed(() => (route.query.operate as string) || 'employee-report-hidden-trouble-create');
+  const operate = computed(() => (route.query.operate as string) || 'employee-report-hidden-trouble-view');
   const currentId = computed(() => Number(route.query.id));
 
-  const isCreateMode = computed(() => operate.value === 'employee-report-hidden-trouble-create');
-  const isEditMode = computed(() => operate.value === 'employee-report-hidden-trouble-edit');
   const isViewMode = computed(() => operate.value === 'employee-report-hidden-trouble-view');
+  const isApproveMode = computed(() => operate.value === 'employee-report-hidden-trouble-approve');
 
   const { ruleFormData, formRules, ruleFormConfig, cloneRuleFormData, beforeRouteLeave } =
     useFormConfigHook(ACADEMY_FILE_FORM_CONFIG, ACADEMY_FILE_FORM_DATA, ACADEMY_FILE_FORM_RULES);
 
-  // 查看模式下,所有字段设为只读
-  const viewFormConfig = ref(
-    ACADEMY_FILE_FORM_CONFIG.map((item) => ({
-      ...item,
-      componentProps: {
-        ...item.componentProps,
-        disabled: true,
-      },
-    })),
-  );
-
-  const computedFormConfig = computed(() => {
-    if (isViewMode.value) {
-      return viewFormConfig.value;
-    }
-    return ruleFormConfig.value;
-  });
+  const formConfig = computed(() => ruleFormConfig.value);
 
   const basicFormRef = ref<InstanceType<typeof BasicForm>>();
-
-  const handleValidate = async () => {
-    if (!basicFormRef.value) return;
-    const res = await basicFormRef.value.validateForm();
-    return res;
-  };
+  const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
+
+  // 附件列表
+  const attachmentList = ref<Array<{ file_name?: string; fileName?: string; url?: string; fileUrl?: string }>>([]);
+
+  // 审核相关
+  const approveDialogVisible = ref(false);
+  const approveForm = ref<ApproveEmployeeHazardReportReq>({
+    hazardId: 0,
+    node: 1,
+    approvalStatus: 2,
+    approvalContent: '',
+  });
 
   const getDetail = async () => {
     if (!currentId.value) return;
@@ -83,17 +116,29 @@
       const res = await queryHiddenDangerById(currentId.value);
       if (res) {
         // 映射接口字段到表单字段
-        ruleFormData.dangerName = res.dangerName || '';
-        ruleFormData.dangerType = res.dangerType ?? undefined;
-        ruleFormData.dangerLevel = res.dangerLevel ?? undefined;
+        ruleFormData.hazardDesc = res.hazardDesc || '';
         ruleFormData.location = res.location || '';
-        ruleFormData.description = res.description || '';
-        ruleFormData.reporterName = res.reporterName || '';
-        ruleFormData.reporterPhone = res.reporterPhone || '';
-        ruleFormData.reporterDept = res.reporterDept || '';
         ruleFormData.reportTime = res.reportTime || '';
-        ruleFormData.imagesText = res.images && res.images.length ? res.images.join(',') : '';
-        ruleFormData.status = res.status ?? 1;
+        ruleFormData.sourceTypeName = res.sourceTypeName || '';
+        ruleFormData.reporterName = res.reporterName || '';
+        ruleFormData.reporterJobNo = res.reporterJobNo || '';
+        ruleFormData.reporterMobile = res.reporterMobile || '';
+
+        // 处理附件
+        if (res.attachment) {
+          try {
+            const attachmentData = typeof res.attachment === 'string' ? JSON.parse(res.attachment) : res.attachment;
+            attachmentList.value = Array.isArray(attachmentData) ? attachmentData : [];
+          } catch (e) {
+            console.error('解析附件失败:', e);
+            attachmentList.value = [];
+          }
+        } else {
+          attachmentList.value = [];
+        }
+
+        // 设置审核表单的隐患ID
+        approveForm.value.hazardId = res.id;
       }
       cloneRuleFormData();
     } catch (e) {
@@ -102,51 +147,52 @@
     }
   };
 
-  const handleSubmit = async () => {
-    const res = await handleValidate();
-    if (!res) return;
-    try {
-      const images =
-        ruleFormData.imagesText && ruleFormData.imagesText.trim().length > 0
-          ? ruleFormData.imagesText.split(',').map((item: string) => item.trim()).filter(Boolean)
-          : [];
-
-      const basePayload = {
-        dangerName: ruleFormData.dangerName,
-        dangerType: Number(ruleFormData.dangerType),
-        dangerLevel: Number(ruleFormData.dangerLevel),
-        location: ruleFormData.location,
-        description: ruleFormData.description,
-        reporterName: ruleFormData.reporterName,
-        reporterPhone: ruleFormData.reporterPhone,
-        reporterDept: ruleFormData.reporterDept,
-        reportTime: ruleFormData.reportTime,
-        images,
-        status: ruleFormData.status ?? 1,
-      };
-
-      if (isCreateMode.value) {
-        await saveHiddenDanger(basePayload);
-        ElMessage.success('创建成功');
-      } else if (isEditMode.value && currentId.value) {
-        await updateHiddenDanger({
-          id: currentId.value,
-          ...basePayload,
-        });
-        ElMessage.success('保存成功');
+  const handlePreview = (url: string) => {
+    if (url) {
+      // 根据文件扩展名判断文件类型
+      const extension = url.split('.').pop()?.toLowerCase() || '';
+      let fileType: 'pdf' | 'word' | 'excel' | 'ppt' = 'pdf';
+      if (extension === 'doc' || extension === 'docx') {
+        fileType = 'word';
+      } else if (extension === 'xls' || extension === 'xlsx') {
+        fileType = 'excel';
+      } else if (extension === 'ppt' || extension === 'pptx') {
+        fileType = 'ppt';
       }
+      previewOnlineRef.value?.open(url, fileType);
+    }
+  };
 
+  const handleApproveClick = () => {
+    approveDialogVisible.value = true;
+  };
+
+  const handleApproveDialogClose = () => {
+    approveForm.value.approvalContent = '';
+    approveForm.value.node = 1;
+    approveForm.value.approvalStatus = 2;
+  };
+
+  const handleApproveSubmit = async () => {
+    if (!approveForm.value.node || !approveForm.value.approvalStatus) {
+      ElMessage.warning('请选择审批节点和审批状态');
+      return;
+    }
+    try {
+      await approveEmployeeHazardReport(approveForm.value);
+      ElMessage.success('审核成功');
+      approveDialogVisible.value = false;
       router.back();
     } catch (e) {
-      console.error('保存隐患上报失败:', e);
-      ElMessage.error('保存失败,请重试');
+      console.error('审核失败:', e);
+      ElMessage.error('审核失败,请重试');
     }
   };
 
   onMounted(() => {
     cloneRuleFormData();
     beforeRouteLeave();
-    if (isEditMode.value || isViewMode.value) {
+    if (currentId.value) {
       getDetail();
     }
   });
@@ -154,5 +200,11 @@
 
 <style scoped lang="scss">
   @use '@/styles/page-details-layout.scss' as *;
+
+  .file-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+  }
 </style>
 

+ 41 - 81
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/configs/form.ts

@@ -1,129 +1,89 @@
 import { FormConfig } from '@/types/basic-form';
 
-// 员工上报隐患管理表单配置
+// 员工上报隐患管理表单配置(详情页,所有字段禁用)
 export const ACADEMY_FILE_FORM_CONFIG: FormConfig[] = [
   {
-    prop: 'dangerName',
-    label: '隐患名称:',
+    prop: 'hazardDesc',
+    label: '隐患问题:',
     component: 'ElInput',
     componentProps: {
-      placeholder: '请输入隐患名称',
+      placeholder: '隐患问题描述',
+      disabled: true,
     },
   },
-  {
-    prop: 'dangerType',
-    label: '隐患类别ID:',
-    component: 'ElInput',
-    componentProps: {
-      placeholder: '请输入隐患类别ID',
-      type: 'number',
-    },
-  },
-  {
-    prop: 'dangerLevel',
-    label: '隐患等级:',
-    component: 'ElSelect',
-    componentProps: {
-      placeholder: '请选择隐患等级',
-    },
-    selectOptions: [
-      { label: '一般', value: 1 },
-      { label: '较大', value: 2 },
-      { label: '重大', value: 3 },
-    ],
-  },
   {
     prop: 'location',
-    label: '隐患位置:',
+    label: '隐患地点:',
     component: 'ElInput',
     componentProps: {
-      placeholder: '请输入隐患位置',
+      placeholder: '隐患地点',
+      disabled: true,
     },
   },
   {
-    prop: 'description',
-    label: '隐患描述:',
-    component: 'ElInput',
+    prop: 'reportTime',
+    label: '上报时间:',
+    component: 'ElDatePicker',
     componentProps: {
-      type: 'textarea',
-      rows: 3,
-      placeholder: '请输入隐患描述',
+      type: 'datetime',
+      placeholder: '上报时间',
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      disabled: true,
     },
   },
   {
-    prop: 'reporterName',
-    label: '上报人姓名:',
+    prop: 'sourceTypeName',
+    label: '提交类型:',
     component: 'ElInput',
     componentProps: {
-      placeholder: '请输入上报人姓名',
+      placeholder: '提交类型',
+      disabled: true,
     },
   },
   {
-    prop: 'reporterPhone',
-    label: '上报人电话:',
+    prop: 'reporterName',
+    label: '姓名:',
     component: 'ElInput',
     componentProps: {
-      placeholder: '请输入上报人电话',
+      placeholder: '姓名',
+      disabled: true,
     },
   },
   {
-    prop: 'reporterDept',
-    label: '上报人部门:',
+    prop: 'reporterJobNo',
+    label: '工号:',
     component: 'ElInput',
     componentProps: {
-      placeholder: '请输入上报人部门',
-    },
-  },
-  {
-    prop: 'reportTime',
-    label: '上报时间:',
-    component: 'ElDatePicker',
-    componentProps: {
-      type: 'datetime',
-      placeholder: '请选择上报时间',
-      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      placeholder: '工号',
+      disabled: true,
     },
   },
   {
-    prop: 'imagesText',
-    label: '隐患图片:',
+    prop: 'reporterMobile',
+    label: '联系电话:',
     component: 'ElInput',
     componentProps: {
-      type: 'textarea',
-      rows: 2,
-      placeholder: '请输入图片URL,多个用英文逗号分隔',
+      placeholder: '联系电话',
+      disabled: true,
     },
   },
   {
-    prop: 'status',
-    label: '处理状态:',
-    slot: 'status',
+    prop: 'attachment',
+    label: '附件:',
+    slot: 'attachment',
   },
 ];
 
 export const ACADEMY_FILE_FORM_DATA = {
-  dangerName: '',
-  dangerType: undefined as number | undefined,
-  dangerLevel: undefined as number | undefined,
+  hazardDesc: '',
   location: '',
-  description: '',
-  reporterName: '',
-  reporterPhone: '',
-  reporterDept: '',
   reportTime: '',
-  imagesText: '',
-  status: 1, // 默认待处理
+  sourceTypeName: '',
+  reporterName: '',
+  reporterJobNo: '',
+  reporterMobile: '',
+  attachment: '',
 };
 
-export const ACADEMY_FILE_FORM_RULES = {
-  dangerName: [{ required: true, message: '请输入隐患名称', trigger: 'blur' }],
-  dangerType: [{ required: true, message: '请输入隐患类别ID', trigger: 'blur' }],
-  dangerLevel: [{ required: true, message: '请选择隐患等级', trigger: 'change' }],
-  location: [{ required: true, message: '请输入隐患位置', trigger: 'blur' }],
-  description: [{ required: true, message: '请输入隐患描述', trigger: 'blur' }],
-  reporterName: [{ required: true, message: '请输入上报人姓名', trigger: 'blur' }],
-  reporterPhone: [{ required: true, message: '请输入上报人电话', trigger: 'blur' }],
-  reporterDept: [{ required: true, message: '请输入上报人部门', trigger: 'blur' }],
-  reportTime: [{ required: true, message: '请选择上报时间', trigger: 'change' }],
-};
+export const ACADEMY_FILE_FORM_RULES = {};
 

+ 21 - 21
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/configs/tables.ts

@@ -16,49 +16,49 @@ export const INVENTORY_TABLE_COLUMNS: TableColumnProps[] = [
     width: '80px',
   },
   {
-    label: '隐患名称',
-    prop: 'dangerName',
+    label: '隐患问题',
+    prop: 'hazardDesc',
     align: 'left',
-    minWidth: '160px',
+    minWidth: '180px',
   },
   {
-    label: '隐患类别',
-    prop: 'dangerTypeName',
+    label: '隐患地点',
+    prop: 'location',
     align: 'left',
-    minWidth: '140px',
+    minWidth: '150px',
   },
   {
-    label: '隐患等级',
-    prop: 'dangerLevelName',
-    align: 'center',
-    minWidth: '120px',
+    label: '上报时间',
+    prop: 'reportTime',
+    align: 'left',
+    minWidth: '180px',
   },
   {
-    label: '隐患位置',
-    prop: 'location',
+    label: '提交类型',
+    prop: 'sourceTypeName',
     align: 'left',
-    minWidth: '180px',
+    minWidth: '120px',
   },
   {
-    label: '上报人',
+    label: '姓名',
     prop: 'reporterName',
     align: 'left',
     minWidth: '120px',
   },
   {
-    label: '上报部门',
-    prop: 'reporterDept',
+    label: '工号',
+    prop: 'reporterJobNo',
     align: 'left',
-    minWidth: '140px',
+    minWidth: '120px',
   },
   {
-    label: '上报时间',
-    prop: 'reportTime',
+    label: '联系电话',
+    prop: 'reporterMobile',
     align: 'left',
-    minWidth: '180px',
+    minWidth: '140px',
   },
   {
-    label: '处理状态',
+    label: '状态',
     prop: 'status',
     slot: 'status',
     align: 'center',

+ 89 - 78
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/employeeReportHiddenTroubleManagement.vue

@@ -21,47 +21,32 @@
           <div class="act-search">
             <section class="select-box">
               <div class="select-box--item">
-                <span>关键字:</span>
+                <span>隐患问题:</span>
                 <el-input
                   v-model="tableQuery.queryParam.keyword"
-                  placeholder="隐患名称/位置/上报人"
+                  placeholder="请输入隐患问题、地点、姓名、工号或联系电话"
                   class="act-search-input"
                 />
               </div>
               <div class="select-box--item">
-                <span>隐患等级:</span>
+                <span>状态:</span>
                 <el-select
-                  v-model="tableQuery.queryParam.dangerLevel"
-                  placeholder="请选择隐患等级"
+                  v-model="tableQuery.queryParam.statusType"
+                  placeholder="请选择状态"
                   clearable
                 >
-                  <el-option label="一般" :value="1" />
-                  <el-option label="较大" :value="2" />
-                  <el-option label="重大" :value="3" />
+                  <el-option label="全部" :value="0" />
+                  <el-option label="待审核" :value="1" />
+                  <el-option label="审核通过" :value="2" />
+                  <el-option label="审核不通过" :value="3" />
                 </el-select>
               </div>
               <div class="select-box--item">
-                <span>处理状态:</span>
-                <el-select
-                  v-model="tableQuery.queryParam.status"
-                  placeholder="请选择处理状态"
-                  clearable
-                >
-                  <el-option label="待处理" :value="1" />
-                  <el-option label="处理中" :value="2" />
-                  <el-option label="已处理" :value="3" />
-                  <el-option label="已驳回" :value="4" />
-                </el-select>
-              </div>
-              <div class="select-box--item">
-                <span>上报日期:</span>
-                <el-date-picker
-                  v-model="reportDateRange"
-                  type="daterange"
-                  range-separator="至"
-                  start-placeholder="开始日期"
-                  end-placeholder="结束日期"
-                  value-format="YYYY-MM-DD"
+                <span>任务来源:</span>
+                <el-input
+                  v-model="tableQuery.queryParam.sourceTypeName"
+                  placeholder="请输入任务来源"
+                  class="act-search-input"
                 />
               </div>
             </section>
@@ -82,30 +67,29 @@
           >
             <template #status="scope">
               <span>
-                {{
-                  scope.row.status === 1
-                    ? '待处理'
-                    : scope.row.status === 2
-                      ? '处理中'
-                      : scope.row.status === 3
-                        ? '已处理'
-                        : scope.row.status === 4
-                          ? '已驳回'
-                          : '-'
-                }}
+                {{ getStatusTypeName(scope.row.statusType ?? getStatusType(scope.row.status)) }}
               </span>
             </template>
             <template #action="scope">
               <div class="action-container--div" style="justify-content: left">
-                <ActionButton text="编辑" @click="handleEdit(scope.row.id)" />
-                <ActionButton
-                  text="删除"
-                  :popconfirm="{
-                    title: '确定要删除?',
-                  }"
-                  @confirm="handleDelete(scope.row.id)"
-                />
-                <ActionButton text="查看" @click="handleView(scope.row.id)" />
+                <!-- 待审核:显示审核和查看 -->
+                <template v-if="scope.row.status === 1">
+                  <ActionButton text="审核" @click="handleApprove(scope.row.id)" />
+                  <ActionButton text="查看" @click="handleView(scope.row.id)" />
+                </template>
+                <!-- 审核通过(需求部门通过或安全部门通过):显示查看和入账 -->
+                <template v-else-if="scope.row.status === 2 || scope.row.status === 4">
+                  <ActionButton text="查看" @click="handleView(scope.row.id)" />
+                  <ActionButton text="入账" @click="handleAccount(scope.row.id)" />
+                </template>
+                <!-- 审核不通过(需求部门驳回或安全部门驳回):显示查看 -->
+                <template v-else-if="scope.row.status === 3 || scope.row.status === 5">
+                  <ActionButton text="查看" @click="handleView(scope.row.id)" />
+                </template>
+                <!-- 其他状态:显示查看 -->
+                <template v-else>
+                  <ActionButton text="查看" @click="handleView(scope.row.id)" />
+                </template>
               </div>
             </template>
           </BasicTable>
@@ -138,8 +122,11 @@
     queryHiddenDangerPage,
     deleteHiddenDanger,
     exportHiddenDanger,
+    approveEmployeeHazardReport,
+    accountEmployeeHazardReport,
     type QueryHiddenDangerReq,
     type HiddenDanger,
+    type ApproveEmployeeHazardReportReq,
   } from '@/api/production-safety';
   import { downloadByData } from '@/utils/file/download';
   import BatchImport from '@/components/batch-import/BatchImport.vue';
@@ -155,22 +142,41 @@
 
   const tableData = ref<HiddenDanger[]>([]);
 
+  // 将 status (1-7) 转换为 statusType (1-3)
+  const getStatusType = (status: number): number => {
+    // statusType = 1: 待审核 (status = 1)
+    if (status === 1) {
+      return 1;
+    }
+    // statusType = 2: 审核通过 (status = 2, 4, 6)
+    if (status === 2 || status === 4 || status === 6) {
+      return 2;
+    }
+    // statusType = 3: 审核不通过 (status = 3, 5, 7)
+    if (status === 3 || status === 5 || status === 7) {
+      return 3;
+    }
+    return 0;
+  };
+
+  // 根据 statusType 显示文本:1=待审核,2=审核通过,3=审核不通过
+  const getStatusTypeName = (statusType: number): string => {
+    if (statusType === 1) return '待审核';
+    if (statusType === 2) return '审核通过';
+    if (statusType === 3) return '审核不通过';
+    return '-';
+  };
+
   const tableQuery = reactive<QueryPageRequest<QueryHiddenDangerReq>>({
     pageNumber: pagination.pageNumber,
     pageSize: pagination.pageSize,
     queryParam: {
       keyword: '',
-      dangerType: undefined,
-      dangerLevel: undefined,
-      status: undefined,
-      startDate: '',
-      endDate: '',
+      statusType: undefined,
+      sourceTypeName: '',
     },
   });
 
-  // 上报日期范围(用于双向绑定日期组件)
-  const reportDateRange = ref<[string, string] | []>([]);
-
   const handleSizeChange = (value: number) => {
     pagination.pageSize = value;
     tableQuery.pageSize = value;
@@ -187,15 +193,6 @@
   async function getTableData() {
     tableConfig.loading = true;
     try {
-      // 将日期范围同步到查询参数
-      if (reportDateRange.value && reportDateRange.value.length === 2) {
-        tableQuery.queryParam.startDate = reportDateRange.value[0];
-        tableQuery.queryParam.endDate = reportDateRange.value[1];
-      } else {
-        tableQuery.queryParam.startDate = '';
-        tableQuery.queryParam.endDate = '';
-      }
-
       const res = await queryHiddenDangerPage(tableQuery);
       if (res) {
         tableData.value = res.records || [];
@@ -218,19 +215,15 @@
 
   const handleReset = () => {
     tableQuery.queryParam.keyword = '';
-    tableQuery.queryParam.dangerType = undefined;
-    tableQuery.queryParam.dangerLevel = undefined;
-    tableQuery.queryParam.status = undefined;
-    tableQuery.queryParam.startDate = '';
-    tableQuery.queryParam.endDate = '';
-    reportDateRange.value = [];
+    tableQuery.queryParam.statusType = undefined;
+    tableQuery.queryParam.sourceTypeName = '';
     handleSearch();
   };
 
   // 批量导入
   const batchImportVisible = ref(false);
   const { urlPrefix } = useGlobSetting();
-  const importApiUrl = ref(urlJoin(urlPrefix, '/admin/prod/hiddenDanger/importHiddenDanger'));
+  const importApiUrl = ref(urlJoin(urlPrefix, '/api/employeeHazardReport/importEmployeeHazardReport'));
   const templateUrl = ref('');
 
   const handleImport = () => {
@@ -246,11 +239,8 @@
     try {
       const exportParams: QueryHiddenDangerReq = {
         keyword: tableQuery.queryParam.keyword || undefined,
-        dangerType: tableQuery.queryParam.dangerType,
-        dangerLevel: tableQuery.queryParam.dangerLevel,
-        status: tableQuery.queryParam.status,
-        startDate: tableQuery.queryParam.startDate || undefined,
-        endDate: tableQuery.queryParam.endDate || undefined,
+        statusType: tableQuery.queryParam.statusType,
+        sourceTypeName: tableQuery.queryParam.sourceTypeName || undefined,
       };
       const response = await exportHiddenDanger(exportParams);
       if (response) {
@@ -304,6 +294,27 @@
     });
   };
 
+  const handleApprove = (id: number) => {
+    router.push({
+      name: 'employeeReportHiddenTroubleManagementItem',
+      query: {
+        id,
+        operate: 'employee-report-hidden-trouble-approve',
+      },
+    });
+  };
+
+  const handleAccount = async (hazardId: number) => {
+    try {
+      await accountEmployeeHazardReport(hazardId);
+      ElMessage.success('入账成功');
+      getTableData();
+    } catch (e) {
+      console.error('入账失败:', e);
+      ElMessage.error('入账失败,请重试');
+    }
+  };
+
   onMounted(() => {
     getTableData();
   });

+ 3 - 1
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/employeeReportHiddenTroubleManagement/employeeReportHiddenTroubleManagementItem.vue

@@ -25,8 +25,10 @@
         return '编辑员工上报隐患';
       case 'employee-report-hidden-trouble-view':
         return '查看员工上报隐患';
+      case 'employee-report-hidden-trouble-approve':
+        return '审核员工上报隐患';
       default:
-        return '未知操作';
+        return '查看员工上报隐患';
     }
   });
 </script>

+ 4 - 6
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleCategoryManagement/components/hiddenTroubleCategoryManagementDetail.vue

@@ -79,9 +79,8 @@
     try {
       const res = await queryDangerTypeById(currentId.value);
       if (res) {
-        ruleFormData.typeName = res.typeName || '';
-        ruleFormData.parentId = (res.parentId ?? undefined) as number | undefined;
-        ruleFormData.sort = res.sort ?? 0;
+        ruleFormData.categoryName = res.categoryName || '';
+        ruleFormData.description = res.description || '';
         ruleFormData.status = res.status ?? 1;
       }
       cloneRuleFormData();
@@ -96,9 +95,8 @@
     if (!res) return;
     try {
       const basePayload = {
-        typeName: ruleFormData.typeName,
-        parentId: ruleFormData.parentId ?? null,
-        sort: Number(ruleFormData.sort ?? 0),
+        categoryName: ruleFormData.categoryName,
+        description: ruleFormData.description || '',
         status: ruleFormData.status ?? 1,
       };
 

+ 12 - 18
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleCategoryManagement/configs/form.ts

@@ -3,7 +3,7 @@ import { FormConfig } from '@/types/basic-form';
 // 隐患类别管理表单配置
 export const ACADEMY_FILE_FORM_CONFIG: FormConfig[] = [
   {
-    prop: 'typeName',
+    prop: 'categoryName',
     label: '类别名称:',
     component: 'ElInput',
     componentProps: {
@@ -11,21 +11,15 @@ export const ACADEMY_FILE_FORM_CONFIG: FormConfig[] = [
     },
   },
   {
-    prop: 'parentId',
-    label: '父级ID:',
+    prop: 'description',
+    label: '类别描述:',
     component: 'ElInput',
     componentProps: {
-      placeholder: '请输入父级ID,顶级留空',
-      type: 'number',
-    },
-  },
-  {
-    prop: 'sort',
-    label: '排序:',
-    component: 'ElInput',
-    componentProps: {
-      placeholder: '请输入排序数字',
-      type: 'number',
+      type: 'textarea',
+      rows: 4,
+      placeholder: '请输入类别描述,最多300字',
+      maxlength: 300,
+      showWordLimit: true,
     },
   },
   {
@@ -36,13 +30,13 @@ export const ACADEMY_FILE_FORM_CONFIG: FormConfig[] = [
 ];
 
 export const ACADEMY_FILE_FORM_DATA = {
-  typeName: '',
-  parentId: undefined as number | undefined,
-  sort: 0,
+  categoryName: '',
+  description: '',
   status: 1, // 默认启用
 };
 
 export const ACADEMY_FILE_FORM_RULES = {
-  typeName: [{ required: true, message: '请输入类别名称', trigger: 'blur' }],
+  categoryName: [{ required: true, message: '请输入类别名称', trigger: 'blur' }],
+  status: [{ required: true, message: '请选择启用状态', trigger: 'change' }],
 };
 

+ 11 - 16
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleCategoryManagement/configs/tables.ts

@@ -10,29 +10,17 @@ export const TABLE_OPTIONS = {
 // 隐患类别管理表格列
 export const INVENTORY_TABLE_COLUMNS: TableColumnProps[] = [
   {
-    label: '号',
-    type: 'index',
+    label: '号',
+    prop: 'id',
     align: 'center',
-    width: '80px',
+    width: '100px',
   },
   {
     label: '类别名称',
-    prop: 'typeName',
+    prop: 'categoryName',
     align: 'left',
     minWidth: '160px',
   },
-  {
-    label: '父级类别',
-    prop: 'parentName',
-    align: 'left',
-    minWidth: '160px',
-  },
-  {
-    label: '排序',
-    prop: 'sort',
-    align: 'center',
-    minWidth: '100px',
-  },
   {
     label: '状态',
     prop: 'status',
@@ -40,6 +28,13 @@ export const INVENTORY_TABLE_COLUMNS: TableColumnProps[] = [
     align: 'center',
     minWidth: '120px',
   },
+  {
+    label: '类别描述',
+    prop: 'description',
+    align: 'left',
+    minWidth: '200px',
+    showOverflowTooltip: true,
+  },
   {
     label: '创建时间',
     prop: 'createdAt',

+ 33 - 6
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/hiddenTroubleCategoryManagement/hiddenTroubleCategoryManagement.vue

@@ -21,10 +21,10 @@
           <div class="act-search">
             <section class="select-box">
               <div class="select-box--item">
-                <span>关键:</span>
+                <span>关键:</span>
                 <el-input
                   v-model="tableQuery.queryParam.keyword"
-                  placeholder="类别名称"
+                  placeholder="类别名称/描述"
                   class="act-search-input"
                 />
               </div>
@@ -39,6 +39,17 @@
                   <el-option label="禁用" :value="0" />
                 </el-select>
               </div>
+              <div class="select-box--item">
+                <span>创建时间:</span>
+                <el-date-picker
+                  v-model="createDateRange"
+                  type="daterange"
+                  range-separator="至"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  value-format="YYYY-MM-DD"
+                />
+              </div>
             </section>
             <section class="search-btn">
               <el-button type="primary" @click="handleSearch">查询</el-button>
@@ -125,11 +136,15 @@
     pageSize: pagination.pageSize,
     queryParam: {
       keyword: '',
-      parentId: undefined,
       status: undefined,
+      startDate: '',
+      endDate: '',
     },
   });
 
+  // 创建时间范围(用于双向绑定日期组件)
+  const createDateRange = ref<[string, string] | []>([]);
+
   const handleSizeChange = (value: number) => {
     pagination.pageSize = value;
     tableQuery.pageSize = value;
@@ -146,6 +161,15 @@
   async function getTableData() {
     tableConfig.loading = true;
     try {
+      // 将日期范围同步到查询参数
+      if (createDateRange.value && createDateRange.value.length === 2) {
+        tableQuery.queryParam.startDate = createDateRange.value[0];
+        tableQuery.queryParam.endDate = createDateRange.value[1];
+      } else {
+        tableQuery.queryParam.startDate = '';
+        tableQuery.queryParam.endDate = '';
+      }
+
       const res = await queryDangerTypePage(tableQuery);
       if (res) {
         tableData.value = res.records || [];
@@ -169,14 +193,16 @@
   const handleReset = () => {
     tableQuery.queryParam.keyword = '';
     tableQuery.queryParam.status = undefined;
-    tableQuery.queryParam.parentId = undefined;
+    tableQuery.queryParam.startDate = '';
+    tableQuery.queryParam.endDate = '';
+    createDateRange.value = [];
     handleSearch();
   };
 
   // 批量导入
   const batchImportVisible = ref(false);
   const { urlPrefix } = useGlobSetting();
-  const importApiUrl = ref(urlJoin(urlPrefix, '/admin/prod/dangerType/importDangerType'));
+  const importApiUrl = ref(urlJoin(urlPrefix, '/api/hazardCategory/importHazardCategory'));
   const templateUrl = ref('');
 
   const handleImport = () => {
@@ -193,7 +219,8 @@
       const exportParams: QueryDangerTypeReq = {
         keyword: tableQuery.queryParam.keyword || undefined,
         status: tableQuery.queryParam.status,
-        parentId: tableQuery.queryParam.parentId,
+        startDate: tableQuery.queryParam.startDate || undefined,
+        endDate: tableQuery.queryParam.endDate || undefined,
       };
       const response = await exportDangerType(exportParams);
       if (response) {

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

@@ -226,7 +226,6 @@
           }));
           break;
         case 'safetyResponsibleDepartment':
-          alert('');
           formValue.safetyDepartmentManager = null;
           formValue.safetySpecificPerson = null;
           safetyDepartmentUserOptions.value = (res.records || []).map((u: any) => ({

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

@@ -119,7 +119,6 @@
   import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
   import {
     queryUserPageByUsername,
-    areaCheckListSavaArea,
     areaCheckListQueryDetail,
     areaCheckListUpdateArea,
   } from '@/api/production-safety/responsibility-implementation';
@@ -207,7 +206,7 @@
     formRef.value?.validate((valid: boolean) => {
       if (valid) {
         submiting.value = true;
-        areaCheckListSavaArea({
+        areaCheckListUpdateArea({
           ...formValue,
           type: 2,
         })

+ 1 - 1
src/views/production-safety/implement-safety-duty/public-area-responsibilities/edit.vue

@@ -208,7 +208,7 @@
     formRef.value?.validate((valid: boolean) => {
       if (valid) {
         submiting.value = true;
-        areaCheckListSavaArea({
+        areaCheckListUpdateArea({
           ...formValue,
           type: 1,
         })

+ 1 - 1
src/views/production-safety/productionSafetySystem/collegeFileManagement/collegeFileManagement.vue

@@ -171,7 +171,7 @@
       const pageQuery: ProductionSafetyFilePageQuery = {
         pageNumber: pagination.pageNumber,
         pageSize: pagination.pageSize,
-        data: {
+        queryParam: {
           keyword: queryParams.keyword || undefined,
           status: queryParams.status,
           classifyName: queryParams.classifyName || undefined,

+ 78 - 24
src/views/production-safety/productionSafetySystem/collegeFileManagement/components/collegeFileManagementDetail.vue

@@ -16,7 +16,9 @@
         <UploadFiles
           label="上传文件"
           :maxCount="1"
-          :fileList="uploadFileList"
+          :file-list="ruleFormData.fileUrlList"
+          :disabled="isViewMode"
+          :allow-all-file-types="true"
           @uploadSuccess="handleUploadSuccess"
         />
       </template>
@@ -69,6 +71,7 @@
     type ProductionSafetyFile,
   } from '@/api/production-safety-system';
   import type { FileItem } from '@/components/UploadFiles/types';
+  import { formatAttachmentList } from '@/components/UploadFiles/utils';
 
   const router = useRouter();
   const route = useRoute();
@@ -119,21 +122,43 @@
   };
 
   // 文件上传
-  const uploadFileList = ref<FileItem[]>([]);
-
   const handleUploadSuccess = (files: FileItem[]) => {
-    uploadFileList.value = files;
-    if (files.length > 0 && files[0].file) {
-      // 这里需要实际上传文件到服务器,获取 fileUrl
-      // 暂时使用文件对象,实际应该调用上传接口
-      ruleFormData.fileUrl = files[0].file.name; // 临时处理,需要替换为实际上传后的URL
-    }
+    ruleFormData.fileUrlList = files;
   };
 
-  const getFileName = (url: string) => {
-    if (!url) return '';
-    const parts = url.split('/');
-    return parts[parts.length - 1];
+  // 将逗号分隔的URL字符串转换为FileItem数组
+  const convertFileUrlToFileItems = (fileUrl: string): FileItem[] => {
+    if (!fileUrl || !fileUrl.trim()) {
+      return [];
+    }
+    
+    // 按逗号分割URL
+    const urls = fileUrl.split(',').map(url => url.trim()).filter(url => url);
+    
+    return urls.map((url, index) => {
+      // 从URL中提取文件名
+      const urlParts = url.split('/');
+      const fileName = urlParts[urlParts.length - 1] || `附件${index + 1}`;
+      
+      // 根据文件扩展名判断文件类型
+      const extension = fileName.split('.').pop()?.toLowerCase() || '';
+      let fileType = 'pdf';
+      if (extension === 'doc' || extension === 'docx') {
+        fileType = 'word';
+      } else if (extension === 'xls' || extension === 'xlsx') {
+        fileType = 'excel';
+      } else if (extension === 'ppt' || extension === 'pptx') {
+        fileType = 'ppt';
+      }
+      
+      return {
+        fileId: Date.now() + index,
+        fileName,
+        fileType,
+        fileSize: '0',
+        fileUrl: url,
+      };
+    });
   };
 
   const handleValidate = async () => {
@@ -159,16 +184,7 @@
         ruleFormData.status = res.status ?? 1;
         
         // 如果有文件URL,转换为FileItem格式
-        if (res.fileUrl) {
-          uploadFileList.value = [
-            {
-              fileId: Date.now(),
-              fileName: getFileName(res.fileUrl),
-              fileType: res.fileFormat?.toLowerCase() === 'pdf' ? 'pdf' : 'word',
-              fileSize: '0KB',
-            },
-          ];
-        }
+        ruleFormData.fileUrlList = convertFileUrlToFileItems(res.fileUrl || '');
       }
       cloneRuleFormData();
     } catch (e) {
@@ -180,7 +196,45 @@
   const handleSubmit = async () => {
     const res = await handleValidate();
     if (!res) return;
+    
+    // 验证文件上传(必填)
+    if (!ruleFormData.fileUrlList || ruleFormData.fileUrlList.length === 0) {
+      ElMessage.warning('请上传文件');
+      return;
+    }
+    
     try {
+      // 处理文件上传:先上传文件获取 URL,然后提取 fileUrl
+      let fileUrl = '';
+      if (ruleFormData.fileUrlList && ruleFormData.fileUrlList.length > 0) {
+        // 分离已有URL的文件和新上传的文件
+        const existingFiles: string[] = [];
+        const newFiles: FileItem[] = [];
+        
+        ruleFormData.fileUrlList.forEach((file: FileItem) => {
+          // 如果文件已经有 fileUrl 且没有 file 对象,说明是已有文件
+          if (file.fileUrl && !file.file) {
+            existingFiles.push(file.fileUrl);
+          } else {
+            // 否则是需要上传的新文件
+            newFiles.push(file);
+          }
+        });
+
+        // 上传新文件
+        let uploadedUrls: string[] = [];
+        if (newFiles.length > 0) {
+          const uploadedFiles = await formatAttachmentList(newFiles);
+          uploadedUrls = uploadedFiles
+            .map((file: any) => file.fileUrl || file.url || '')
+            .filter((url: string) => url);
+        }
+
+        // 合并已有URL和新上传的URL,取第一个作为fileUrl
+        const allUrls = [...existingFiles, ...uploadedUrls].filter((url: string) => url);
+        fileUrl = allUrls.length > 0 ? allUrls[0] : '';
+      }
+
       const basePayload: ProductionSafetyFile = {
         fileName: ruleFormData.fileName,
         classifyName: ruleFormData.classifyName,
@@ -188,7 +242,7 @@
         fileVersion: ruleFormData.fileVersion,
         fileFormat: ruleFormData.fileFormat,
         releaseDate: ruleFormData.releaseDate,
-        fileUrl: ruleFormData.fileUrl || undefined,
+        fileUrl: fileUrl || undefined,
         content: ruleFormData.content || undefined,
         status: ruleFormData.status ?? 1,
       };

+ 1 - 0
src/views/production-safety/productionSafetySystem/collegeFileManagement/configs/form.ts

@@ -77,6 +77,7 @@ export const ACADEMY_FILE_FORM_DATA = {
   fileFormat: '',
   releaseDate: '',
   fileUrl: '',
+  fileUrlList: [] as any[], // 文件列表(FileItem数组)
   content: '',
   status: 1, // 默认启用
 };

+ 78 - 24
src/views/production-safety/productionSafetySystem/doubleSystemManagement/components/doubleSystemManagementDetail.vue

@@ -16,7 +16,9 @@
         <UploadFiles
           label="上传文件"
           :maxCount="1"
-          :fileList="uploadFileList"
+          :file-list="ruleFormData.fileUrlList"
+          :disabled="isViewMode"
+          :allow-all-file-types="true"
           @uploadSuccess="handleUploadSuccess"
         />
       </template>
@@ -69,6 +71,7 @@
     type ProductionSafetyFile,
   } from '@/api/production-safety-system';
   import type { FileItem } from '@/components/UploadFiles/types';
+  import { formatAttachmentList } from '@/components/UploadFiles/utils';
 
   const router = useRouter();
   const route = useRoute();
@@ -119,21 +122,43 @@
   };
 
   // 文件上传
-  const uploadFileList = ref<FileItem[]>([]);
-
   const handleUploadSuccess = (files: FileItem[]) => {
-    uploadFileList.value = files;
-    if (files.length > 0 && files[0].file) {
-      // 这里需要实际上传文件到服务器,获取 fileUrl
-      // 暂时使用文件对象,实际应该调用上传接口
-      ruleFormData.fileUrl = files[0].file.name; // 临时处理,需要替换为实际上传后的URL
-    }
+    ruleFormData.fileUrlList = files;
   };
 
-  const getFileName = (url: string) => {
-    if (!url) return '';
-    const parts = url.split('/');
-    return parts[parts.length - 1];
+  // 将逗号分隔的URL字符串转换为FileItem数组
+  const convertFileUrlToFileItems = (fileUrl: string): FileItem[] => {
+    if (!fileUrl || !fileUrl.trim()) {
+      return [];
+    }
+    
+    // 按逗号分割URL
+    const urls = fileUrl.split(',').map(url => url.trim()).filter(url => url);
+    
+    return urls.map((url, index) => {
+      // 从URL中提取文件名
+      const urlParts = url.split('/');
+      const fileName = urlParts[urlParts.length - 1] || `附件${index + 1}`;
+      
+      // 根据文件扩展名判断文件类型
+      const extension = fileName.split('.').pop()?.toLowerCase() || '';
+      let fileType = 'pdf';
+      if (extension === 'doc' || extension === 'docx') {
+        fileType = 'word';
+      } else if (extension === 'xls' || extension === 'xlsx') {
+        fileType = 'excel';
+      } else if (extension === 'ppt' || extension === 'pptx') {
+        fileType = 'ppt';
+      }
+      
+      return {
+        fileId: Date.now() + index,
+        fileName,
+        fileType,
+        fileSize: '0',
+        fileUrl: url,
+      };
+    });
   };
 
   const handleValidate = async () => {
@@ -159,16 +184,7 @@
         ruleFormData.status = res.status ?? 1;
         
         // 如果有文件URL,转换为FileItem格式
-        if (res.fileUrl) {
-          uploadFileList.value = [
-            {
-              fileId: Date.now(),
-              fileName: getFileName(res.fileUrl),
-              fileType: res.fileFormat?.toLowerCase() === 'pdf' ? 'pdf' : 'word',
-              fileSize: '0KB',
-            },
-          ];
-        }
+        ruleFormData.fileUrlList = convertFileUrlToFileItems(res.fileUrl || '');
       }
       cloneRuleFormData();
     } catch (e) {
@@ -180,7 +196,45 @@
   const handleSubmit = async () => {
     const res = await handleValidate();
     if (!res) return;
+    
+    // 验证文件上传(必填)
+    if (!ruleFormData.fileUrlList || ruleFormData.fileUrlList.length === 0) {
+      ElMessage.warning('请上传文件');
+      return;
+    }
+    
     try {
+      // 处理文件上传:先上传文件获取 URL,然后提取 fileUrl
+      let fileUrl = '';
+      if (ruleFormData.fileUrlList && ruleFormData.fileUrlList.length > 0) {
+        // 分离已有URL的文件和新上传的文件
+        const existingFiles: string[] = [];
+        const newFiles: FileItem[] = [];
+        
+        ruleFormData.fileUrlList.forEach((file: FileItem) => {
+          // 如果文件已经有 fileUrl 且没有 file 对象,说明是已有文件
+          if (file.fileUrl && !file.file) {
+            existingFiles.push(file.fileUrl);
+          } else {
+            // 否则是需要上传的新文件
+            newFiles.push(file);
+          }
+        });
+
+        // 上传新文件
+        let uploadedUrls: string[] = [];
+        if (newFiles.length > 0) {
+          const uploadedFiles = await formatAttachmentList(newFiles);
+          uploadedUrls = uploadedFiles
+            .map((file: any) => file.fileUrl || file.url || '')
+            .filter((url: string) => url);
+        }
+
+        // 合并已有URL和新上传的URL,取第一个作为fileUrl
+        const allUrls = [...existingFiles, ...uploadedUrls].filter((url: string) => url);
+        fileUrl = allUrls.length > 0 ? allUrls[0] : '';
+      }
+
       const basePayload: ProductionSafetyFile = {
         fileName: ruleFormData.fileName,
         classifyName: ruleFormData.classifyName,
@@ -188,7 +242,7 @@
         fileVersion: ruleFormData.fileVersion,
         fileFormat: ruleFormData.fileFormat,
         releaseDate: ruleFormData.releaseDate,
-        fileUrl: ruleFormData.fileUrl || undefined,
+        fileUrl: fileUrl || undefined,
         content: ruleFormData.content || undefined,
         status: ruleFormData.status ?? 1,
       };

+ 1 - 0
src/views/production-safety/productionSafetySystem/doubleSystemManagement/configs/form.ts

@@ -77,6 +77,7 @@ export const DUAL_SYSTEM_FORM_DATA = {
   fileFormat: '',
   releaseDate: '',
   fileUrl: '',
+  fileUrlList: [] as any[], // 文件列表(FileItem数组)
   content: '',
   status: 1, // 默认启用
 };

+ 1 - 1
src/views/production-safety/productionSafetySystem/doubleSystemManagement/doubleSystemManagement.vue

@@ -171,7 +171,7 @@
       const pageQuery: ProductionSafetyFilePageQuery = {
         pageNumber: pagination.pageNumber,
         pageSize: pagination.pageSize,
-        data: {
+        queryParam: {
           keyword: queryParams.keyword || undefined,
           status: queryParams.status,
           classifyName: queryParams.classifyName || undefined,

+ 79 - 25
src/views/production-safety/productionSafetySystem/lawManagement/components/lawManagementDetail.vue

@@ -16,7 +16,9 @@
         <UploadFiles
           label="上传文件"
           :maxCount="1"
-          :fileList="uploadFileList"
+          :file-list="ruleFormData.fileUrlList"
+          :disabled="isViewMode"
+          :allow-all-file-types="true"
           @uploadSuccess="handleUploadSuccess"
         />
       </template>
@@ -69,6 +71,7 @@
     type ProductionSafetyFile,
   } from '@/api/production-safety-system';
   import type { FileItem } from '@/components/UploadFiles/types';
+  import { formatAttachmentList } from '@/components/UploadFiles/utils';
 
   const router = useRouter();
   const route = useRoute();
@@ -119,21 +122,43 @@
   };
 
   // 文件上传
-  const uploadFileList = ref<FileItem[]>([]);
-
   const handleUploadSuccess = (files: FileItem[]) => {
-    uploadFileList.value = files;
-    if (files.length > 0 && files[0].file) {
-      // 这里需要实际上传文件到服务器,获取 fileUrl
-      // 暂时使用文件对象,实际应该调用上传接口
-      ruleFormData.fileUrl = files[0].file.name; // 临时处理,需要替换为实际上传后的URL
-    }
+    ruleFormData.fileUrlList = files;
   };
 
-  const getFileName = (url: string) => {
-    if (!url) return '';
-    const parts = url.split('/');
-    return parts[parts.length - 1];
+  // 将逗号分隔的URL字符串转换为FileItem数组
+  const convertFileUrlToFileItems = (fileUrl: string): FileItem[] => {
+    if (!fileUrl || !fileUrl.trim()) {
+      return [];
+    }
+    
+    // 按逗号分割URL
+    const urls = fileUrl.split(',').map(url => url.trim()).filter(url => url);
+    
+    return urls.map((url, index) => {
+      // 从URL中提取文件名
+      const urlParts = url.split('/');
+      const fileName = urlParts[urlParts.length - 1] || `附件${index + 1}`;
+      
+      // 根据文件扩展名判断文件类型
+      const extension = fileName.split('.').pop()?.toLowerCase() || '';
+      let fileType = 'pdf';
+      if (extension === 'doc' || extension === 'docx') {
+        fileType = 'word';
+      } else if (extension === 'xls' || extension === 'xlsx') {
+        fileType = 'excel';
+      } else if (extension === 'ppt' || extension === 'pptx') {
+        fileType = 'ppt';
+      }
+      
+      return {
+        fileId: Date.now() + index,
+        fileName,
+        fileType,
+        fileSize: '0',
+        fileUrl: url,
+      };
+    });
   };
 
   const handleValidate = async () => {
@@ -159,16 +184,7 @@
         ruleFormData.status = res.status ?? 1;
         
         // 如果有文件URL,转换为FileItem格式
-        if (res.fileUrl) {
-          uploadFileList.value = [
-            {
-              fileId: Date.now(),
-              fileName: getFileName(res.fileUrl),
-              fileType: res.fileFormat?.toLowerCase() === 'pdf' ? 'pdf' : 'word',
-              fileSize: '0KB',
-            },
-          ];
-        }
+        ruleFormData.fileUrlList = convertFileUrlToFileItems(res.fileUrl || '');
       }
       cloneRuleFormData();
     } catch (e) {
@@ -180,7 +196,45 @@
   const handleSubmit = async () => {
     const res = await handleValidate();
     if (!res) return;
+    
+    // 验证文件上传(必填)
+    if (!ruleFormData.fileUrlList || ruleFormData.fileUrlList.length === 0) {
+      ElMessage.warning('请上传文件');
+      return;
+    }
+    
     try {
+      // 处理文件上传:先上传文件获取 URL,然后提取 fileUrl
+      let fileUrl = '';
+      if (ruleFormData.fileUrlList && ruleFormData.fileUrlList.length > 0) {
+        // 分离已有URL的文件和新上传的文件
+        const existingFiles: string[] = [];
+        const newFiles: FileItem[] = [];
+        
+        ruleFormData.fileUrlList.forEach((file: FileItem) => {
+          // 如果文件已经有 fileUrl 且没有 file 对象,说明是已有文件
+          if (file.fileUrl && !file.file) {
+            existingFiles.push(file.fileUrl);
+          } else {
+            // 否则是需要上传的新文件
+            newFiles.push(file);
+          }
+        });
+
+        // 上传新文件
+        let uploadedUrls: string[] = [];
+        if (newFiles.length > 0) {
+          const uploadedFiles = await formatAttachmentList(newFiles);
+          uploadedUrls = uploadedFiles
+            .map((file: any) => file.fileUrl || file.url || '')
+            .filter((url: string) => url);
+        }
+
+        // 合并已有URL和新上传的URL,取第一个作为fileUrl
+        const allUrls = [...existingFiles, ...uploadedUrls].filter((url: string) => url);
+        fileUrl = allUrls.length > 0 ? allUrls[0] : '';
+      }
+
       const basePayload: ProductionSafetyFile = {
         fileName: ruleFormData.fileName,
         classifyName: ruleFormData.classifyName,
@@ -188,7 +242,7 @@
         fileVersion: ruleFormData.fileVersion,
         fileFormat: ruleFormData.fileFormat,
         releaseDate: ruleFormData.releaseDate,
-        fileUrl: ruleFormData.fileUrl || undefined,
+        fileUrl: fileUrl || undefined,
         content: ruleFormData.content || undefined,
         status: ruleFormData.status ?? 1,
       };
@@ -219,7 +273,7 @@
       ruleFormData.fileFormat = ruleFormData.fileFormat || '';
       ruleFormData.status = ruleFormData.status ?? 1;
       ruleFormData.content = ruleFormData.content || '';
-      uploadFileList.value = [];
+      ruleFormData.fileUrlList = [];
     }
     if (isEditMode.value || isViewMode.value) {
       getDetail();

+ 2 - 0
src/views/production-safety/productionSafetySystem/lawManagement/configs/form.ts

@@ -77,6 +77,7 @@ export const LAW_REGULATION_FORM_DATA = {
   fileFormat: '',
   releaseDate: '',
   fileUrl: '',
+  fileUrlList: [] as any[], // 文件列表(FileItem数组)
   content: '',
   status: 1, // 默认启用
 };
@@ -88,4 +89,5 @@ export const LAW_REGULATION_FORM_RULES = {
   fileVersion: [{ required: true, message: '请输入文件版本号', trigger: 'blur' }],
   fileFormat: [{ required: true, message: '请选择文件格式', trigger: 'change' }],
   releaseDate: [{ required: true, message: '请选择发布日期', trigger: 'change' }],
+  // 文件上传的验证在 handleSubmit 中手动处理
 };

+ 1 - 1
src/views/production-safety/productionSafetySystem/lawManagement/lawManagement.vue

@@ -171,7 +171,7 @@
       const pageQuery: ProductionSafetyFilePageQuery = {
         pageNumber: pagination.pageNumber,
         pageSize: pagination.pageSize,
-        data: {
+        queryParam: {
           keyword: queryParams.keyword || undefined,
           status: queryParams.status,
           classifyName: queryParams.classifyName || undefined,

+ 78 - 24
src/views/production-safety/productionSafetySystem/safetyStandardizationSystemManagement/components/safetyStandardizationSystemManagementDetail.vue

@@ -16,7 +16,9 @@
         <UploadFiles
           label="上传文件"
           :maxCount="1"
-          :fileList="uploadFileList"
+          :file-list="ruleFormData.fileUrlList"
+          :disabled="isViewMode"
+          :allow-all-file-types="true"
           @uploadSuccess="handleUploadSuccess"
         />
       </template>
@@ -73,6 +75,7 @@
     type ProductionSafetyFile,
   } from '@/api/production-safety-system';
   import type { FileItem } from '@/components/UploadFiles/types';
+  import { formatAttachmentList } from '@/components/UploadFiles/utils';
 
   const router = useRouter();
   const route = useRoute();
@@ -127,21 +130,43 @@
   };
 
   // 文件上传
-  const uploadFileList = ref<FileItem[]>([]);
-
   const handleUploadSuccess = (files: FileItem[]) => {
-    uploadFileList.value = files;
-    if (files.length > 0 && files[0].file) {
-      // 这里需要实际上传文件到服务器,获取 fileUrl
-      // 暂时使用文件对象,实际应该调用上传接口
-      ruleFormData.fileUrl = files[0].file.name; // 临时处理,需要替换为实际上传后的URL
-    }
+    ruleFormData.fileUrlList = files;
   };
 
-  const getFileName = (url: string) => {
-    if (!url) return '';
-    const parts = url.split('/');
-    return parts[parts.length - 1];
+  // 将逗号分隔的URL字符串转换为FileItem数组
+  const convertFileUrlToFileItems = (fileUrl: string): FileItem[] => {
+    if (!fileUrl || !fileUrl.trim()) {
+      return [];
+    }
+    
+    // 按逗号分割URL
+    const urls = fileUrl.split(',').map(url => url.trim()).filter(url => url);
+    
+    return urls.map((url, index) => {
+      // 从URL中提取文件名
+      const urlParts = url.split('/');
+      const fileName = urlParts[urlParts.length - 1] || `附件${index + 1}`;
+      
+      // 根据文件扩展名判断文件类型
+      const extension = fileName.split('.').pop()?.toLowerCase() || '';
+      let fileType = 'pdf';
+      if (extension === 'doc' || extension === 'docx') {
+        fileType = 'word';
+      } else if (extension === 'xls' || extension === 'xlsx') {
+        fileType = 'excel';
+      } else if (extension === 'ppt' || extension === 'pptx') {
+        fileType = 'ppt';
+      }
+      
+      return {
+        fileId: Date.now() + index,
+        fileName,
+        fileType,
+        fileSize: '0',
+        fileUrl: url,
+      };
+    });
   };
 
   const handleValidate = async () => {
@@ -167,16 +192,7 @@
         ruleFormData.status = res.status ?? 1;
         
         // 如果有文件URL,转换为FileItem格式
-        if (res.fileUrl) {
-          uploadFileList.value = [
-            {
-              fileId: Date.now(),
-              fileName: getFileName(res.fileUrl),
-              fileType: res.fileFormat?.toLowerCase() === 'pdf' ? 'pdf' : 'word',
-              fileSize: '0KB',
-            },
-          ];
-        }
+        ruleFormData.fileUrlList = convertFileUrlToFileItems(res.fileUrl || '');
       }
       cloneRuleFormData();
     } catch (e) {
@@ -188,7 +204,45 @@
   const handleSubmit = async () => {
     const res = await handleValidate();
     if (!res) return;
+    
+    // 验证文件上传(必填)
+    if (!ruleFormData.fileUrlList || ruleFormData.fileUrlList.length === 0) {
+      ElMessage.warning('请上传文件');
+      return;
+    }
+    
     try {
+      // 处理文件上传:先上传文件获取 URL,然后提取 fileUrl
+      let fileUrl = '';
+      if (ruleFormData.fileUrlList && ruleFormData.fileUrlList.length > 0) {
+        // 分离已有URL的文件和新上传的文件
+        const existingFiles: string[] = [];
+        const newFiles: FileItem[] = [];
+        
+        ruleFormData.fileUrlList.forEach((file: FileItem) => {
+          // 如果文件已经有 fileUrl 且没有 file 对象,说明是已有文件
+          if (file.fileUrl && !file.file) {
+            existingFiles.push(file.fileUrl);
+          } else {
+            // 否则是需要上传的新文件
+            newFiles.push(file);
+          }
+        });
+
+        // 上传新文件
+        let uploadedUrls: string[] = [];
+        if (newFiles.length > 0) {
+          const uploadedFiles = await formatAttachmentList(newFiles);
+          uploadedUrls = uploadedFiles
+            .map((file: any) => file.fileUrl || file.url || '')
+            .filter((url: string) => url);
+        }
+
+        // 合并已有URL和新上传的URL,取第一个作为fileUrl
+        const allUrls = [...existingFiles, ...uploadedUrls].filter((url: string) => url);
+        fileUrl = allUrls.length > 0 ? allUrls[0] : '';
+      }
+
       const basePayload: ProductionSafetyFile = {
         fileName: ruleFormData.fileName,
         classifyName: ruleFormData.classifyName,
@@ -196,7 +250,7 @@
         fileVersion: ruleFormData.fileVersion,
         fileFormat: ruleFormData.fileFormat,
         releaseDate: ruleFormData.releaseDate,
-        fileUrl: ruleFormData.fileUrl || undefined,
+        fileUrl: fileUrl || undefined,
         content: ruleFormData.content || undefined,
         status: ruleFormData.status ?? 1,
       };

+ 1 - 0
src/views/production-safety/productionSafetySystem/safetyStandardizationSystemManagement/configs/form.ts

@@ -77,6 +77,7 @@ export const SAFETY_STANDARDIZATION_FORM_DATA = {
   fileFormat: '',
   releaseDate: '',
   fileUrl: '',
+  fileUrlList: [] as any[], // 文件列表(FileItem数组)
   content: '',
   status: 1, // 默认启用
 };

+ 1 - 1
src/views/production-safety/productionSafetySystem/safetyStandardizationSystemManagement/safetyStandardizationSystemManagement.vue

@@ -171,7 +171,7 @@
       const pageQuery: ProductionSafetyFilePageQuery = {
         pageNumber: pagination.pageNumber,
         pageSize: pagination.pageSize,
-        data: {
+        queryParam: {
           keyword: queryParams.keyword || undefined,
           status: queryParams.status,
           classifyName: queryParams.classifyName || undefined,

+ 78 - 24
src/views/production-safety/productionSafetySystem/safetyTraining/components/safetyTrainingDetail.vue

@@ -16,7 +16,9 @@
         <UploadFiles
           label="上传文件"
           :maxCount="1"
-          :fileList="uploadFileList"
+          :file-list="ruleFormData.fileUrlList"
+          :disabled="isViewMode"
+          :allow-all-file-types="true"
           @uploadSuccess="handleUploadSuccess"
         />
       </template>
@@ -73,6 +75,7 @@
     type ProductionSafetyFile,
   } from '@/api/production-safety-system';
   import type { FileItem } from '@/components/UploadFiles/types';
+  import { formatAttachmentList } from '@/components/UploadFiles/utils';
 
   const router = useRouter();
   const route = useRoute();
@@ -126,21 +129,43 @@
   };
 
   // 文件上传
-  const uploadFileList = ref<FileItem[]>([]);
-
   const handleUploadSuccess = (files: FileItem[]) => {
-    uploadFileList.value = files;
-    if (files.length > 0 && files[0].file) {
-      // 这里需要实际上传文件到服务器,获取 fileUrl
-      // 暂时使用文件对象,实际应该调用上传接口
-      ruleFormData.fileUrl = files[0].file.name; // 临时处理,需要替换为实际上传后的URL
-    }
+    ruleFormData.fileUrlList = files;
   };
 
-  const getFileName = (url: string) => {
-    if (!url) return '';
-    const parts = url.split('/');
-    return parts[parts.length - 1];
+  // 将逗号分隔的URL字符串转换为FileItem数组
+  const convertFileUrlToFileItems = (fileUrl: string): FileItem[] => {
+    if (!fileUrl || !fileUrl.trim()) {
+      return [];
+    }
+    
+    // 按逗号分割URL
+    const urls = fileUrl.split(',').map(url => url.trim()).filter(url => url);
+    
+    return urls.map((url, index) => {
+      // 从URL中提取文件名
+      const urlParts = url.split('/');
+      const fileName = urlParts[urlParts.length - 1] || `附件${index + 1}`;
+      
+      // 根据文件扩展名判断文件类型
+      const extension = fileName.split('.').pop()?.toLowerCase() || '';
+      let fileType = 'pdf';
+      if (extension === 'doc' || extension === 'docx') {
+        fileType = 'word';
+      } else if (extension === 'xls' || extension === 'xlsx') {
+        fileType = 'excel';
+      } else if (extension === 'ppt' || extension === 'pptx') {
+        fileType = 'ppt';
+      }
+      
+      return {
+        fileId: Date.now() + index,
+        fileName,
+        fileType,
+        fileSize: '0',
+        fileUrl: url,
+      };
+    });
   };
 
   const handleValidate = async () => {
@@ -166,16 +191,7 @@
         ruleFormData.status = res.status ?? 1;
 
         // 如果有文件URL,转换为FileItem格式
-        if (res.fileUrl) {
-          uploadFileList.value = [
-            {
-              fileId: Date.now(),
-              fileName: getFileName(res.fileUrl),
-              fileType: res.fileFormat?.toLowerCase() === 'pdf' ? 'pdf' : 'word',
-              fileSize: '0KB',
-            },
-          ];
-        }
+        ruleFormData.fileUrlList = convertFileUrlToFileItems(res.fileUrl || '');
       }
       cloneRuleFormData();
     } catch (e) {
@@ -187,7 +203,45 @@
   const handleSubmit = async () => {
     const res = await handleValidate();
     if (!res) return;
+    
+    // 验证文件上传(必填)
+    if (!ruleFormData.fileUrlList || ruleFormData.fileUrlList.length === 0) {
+      ElMessage.warning('请上传文件');
+      return;
+    }
+    
     try {
+      // 处理文件上传:先上传文件获取 URL,然后提取 fileUrl
+      let fileUrl = '';
+      if (ruleFormData.fileUrlList && ruleFormData.fileUrlList.length > 0) {
+        // 分离已有URL的文件和新上传的文件
+        const existingFiles: string[] = [];
+        const newFiles: FileItem[] = [];
+        
+        ruleFormData.fileUrlList.forEach((file: FileItem) => {
+          // 如果文件已经有 fileUrl 且没有 file 对象,说明是已有文件
+          if (file.fileUrl && !file.file) {
+            existingFiles.push(file.fileUrl);
+          } else {
+            // 否则是需要上传的新文件
+            newFiles.push(file);
+          }
+        });
+
+        // 上传新文件
+        let uploadedUrls: string[] = [];
+        if (newFiles.length > 0) {
+          const uploadedFiles = await formatAttachmentList(newFiles);
+          uploadedUrls = uploadedFiles
+            .map((file: any) => file.fileUrl || file.url || '')
+            .filter((url: string) => url);
+        }
+
+        // 合并已有URL和新上传的URL,取第一个作为fileUrl
+        const allUrls = [...existingFiles, ...uploadedUrls].filter((url: string) => url);
+        fileUrl = allUrls.length > 0 ? allUrls[0] : '';
+      }
+
       const basePayload: ProductionSafetyFile = {
         fileName: ruleFormData.fileName,
         classifyName: ruleFormData.classifyName,
@@ -195,7 +249,7 @@
         fileVersion: ruleFormData.fileVersion,
         fileFormat: ruleFormData.fileFormat,
         releaseDate: ruleFormData.releaseDate,
-        fileUrl: ruleFormData.fileUrl || undefined,
+        fileUrl: fileUrl || undefined,
         content: ruleFormData.content || undefined,
         status: ruleFormData.status ?? 1,
       };

+ 1 - 0
src/views/production-safety/productionSafetySystem/safetyTraining/configs/form.ts

@@ -77,6 +77,7 @@ export const INDUSTRY_STANDARD_FORM_DATA = {
   fileFormat: '',
   releaseDate: '',
   fileUrl: '',
+  fileUrlList: [] as any[], // 文件列表(FileItem数组)
   content: '',
   status: 1, // 默认启用
 };

+ 1 - 1
src/views/production-safety/productionSafetySystem/safetyTraining/safetyTraining.vue

@@ -171,7 +171,7 @@
       const pageQuery: ProductionSafetyFilePageQuery = {
         pageNumber: pagination.pageNumber,
         pageSize: pagination.pageSize,
-        data: {
+        queryParam: {
           keyword: queryParams.keyword || undefined,
           status: queryParams.status,
           classifyName: queryParams.classifyName || undefined,

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

@@ -0,0 +1 @@
+<template> xxx </template>

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

@@ -0,0 +1 @@
+<template> xxx </template>

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

@@ -0,0 +1,249 @@
+<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-form">
+        <el-form :inline="true">
+          <el-form-item label="项目名称">
+            <el-input v-model="queryParams.queryParam.projectName" placeholder="搜索项目名称" style="width: 170px" />
+          </el-form-item>
+          <el-form-item label="状态">
+            <el-select v-model="queryParams.queryParam.status" clearable placeholder="状态" style="width: 170px">
+              <el-option value="" label="全部" />
+              <el-option :value="1" label="待提交" />
+              <el-option :value="2" label="待审批" />
+              <el-option :value="3" label="已完成" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="部门名称">
+            <el-cascader
+              v-model="queryParams.queryParam.departmentId"
+              style="width: 170px"
+              ref="cascaderRef"
+              :options="firstLevelDepts"
+              :props="cascaderProp"
+              :show-all-levels="false"
+              placeholder="部门名称"
+              filterable
+              @change="handleChangeDept"
+            />
+          </el-form-item>
+          <el-form-item label="施工地点">
+            <el-input
+              v-model="queryParams.queryParam.constructionLocation"
+              placeholder="输入施工地点"
+              style="width: 170px"
+            />
+          </el-form-item>
+        </el-form>
+
+        <div>
+          <el-button type="primary" @click="$router.push({ name: 'hazardManageAdd' })">添加 </el-button>
+          <el-button type="primary" @click="queryTableList">查询</el-button>
+          <el-button @click="handleRestParams">重置</el-button>
+        </div>
+      </div>
+
+      <div class="table-content">
+        <el-table :data="tableData.data">
+          <el-table-column type="index" label="序号" width="80" />
+          <el-table-column label="项目名称" prop="projectName" width="180" />
+          <el-table-column label="申请单号" prop="code" width="180" />
+          <el-table-column label="施工地点(区域)" prop="constructionLocation" width="180" />
+          <el-table-column label="工程施工内容简要描述 " prop="constructionContent" width="230" />
+          <el-table-column label="施工单位名称" prop="constructionUnit" width="180" />
+          <el-table-column label="施工项目负责人" prop="projectManagerName" width="180" />
+          <el-table-column label="施工现场安全负责人" prop="siteSafetyManagerName" width="240" />
+          <el-table-column label="当前流程节点" prop="nodeDescription" width="180" />
+          <el-table-column label="状态" props="statusName" width="100" />
+          <el-table-column fixed="right" min-width="240" label="操作">
+            <template #default="scope">
+              <el-button
+                type="primary"
+                link
+                @click="$router.push({ name: 'hazardManageEdit', query: { id: scope.row.id } })"
+                >编辑</el-button
+              >
+
+              <el-button type="primary" link @click="handleConfirmDeleteRow(scope)">删除</el-button>
+              <el-button
+                type="primary"
+                link
+                @click="$router.push({ name: 'hazardManageView', query: { id: scope.row.id } })"
+                >查看</el-button
+              >
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div class="pagination-container" v-if="tableData.total > 0">
+        <el-pagination
+          background
+          :current-page="queryParams.pageNumber"
+          :page-size="queryParams.pageSize"
+          :total="tableData.total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </main>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { useRouter } from 'vue-router';
+  import {
+    constructionSafetyQueryPageConstruction,
+    constructionSafetyDeleteConstructionById,
+  } from '@/api/production-safety/responsibility-implementation';
+  import { omit } from 'lodash-es';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+  import { unformatAttachment } from '@/components/UploadFiles/utils';
+  import { downloadFile } from '@/views/disaster/utils';
+  import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
+  import { getAllDepartments } from '@/api/auth/dept';
+
+  const router = useRouter();
+  const { id } = useUserInfoHook();
+  const firstLevelDepts = ref<any[]>([]);
+  const cascaderProp = {
+    expandTrigger: 'click',
+    checkStrictly: true,
+    // emitPath: false,
+    value: 'id',
+    label: 'deptName',
+  };
+  const queryParams = reactive<any>({
+    pageNumber: 1,
+    pageSize: 10,
+    queryParam: {
+      status: '',
+      projectName: '',
+      constructionLocation: '',
+      department: '',
+      departmentId: [],
+    },
+  });
+  const cascaderRef = ref();
+
+  const tableData = reactive({
+    data: [],
+    total: 0,
+  });
+
+  const handleSizeChange = (value) => {};
+
+  const handleCurrentChange = (value) => {
+    queryParams.pageNumber = value;
+    queryTableList();
+  };
+
+  const getDeptData = () => {
+    getAllDepartments().then((res) => {
+      firstLevelDepts.value = formatDeptTree(res);
+    });
+  };
+  const handleChangeDept = () => {
+    const deptInfo = cascaderRef.value?.getCheckedNodes();
+    if (deptInfo?.[0]) {
+      queryParams.queryParam.department = deptInfo[0].label;
+      queryParams.queryParam.departmentId = deptInfo[0].pathValues;
+    }
+  };
+
+  const handleConfirmDeleteRow = (scope) => {
+    constructionSafetyDeleteConstructionById(scope.row.id).then(() => {
+      ElMessage.success('删除成功!');
+      queryTableList();
+    });
+  };
+
+  const queryTableList = () => {
+    constructionSafetyQueryPageConstruction({
+      ...queryParams,
+      queryParam: {
+        ...omit(queryParams.queryParam, 'responsibleDepartmentId'),
+      },
+    }).then((res) => {
+      tableData.data = res.records;
+      tableData.total = res.totalRow;
+    });
+  };
+  const handleRestParams = () => {
+    Object.assign(queryParams, {
+      pageNumber: 1,
+      pageSize: 10,
+      queryParam: {
+        ...queryParams.queryParam,
+        status: '',
+        projectName: '',
+        constructionLocation: '',
+        department: '',
+        departmentId: [],
+      },
+    });
+    queryTableList();
+  };
+
+  onMounted(async () => {
+    await getDeptData();
+    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 *;
+
+  :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: center;
+    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;
+  }
+</style>

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

@@ -0,0 +1 @@
+<template>monitor</template>

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

@@ -0,0 +1 @@
+<template> xxx </template>

+ 55 - 39
src/views/production-safety/risk-identification-and-control/hazard-manage/add.vue

@@ -59,7 +59,8 @@
           />
         </el-form-item>
         <el-form-item label="风险管理部门" prop="riskManagementDept">
-          <el-cascader
+          <!-- <el-cascader
+            :ref="(el) => (cascaderRef['riskManagementDept'] = el)"
             v-model="formValue.riskManagementDeptId"
             size="large"
             style="width: 330px"
@@ -68,12 +69,14 @@
             :show-all-levels="false"
             placeholder="请选择"
             filterable
-            @change="handleDeptChange('riskManagementDept')"
-          />
+            @change="() => handleChangeDept('riskManagementDept')"
+          /> -->
+          <el-input size="large" placeholder="请选择" v-model="formValue.riskManagementDept" style="width: 330px" />
         </el-form-item>
         <el-form-item label="协作部门" prop="cooperationDept">
-          <el-cascader
+          <!-- <el-cascader
             v-model="formValue.cooperationDeptId"
+            :ref="(el) => (cascaderRef['cooperationDept'] = el)"
             size="large"
             style="width: 330px"
             :options="firstLevelDepts"
@@ -81,8 +84,9 @@
             :show-all-levels="false"
             placeholder="请选择"
             filterable
-            @change="handleDeptChange('cooperationDept')"
-          />
+            @change="handleChangeDept('cooperationDept')"
+          /> -->
+          <el-input size="large" placeholder="请选择" v-model="formValue.cooperationDept" style="width: 330px" />
         </el-form-item>
 
         <!-- 风险评估 -->
@@ -97,19 +101,22 @@
 
         <el-divider content-position="left">固有风险评估</el-divider>
         <el-form-item label="可能性(M)" prop="inherentRiskM">
-          <el-select v-model="formValue.inherentRiskM" size="large" placeholder="1-5" style="width: 330px">
+          <!-- <el-select v-model="formValue.inherentRiskM" size="large" placeholder="1-5" style="width: 330px">
             <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
-          </el-select>
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.inherentRiskM" :min="1" style="width: 330px" />
         </el-form-item>
         <el-form-item label="暴露频率(E)" prop="inherentRiskE">
-          <el-select v-model="formValue.inherentRiskE" size="large" placeholder="1-6" style="width: 330px">
+          <!-- <el-select v-model="formValue.inherentRiskE" size="large" placeholder="1-6" style="width: 330px">
             <el-option v-for="n in 6" :key="n" :label="n" :value="n" />
-          </el-select>
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.inherentRiskE" :min="1" style="width: 330px" />
         </el-form-item>
         <el-form-item label="严重度(S)" prop="inherentRiskS">
-          <el-select v-model="formValue.inherentRiskS" size="large" placeholder="1-5" style="width: 330px">
-            <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
-          </el-select>
+          <!-- <el-select v-model="formValue.inherentRiskS" size="large" placeholder="1-5" style="width: 330px">
+                  <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+                </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.inherentRiskS" :min="1" style="width: 330px" />
         </el-form-item>
         <el-form-item label="风险值(R)" prop="inherentRiskR">
           <el-input
@@ -122,10 +129,10 @@
         </el-form-item>
         <el-form-item label="风险等级" prop="inherentRiskLevel">
           <el-select v-model="formValue.inherentRiskLevel" size="large" placeholder="请选择" style="width: 330px">
-            <el-option label="A" value="A" />
+            <!-- <el-option label="A" value="A" /> -->
             <el-option label="B" value="B" />
             <el-option label="C" value="C" />
-            <el-option label="D" value="D" />
+            <!-- <el-option label="D" value="D" /> -->
           </el-select>
         </el-form-item>
         <el-form-item label="升级等级" prop="inherentRiskUpgrade">
@@ -145,8 +152,9 @@
           />
         </el-form-item>
         <el-form-item label="主责部门" prop="controlMainDept">
-          <el-cascader
+          <!-- <el-cascader
             v-model="formValue.controlMainDeptId"
+            :ref="(el) => (cascaderRef['controlMainDept'] = el)"
             size="large"
             style="width: 330px"
             :options="firstLevelDepts"
@@ -154,12 +162,14 @@
             :show-all-levels="false"
             placeholder="请选择"
             filterable
-            @change="handleDeptChange('controlMainDept')"
-          />
+            @change="handleChangeDept('controlMainDept')"
+          /> -->
+          <el-input size="large" placeholder="请选择" v-model="formValue.controlMainDept" style="width: 330px" />
         </el-form-item>
         <el-form-item label="协作部门" prop="controlCoopDept">
-          <el-cascader
+          <!-- <el-cascader
             v-model="formValue.controlCoopDeptId"
+            :ref="(el) => (cascaderRef['controlCoopDept'] = el)"
             size="large"
             style="width: 330px"
             :options="firstLevelDepts"
@@ -167,8 +177,9 @@
             :show-all-levels="false"
             placeholder="请选择"
             filterable
-            @change="handleDeptChange('controlCoopDept')"
-          />
+            @change="handleChangeDept('controlCoopDept')"
+          /> -->
+          <el-input size="large" placeholder="请选择" v-model="formValue.controlCoopDept" style="width: 330px" />
         </el-form-item>
         <el-form-item label="完成期限" prop="completionDeadline">
           <el-date-picker
@@ -201,19 +212,22 @@
         <!-- 剩余风险 -->
         <el-divider content-position="left">剩余风险评估</el-divider>
         <el-form-item label="可能性(M)" prop="controlRiskM">
-          <el-select v-model="formValue.controlRiskM" size="large" placeholder="1-5" style="width: 330px">
+          <!-- <el-select v-model="formValue.controlRiskM" size="large" placeholder="1-5" style="width: 330px">
             <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
-          </el-select>
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.controlRiskM" :min="1" style="width: 330px" />
         </el-form-item>
         <el-form-item label="暴露频率(E)" prop="controlRiskE">
-          <el-select v-model="formValue.controlRiskE" size="large" placeholder="1-6" style="width: 330px">
+          <!-- <el-select v-model="formValue.controlRiskE" size="large" placeholder="1-6" style="width: 330px">
             <el-option v-for="n in 6" :key="n" :label="n" :value="n" />
-          </el-select>
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.controlRiskE" :min="1" style="width: 330px" />
         </el-form-item>
         <el-form-item label="严重度(S)" prop="controlRiskS">
-          <el-select v-model="formValue.controlRiskS" size="large" placeholder="1-5" style="width: 330px">
+          <!-- <el-select v-model="formValue.controlRiskS" size="large" placeholder="1-5" style="width: 330px">
             <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
-          </el-select>
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.controlRiskS" :min="1" style="width: 330px" />
         </el-form-item>
         <el-form-item label="风险值(R)" prop="controlRiskR">
           <el-input
@@ -226,9 +240,9 @@
         </el-form-item>
         <el-form-item label="风险等级" prop="controlRiskLevel">
           <el-select v-model="formValue.controlRiskLevel" size="large" placeholder="请选择" style="width: 330px">
-            <el-option label="A" value="A" />
+            <!-- <el-option label="A" value="A" />
             <el-option label="B" value="B" />
-            <el-option label="C" value="C" />
+            <el-option label="C" value="C" /> -->
             <el-option label="D" value="D" />
           </el-select>
         </el-form-item>
@@ -239,14 +253,14 @@
             type="textarea"
             v-model="formValue.remarks"
             size="large"
-            :rows="3"
+            :rows="7"
             placeholder="例如:需每月提交检查报告"
           />
         </el-form-item>
       </el-form>
     </main>
     <footer class="safety-platform-container__footer">
-      <el-button @click="$router.push({ name: 'areaResponsibilities:public' })">返回</el-button>
+      <el-button @click="$router.push({ name: 'hazardManage' })">返回</el-button>
       <el-button type="primary" :loading="submiting" @click="handleSubmit">提交</el-button>
     </footer>
   </div>
@@ -261,6 +275,7 @@
   import {
     queryAvailableUserList,
     safetyRiskListSaveRiskList,
+    safetyHazardInventorySaveHazard,
   } from '@/api/production-safety/responsibility-implementation';
 
   const router = useRouter();
@@ -275,7 +290,7 @@
     value: 'id',
     label: 'deptName',
   };
-  const cascaderRef = ref<any>();
+  const cascaderRef = ref<any>({});
 
   const formValue = reactive({
     hazardCode: '',
@@ -334,13 +349,14 @@
   });
 
   const handleChangeDept = (prop) => {
-    const cascader = cascaderRef.value;
+    const cascader = cascaderRef.value?.[prop];
     const deptInfo = cascader?.getCheckedNodes();
-    formValue[prop] = deptInfo[0].label;
+    formValue[prop] = deptInfo[0]?.label;
     formRef.value.validateField(prop);
-    nextTick(() => {
-      handleQueryAvailableUserList(deptInfo[0].label, prop);
-    });
+
+    // nextTick(() => {
+    //   handleQueryAvailableUserList(deptInfo[0].label, prop);
+    // });
   };
 
   const getDeptData = () => {
@@ -384,12 +400,12 @@
     formRef.value?.validate((valid: boolean) => {
       if (valid) {
         submiting.value = true;
-        safetyRiskListSaveRiskList({
+        safetyHazardInventorySaveHazard({
           ...formValue,
         })
           .then(() => {
             ElMessage.success('创建成功!');
-            router.push({ name: 'riskManage' });
+            router.push({ name: 'hazardManage' });
           })
           .finally(() => {
             submiting.value = false;

+ 274 - 0
src/views/production-safety/risk-identification-and-control/hazard-manage/components/PlansAndProgramsDialog.vue

@@ -0,0 +1,274 @@
+<template>
+  <el-dialog
+    :model-value="props.modelValue"
+    @update:model-value="$emit('update:modelValue', $event)"
+    :title="props.dialogInfo.title"
+    width="600"
+    @close="clearData"
+  >
+    <el-form ref="formRef" label-width="auto" :model="formData" :rules="rules">
+      <el-form-item label="计划和名称" prop="planName">
+        <el-input :disabled="isView" v-model="formData.planName" size="large" />
+      </el-form-item>
+      <el-form-item label="执行部门" prop="execDepartmentId">
+        <el-cascader
+          :disabled="isView"
+          style="width: 100%"
+          v-model="formData.execDepartmentId"
+          size="large"
+          ref="cascaderRef"
+          :options="firstLevelDepts"
+          :props="cascaderProp"
+          :show-all-levels="false"
+          placeholder="请选择责任部门"
+          filterable
+          @change="handleChangeDept('execDepartmentId')"
+        />
+      </el-form-item>
+
+      <el-form-item label="执行人" prop="executor">
+        <el-select
+          :disabled="isView"
+          multiple
+          v-model="formData.executor"
+          placeholder="请选择"
+          size="large"
+          style="width: 100%"
+          filterable
+        >
+          <el-option v-for="item in userOptions" :key="item.value" :label="item.label" :value="item.value" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="计划内容" prop="planContent">
+        <el-input :disabled="isView" v-model="formData.planContent" type="textarea" :rows="6" size="large" />
+      </el-form-item>
+      <el-form-item prop="planStartDate" label="计划开始日期">
+        <el-date-picker
+          :disabled="isView"
+          v-model="formData.planStartDate"
+          size="large"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          style="width: 100%"
+          placeholder="计划开始日期"
+        />
+      </el-form-item>
+      <el-form-item prop="planEndDate" label="计划结束日期">
+        <el-date-picker
+          :disabled="isView"
+          v-model="formData.planEndDate"
+          style="width: 100%"
+          size="large"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          placeholder="计划结束日期"
+        />
+      </el-form-item>
+      <el-form-item prop="teamName" label="是否推送">
+        <el-radio-group :disabled="isView" v-model="formData.status">
+          <el-radio :value="1">待开始</el-radio>
+          <el-radio :value="2">进行中</el-radio>
+          <el-radio :value="3">已完成</el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div>
+        <el-button type="primary" @click="submitForm" :loading="submitLoading" :disabled="isView"> 保存 </el-button>
+        <el-button @click="handleCancel">取消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, watch, onMounted, computed } from 'vue';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { getAllDepartments } from '@/api/auth/dept';
+  import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
+  import dayjs from 'dayjs';
+  import {
+    queryAvailableUserList,
+    safetyHazardInventoryQueryPlanDetail,
+  } from '@/api/production-safety/responsibility-implementation';
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const props = defineProps<{
+    modelValue: boolean;
+    dialogInfo: { [key: string]: any };
+  }>();
+  const formRef = ref<FormInstance>();
+  const emit = defineEmits(['close', 'submit', 'update:modelValue']);
+  const submitLoading = ref(false);
+  const showDialog = ref(false);
+  const firstLevelDepts = ref<any[]>([]);
+  const cascaderProp = {
+    expandTrigger: 'click',
+    checkStrictly: true,
+    value: 'id',
+    label: 'deptName',
+  };
+  const cascaderRef = ref<any>();
+  const userOptions = ref<any[]>([]);
+  const isView = computed(() => props.dialogInfo.type === 'view');
+
+  const formData = reactive<any>({
+    planName: '',
+    execDepartment: '',
+    execDepartmentId: [],
+    planStartDate: null,
+    planEndDate: null,
+    status: 1,
+    executor: '',
+    planContent: '',
+  });
+  const rules = reactive<Record<string, any>>({
+    planName: [{ required: true, message: '请输入计划和名称' }],
+    planContent: [{ required: true, message: '请输入计划内容' }],
+    execDepartment: [{ required: true, message: '请输入执行部门' }],
+    planStartDate: [
+      { required: true, message: '请选择计划开始日期' },
+      {
+        validator: (rule, value) => {
+          return new Promise((resolve, reject) => {
+            if (value && formData.planEndDate) {
+              if (dayjs(value).isAfter(formData.planEndDate)) {
+                reject(new Error('开始日期不能大于结束日期'));
+                return;
+              }
+            }
+            resolve(true);
+          });
+        },
+      },
+    ],
+    planEndDate: [
+      { required: true, message: '请选择计划结束日期' },
+      {
+        validator: (rule, value) => {
+          return new Promise((resolve, reject) => {
+            if (value && formData.planStartDate) {
+              if (dayjs(value).isBefore(formData.planStartDate)) {
+                reject(new Error('结束日期不能小于开始日期'));
+                return;
+              }
+            }
+            resolve(true);
+          });
+        },
+      },
+    ],
+    executor: [{ required: true, message: '请输入执行人' }],
+  });
+
+  const handleQueryAvailableUserList = (deptName, realname = '') => {
+    return queryAvailableUserList({
+      pageNumber: 1,
+      pageSize: 200,
+      queryParam: {
+        deptName,
+        realname,
+      },
+    }).then((res: any) => {
+      userOptions.value = (res.records || []).map((u: any) => ({
+        value: u.userId || u.id,
+        label: u.realname,
+      }));
+      // switch (prop) {
+      //   case 'safetyResponsibleCenter':
+      //     formValue.safetyCenterManager = null;
+      //     safetyCenterManagerOptions.value = (res.records || []).map((u: any) => ({
+      //       value: u.userId || u.id,
+      //       label: u.realname,
+      //     }));
+      //     break;
+      //   case 'safetyResponsibleDepartment':
+      //     alert('');
+      //     formValue.safetyDepartmentManager = null;
+      //     formValue.safetySpecificPerson = null;
+      //     safetyDepartmentUserOptions.value = (res.records || []).map((u: any) => ({
+      //       value: u.userId || u.id,
+      //       label: u.realname,
+      //     }));
+      //     break;
+      //   default:
+      //     break;
+      // }
+    });
+  };
+
+  function dialogShow() {
+    showDialog.value = true;
+  }
+  function dialogHide() {
+    showDialog.value = false;
+  }
+
+  const getDeptData = () => {
+    return getAllDepartments().then((res) => {
+      firstLevelDepts.value = formatDeptTree(res);
+    });
+  };
+
+  const handleChangeDept = (prop) => {
+    const cascader = cascaderRef.value;
+    const deptInfo = cascader?.getCheckedNodes();
+    formData['execDepartmentId'] = deptInfo[0].pathValues;
+    formData['execDepartment'] = deptInfo[0].label;
+    formRef.value?.validateField('execDepartment');
+    // nextTick(() => {
+    //   handleQueryAvailableUserList(deptInfo[0].label, prop);
+    // });
+  };
+
+  const handleCancel = () => {
+    emit('update:modelValue', false);
+  };
+  function clearData() {
+    formRef.value?.resetFields();
+    Object.assign(formData, {
+      planName: '',
+      execDepartment: '',
+      planStartDate: null,
+      planEndDate: null,
+      status: 0,
+      execPerson: '',
+    });
+  }
+
+  function submitForm() {
+    formRef.value?.validate((valid) => {
+      if (!valid) {
+        return;
+      }
+      emit('submit', formData, submitLoading);
+    });
+  }
+
+  const handlesafetyHazardInventoryQueryPlanDetail = async (id) => {
+    const res = await safetyHazardInventoryQueryPlanDetail(id as string);
+    return res;
+  };
+
+  onMounted(async () => {
+    await Promise.all([getDeptData(), handleQueryAvailableUserList('')]);
+    if (props.dialogInfo.type !== 'add') {
+      handlesafetyHazardInventoryQueryPlanDetail(props.dialogInfo.currentRow.id).then((res) => {
+        Object.assign(formData, {
+          ...res,
+          execDepartmentId: res.execDepartmentId ? res.execDepartmentId.split(',').map((item) => Number(item)) : [],
+          executor: res.executor ? res.executor.split(',').map((item) => Number(item)) : [],
+        });
+      });
+    }
+  });
+
+  defineExpose({
+    submitLoading,
+    dialogShow,
+    dialogHide,
+  });
+</script>
+
+<style scoped lang="scss"></style>

+ 328 - 1
src/views/production-safety/risk-identification-and-control/hazard-manage/create-plan.vue

@@ -1 +1,328 @@
-<template>list</template>
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <div class="breadcrumb-title"> {{ detailData?.hazardName }} </div>
+      <div class="detail-content">
+        <span>编号:{{ detailData?.hazardCode }} </span>
+        <span>创建人:{{ detailData?.createdByName }} </span>
+        <span>创建时间:{{ detailData?.createdAt }} </span>
+      </div>
+      <el-tabs v-model="activeTab">
+        <el-tab-pane label="全部" name="" />
+        <el-tab-pane label="待开始" :name="1" />
+        <el-tab-pane label="进行中" :name="2" />
+        <el-tab-pane label="已完成" :name="3" />
+      </el-tabs>
+    </header>
+    <main class="safety-platform-container__main">
+      <div class="search-form">
+        <el-form :inline="true">
+          <el-form-item label="状态">
+            <el-select v-model="queryParams.queryParam.status" clearable placeholder="状态" style="width: 170px">
+              <el-option value="" label="全部" />
+              <el-option :value="1" label="待开始" />
+              <el-option :value="2" label="进行中" />
+              <el-option :value="3" label="已完成" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="部门名称">
+            <el-cascader
+              v-model="queryParams.queryParam.responsibleDepartmentId"
+              style="width: 170px"
+              ref="cascaderRef"
+              :options="firstLevelDepts"
+              :props="cascaderProp"
+              :show-all-levels="false"
+              placeholder="部门名称"
+              filterable
+              @change="handleChangeDept"
+            />
+          </el-form-item>
+          <el-form-item label="计划日期">
+            <el-date-picker
+              v-model="queryParams.queryParam.date"
+              clearable
+              type="daterange"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              style="width: 230px"
+              format="YYYY-MM-DD"
+              value-format="YYYY-MM-DD"
+            />
+          </el-form-item>
+        </el-form>
+
+        <div>
+          <el-button type="primary" @click="handleAddPlan">添加 </el-button>
+          <el-button type="primary" @click="queryTableList">查询</el-button>
+          <el-button @click="handleRestParams">重置</el-button>
+        </div>
+      </div>
+
+      <div class="table-content">
+        <el-table :data="tableData.data">
+          <el-table-column type="index" label="序号" width="80" />
+          <el-table-column label="计划/方案名称" prop="planName" width="180">
+            <template #default="scope">
+              <el-link type="primary" underline @click="handleShowPlan(scope)">
+                {{ scope.row.planName }}
+              </el-link>
+            </template>
+          </el-table-column>
+
+          <el-table-column label="状态" prop="statusName" width="100" />
+          <el-table-column label="执行部门" prop="executorName" width="180" />
+          <el-table-column label="执行人" prop="executor" width="130" />
+          <el-table-column label="计划/方案描述" prop="planContent" width="180" />
+          <el-table-column label="计划开始时间" prop="planStartDate" width="220" />
+          <el-table-column label="计划结束时间" prop="planEndDate" width="220" />
+
+          <el-table-column fixed="right" min-width="240" label="操作">
+            <template #default="scope">
+              <el-button type="primary" link @click="handleEditPlan(scope)"> 编辑 </el-button>
+
+              <el-button type="primary" link @click="handleConfirmDeleteRow(scope)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div class="pagination-container" v-if="tableData.total > 0">
+        <el-pagination
+          background
+          :current-page="queryParams.pageNumber"
+          :page-size="queryParams.pageSize"
+          :total="tableData.total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </main>
+  </div>
+  <PlansAndProgramsDialog
+    v-if="planDialogOpen"
+    v-model.visible="planDialogOpen"
+    :dialogInfo="dialogInfo"
+    @submit="handlePlanSubmit"
+  />
+</template>
+<script lang="ts" setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import { ElMessage } from 'element-plus';
+  import { useRouter, useRoute } from 'vue-router';
+  import {
+    safetyHazardInventoryQueryPlanAndSchemePage,
+    safetyHazardInventoryDeletePlan,
+    safetyHazardInventoryQueryPlanDetail,
+    safetyHazardInventorySavePlan,
+    safetyHazardInventoryUpdatePlan,
+  } from '@/api/production-safety/responsibility-implementation';
+  import { omit } from 'lodash-es';
+  import { useUserInfoHook } from '@/hooks/useUserInfoHook';
+  import { unformatAttachment } from '@/components/UploadFiles/utils';
+  import { downloadFile } from '@/views/disaster/utils';
+  import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
+  import { getAllDepartments } from '@/api/auth/dept';
+  import PlansAndProgramsDialog from './components/PlansAndProgramsDialog.vue';
+  import { exec } from 'child_process';
+
+  const router = useRouter();
+  const route = useRoute();
+  const { id } = useUserInfoHook();
+  const firstLevelDepts = ref<any[]>([]);
+  const cascaderProp = {
+    expandTrigger: 'click',
+    checkStrictly: true,
+    // emitPath: false,
+    value: 'id',
+    label: 'deptName',
+  };
+  const activeTab = ref('');
+  const planDialogOpen = ref(false);
+  const queryParams = reactive<any>({
+    pageNumber: 1,
+    pageSize: 10,
+    queryParam: {
+      status: '',
+      date: null,
+      execDepartment: '',
+    },
+  });
+  const cascaderRef = ref();
+  const detailData = reactive({
+    createdAt: '',
+    createdByName: '',
+    hazardName: '',
+    hazardCode: '',
+  });
+  const tableData = reactive({
+    data: [],
+    total: 0,
+  });
+
+  const dialogInfo = reactive<any>({
+    title: '新增风险源计划与方案',
+    currentRow: null,
+    type: 'add',
+  });
+
+  const handleAddPlan = () => {
+    planDialogOpen.value = true;
+    dialogInfo.title = '新增风险源计划与方案';
+    dialogInfo.type = 'add';
+  };
+
+  const handleEditPlan = async (scope: any) => {
+    dialogInfo.currentRow = scope.row;
+    dialogInfo.title = '编辑风险源计划与方案';
+    dialogInfo.type = 'edit';
+    planDialogOpen.value = true;
+  };
+
+  const handleShowPlan = async (scope: any) => {
+    dialogInfo.currentRow = scope.row;
+    dialogInfo.title = '查看风险源计划与方案';
+    dialogInfo.type = 'view';
+    planDialogOpen.value = true;
+  };
+
+  const handleClosePlanDialog = () => {
+    planDialogOpen.value = false;
+    dialogInfo.currentRow = null;
+    dialogInfo.type = '';
+    dialogInfo.title = '';
+  };
+
+  const handlePlanSubmit = (formData, loading) => {
+    const req = dialogInfo.type === 'edit' ? safetyHazardInventoryUpdatePlan : safetyHazardInventorySavePlan;
+    loading.value = true;
+    req({
+      ...formData,
+      id: dialogInfo.type === 'edit' ? dialogInfo.currentRow.id : undefined,
+      hazardListId: route.query.id,
+      execDepartmentId: formData.execDepartmentId.join(','),
+      executor: formData.executor.join(','),
+    })
+      .then(() => {
+        handleClosePlanDialog();
+        queryTableList();
+        ElMessage.success('操作成功!');
+      })
+      .finally(() => {
+        loading.value = false;
+      });
+  };
+
+  const handleSizeChange = (value) => {};
+  const handleCurrentChange = (value) => {
+    queryParams.pageNumber = value;
+    queryTableList();
+  };
+
+  const getDeptData = () => {
+    getAllDepartments().then((res) => {
+      firstLevelDepts.value = formatDeptTree(res);
+    });
+  };
+  const handleChangeDept = () => {
+    const deptInfo = cascaderRef.value?.getCheckedNodes();
+    if (deptInfo?.[0]) {
+      queryParams.queryParam.execDepartment = deptInfo[0].label;
+    }
+  };
+
+  const handleConfirmDeleteRow = (scope) => {
+    safetyHazardInventoryDeletePlan(scope.row.id).then(() => {
+      ElMessage.success('删除成功!');
+      queryTableList();
+    });
+  };
+
+  const queryTableList = () => {
+    safetyHazardInventoryQueryPlanAndSchemePage({
+      ...queryParams,
+      queryParam: {
+        ...omit(queryParams.queryParam, ['date', 'responsibleDepartmentId']),
+        hazardListId: route.query.id,
+
+        startTime: queryParams.queryParam.date ? queryParams.queryParam.date[0] : undefined,
+        endTime: queryParams.queryParam.date ? queryParams.queryParam.date[1] : undefined,
+      },
+    }).then((res) => {
+      tableData.data = res.page.records;
+      tableData.total = res.totalRow;
+      Object.assign(detailData, res);
+    });
+  };
+  const handleRestParams = () => {
+    Object.assign(queryParams, {
+      pageNumber: 1,
+      pageSize: 10,
+      queryParam: {
+        ...queryParams.queryParam,
+        status: '',
+        date: null,
+        execDepartment: '',
+      },
+    });
+    queryTableList();
+  };
+
+  onMounted(async () => {
+    await getDeptData();
+    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 *;
+
+  :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: center;
+    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;
+  }
+</style>

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

@@ -1 +1,471 @@
-<template>sdfsdf</template>
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <div class="breadcrumb-title">
+        <BreadcrumbBack />
+        编辑危险源清单
+      </div>
+    </header>
+    <main class="safety-platform-container__main">
+      <el-form ref="formRef" :inline="true" label-width="auto" :model="formValue" :rules="rules">
+        <!-- 基础信息 -->
+        <el-divider content-position="left">基础信息</el-divider>
+        <el-form-item label="危险源编号" prop="hazardCode">
+          <el-input
+            v-model="formValue.hazardCode"
+            size="large"
+            placeholder="例如:RD-202311-042"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="关键过程/作业单元" prop="keyProcessUnit">
+          <el-input
+            v-model="formValue.keyProcessUnit"
+            size="large"
+            placeholder="例如:张江快堆实验室(10号楼)"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="危险源名称" prop="hazardName">
+          <el-input
+            v-model="formValue.hazardName"
+            size="large"
+            placeholder="例如:高温熔融金属操作"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="是否重大危险源" prop="isMajorHazard">
+          <el-select v-model="formValue.isMajorHazard" size="large" placeholder="请选择" style="width: 330px">
+            <el-option :value="1" label="是" />
+            <el-option :value="0" label="否" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="危险源来源" prop="hazardSource">
+          <el-input
+            v-model="formValue.hazardSource"
+            size="large"
+            placeholder="例如:作业单元划分"
+            style="width: 330px"
+          />
+        </el-form-item>
+
+        <!-- 部门责任 -->
+        <el-form-item label="涉及部门" prop="involvedDepartment">
+          <el-input
+            v-model="formValue.involvedDepartment"
+            size="large"
+            placeholder="例如:核材料研究部/设备管理部(支持手动输入多部门)"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="风险管理部门" prop="riskManagementDept">
+          <!-- <el-cascader
+            :ref="(el) => (cascaderRef['riskManagementDept'] = el)"
+            v-model="formValue.riskManagementDeptId"
+            size="large"
+            style="width: 330px"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择"
+            filterable
+            @change="() => handleChangeDept('riskManagementDept')"
+          /> -->
+          <el-input size="large" placeholder="请选择" v-model="formValue.riskManagementDept" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="协作部门" prop="cooperationDept">
+          <!-- <el-cascader
+            v-model="formValue.cooperationDeptId"
+            :ref="(el) => (cascaderRef['cooperationDept'] = el)"
+            size="large"
+            style="width: 330px"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择"
+            filterable
+            @change="handleChangeDept('cooperationDept')"
+          /> -->
+          <el-input size="large" placeholder="请选择" v-model="formValue.cooperationDept" style="width: 330px" />
+        </el-form-item>
+
+        <!-- 风险评估 -->
+        <el-form-item label="可能后果" prop="possibleConsequences">
+          <el-input
+            v-model="formValue.possibleConsequences"
+            size="large"
+            placeholder="例如:灼伤、火灾"
+            style="width: 330px"
+          />
+        </el-form-item>
+
+        <el-divider content-position="left">固有风险评估</el-divider>
+        <el-form-item label="可能性(M)" prop="inherentRiskM">
+          <!-- <el-select v-model="formValue.inherentRiskM" size="large" placeholder="1-5" style="width: 330px">
+            <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.inherentRiskM" :min="1" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="暴露频率(E)" prop="inherentRiskE">
+          <!-- <el-select v-model="formValue.inherentRiskE" size="large" placeholder="1-6" style="width: 330px">
+            <el-option v-for="n in 6" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.inherentRiskE" :min="1" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="严重度(S)" prop="inherentRiskS">
+          <!-- <el-select v-model="formValue.inherentRiskS" size="large" placeholder="1-5" style="width: 330px">
+                  <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+                </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.inherentRiskS" :min="1" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="风险值(R)" prop="inherentRiskR">
+          <el-input
+            v-model="formValue.inherentRiskR"
+            size="large"
+            disabled
+            placeholder="自动计算: M×E×S"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="风险等级" prop="inherentRiskLevel">
+          <el-select v-model="formValue.inherentRiskLevel" size="large" placeholder="请选择" style="width: 330px">
+            <!-- <el-option label="A" value="A" /> -->
+            <el-option label="B" value="B" />
+            <el-option label="C" value="C" />
+            <!-- <el-option label="D" value="D" /> -->
+          </el-select>
+        </el-form-item>
+        <el-form-item label="升级等级" prop="inherentRiskUpgrade">
+          <el-input v-model="formValue.inherentRiskUpgrade" size="large" placeholder="例如:2级" style="width: 330px" />
+        </el-form-item>
+
+        <!-- 管控措施 -->
+        <el-divider content-position="left">管控措施</el-divider>
+        <el-form-item label="主要管控措施" prop="controlMeasures">
+          <el-input
+            type="textarea"
+            :rows="3"
+            v-model="formValue.controlMeasures"
+            size="large"
+            placeholder="每条措施用分号分隔,例如:1.穿戴防护服;2.设置隔离区;3.定期检查冷却系统"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="主责部门" prop="controlMainDept">
+          <!-- <el-cascader
+            v-model="formValue.controlMainDeptId"
+            :ref="(el) => (cascaderRef['controlMainDept'] = el)"
+            size="large"
+            style="width: 330px"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择"
+            filterable
+            @change="handleChangeDept('controlMainDept')"
+          /> -->
+          <el-input size="large" placeholder="请选择" v-model="formValue.controlMainDept" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="协作部门" prop="controlCoopDept">
+          <!-- <el-cascader
+            v-model="formValue.controlCoopDeptId"
+            :ref="(el) => (cascaderRef['controlCoopDept'] = el)"
+            size="large"
+            style="width: 330px"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择"
+            filterable
+            @change="handleChangeDept('controlCoopDept')"
+          /> -->
+          <el-input size="large" placeholder="请选择" v-model="formValue.controlCoopDept" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="完成期限" prop="completionDeadline">
+          <el-date-picker
+            v-model="formValue.completionDeadline"
+            type="date"
+            size="large"
+            placeholder="选择日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="完成状态" prop="completionStatus">
+          <el-input
+            v-model="formValue.completionStatus"
+            size="large"
+            placeholder="例如:措施1,2已完成"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="验证状态" prop="verificationStatus">
+          <el-input
+            v-model="formValue.verificationStatus"
+            size="large"
+            placeholder="例如:措施1已验证"
+            style="width: 330px"
+          />
+        </el-form-item>
+
+        <!-- 剩余风险 -->
+        <el-divider content-position="left">剩余风险评估</el-divider>
+        <el-form-item label="可能性(M)" prop="controlRiskM">
+          <!-- <el-select v-model="formValue.controlRiskM" size="large" placeholder="1-5" style="width: 330px">
+            <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.controlRiskM" :min="1" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="暴露频率(E)" prop="controlRiskE">
+          <!-- <el-select v-model="formValue.controlRiskE" size="large" placeholder="1-6" style="width: 330px">
+            <el-option v-for="n in 6" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.controlRiskE" :min="1" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="严重度(S)" prop="controlRiskS">
+          <!-- <el-select v-model="formValue.controlRiskS" size="large" placeholder="1-5" style="width: 330px">
+            <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number :step="1" :precision="0" v-model="formValue.controlRiskS" :min="1" style="width: 330px" />
+        </el-form-item>
+        <el-form-item label="风险值(R)" prop="controlRiskR">
+          <el-input
+            v-model="formValue.controlRiskR"
+            size="large"
+            disabled
+            placeholder="自动计算: M×E×S"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="风险等级" prop="controlRiskLevel">
+          <el-select v-model="formValue.controlRiskLevel" size="large" placeholder="请选择" style="width: 330px">
+            <!-- <el-option label="A" value="A" />
+            <el-option label="B" value="B" />
+            <el-option label="C" value="C" /> -->
+            <el-option label="D" value="D" />
+          </el-select>
+        </el-form-item>
+
+        <!-- 备注 -->
+        <el-form-item label="备注" prop="remarks" style="width: 87.2%">
+          <el-input
+            type="textarea"
+            v-model="formValue.remarks"
+            size="large"
+            :rows="7"
+            placeholder="例如:需每月提交检查报告"
+          />
+        </el-form-item>
+      </el-form>
+    </main>
+    <footer class="safety-platform-container__footer">
+      <el-button @click="$router.push({ name: 'hazardManage' })">返回</el-button>
+      <el-button type="primary" :loading="submiting" @click="handleSubmit">提交</el-button>
+    </footer>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ref, reactive, onMounted, nextTick } from 'vue';
+  import { useRouter, useRoute } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import { getAllDepartments } from '@/api/auth/dept';
+  import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
+
+  import {
+    queryAvailableUserList,
+    safetyHazardInventoryQueryDetail,
+    safetyHazardInventoryUpdateHazard,
+  } from '@/api/production-safety/responsibility-implementation';
+
+  const router = useRouter();
+  const route = useRoute();
+  const formRef = ref<any>(null);
+  const submiting = ref(false);
+
+  const userOptions = ref<any[]>([]);
+  const firstLevelDepts = ref<any[]>([]);
+  const cascaderProp = {
+    expandTrigger: 'click',
+    checkStrictly: true,
+    value: 'id',
+    label: 'deptName',
+  };
+  const cascaderRef = ref<any>({});
+
+  const formValue = reactive({
+    hazardCode: '',
+    keyProcessUnit: '',
+    hazardName: '',
+    isMajorHazard: undefined as number | undefined,
+    hazardSource: '',
+    involvedDepartment: '',
+    riskManagementDept: '',
+    // riskManagementDeptId: [] as number[],
+    cooperationDept: '',
+    // cooperationDeptId: [] as number[],
+    possibleConsequences: '',
+    inherentRiskM: undefined as number | undefined,
+    inherentRiskE: undefined as number | undefined,
+    inherentRiskS: undefined as number | undefined,
+    inherentRiskR: 0, // 自动计算
+    inherentRiskLevel: '',
+    inherentRiskUpgrade: '',
+    controlMeasures: '',
+    controlMainDept: '',
+    // controlMainDeptId: [] as number[],
+    controlCoopDept: '',
+    // controlCoopDeptId: [] as number[],
+    completionDeadline: '',
+    completionStatus: '',
+    verificationStatus: '',
+    controlRiskM: undefined as number | undefined,
+    controlRiskE: undefined as number | undefined,
+    controlRiskS: undefined as number | undefined,
+    controlRiskR: 0,
+    controlRiskLevel: '',
+    remarks: '',
+  });
+
+  const rules = reactive({
+    hazardCode: [{ required: true, message: '请输入危险源编号', trigger: 'blur' }],
+    keyProcessUnit: [{ required: true, message: '请输入关键工艺单元', trigger: 'blur' }],
+    hazardName: [{ required: true, message: '请输入危险源名称', trigger: 'blur' }],
+    isMajorHazard: [{ required: true, message: '请选择是否重大危险源', trigger: 'change' }],
+    hazardSource: [{ required: true, message: '请输入危险源来源', trigger: 'blur' }],
+    involvedDepartment: [{ required: true, message: '请输入涉及部门', trigger: 'blur' }],
+    riskManagementDept: [{ required: true, message: '请选择风险管理部门', trigger: 'change' }],
+    cooperationDept: [{ required: true, message: '请选择协作部门', trigger: 'change' }],
+    possibleConsequences: [{ required: true, message: '请输入可能后果', trigger: 'blur' }],
+    inherentRiskM: [{ required: true, message: '请选择可能性', trigger: 'change' }],
+    inherentRiskE: [{ required: true, message: '请选择暴露频率', trigger: 'change' }],
+    inherentRiskS: [{ required: true, message: '请选择严重度', trigger: 'change' }],
+    inherentRiskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
+    controlMeasures: [{ required: true, message: '请输入管控措施', trigger: 'blur' }],
+    controlMainDept: [{ required: true, message: '请选择主责部门', trigger: 'change' }],
+    controlRiskM: [{ required: true, message: '请选择剩余风险可能性', trigger: 'change' }],
+    controlRiskE: [{ required: true, message: '请选择剩余风险暴露频率', trigger: 'change' }],
+    controlRiskS: [{ required: true, message: '请选择剩余风险严重度', trigger: 'change' }],
+    controlRiskLevel: [{ required: true, message: '请选择剩余风险等级', trigger: 'change' }],
+  });
+
+  const handleChangeDept = (prop) => {
+    const cascader = cascaderRef.value?.[prop];
+    const deptInfo = cascader?.getCheckedNodes();
+    formValue[prop] = deptInfo[0]?.label;
+    formRef.value.validateField(prop);
+
+    // nextTick(() => {
+    //   handleQueryAvailableUserList(deptInfo[0].label, prop);
+    // });
+  };
+
+  const getDeptData = () => {
+    getAllDepartments().then((res) => {
+      firstLevelDepts.value = formatDeptTree(res);
+    });
+  };
+
+  const handleQueryAvailableUserList = (deptName, realname = '') => {
+    queryAvailableUserList({
+      pageNumber: 1,
+      pageSize: 200,
+      queryParam: {
+        deptName,
+        realname,
+      },
+    }).then((res: any) => {
+      userOptions.value = (res.records || []).map((u: any) => ({
+        value: u.userId || u.id,
+        label: u.realname,
+      }));
+    });
+  };
+  const loadDetailData = (id: number) => {
+    safetyHazardInventoryQueryDetail(id).then((res: any) => {
+      Object.keys(formValue).forEach((key) => {
+        if (res[key] !== undefined) {
+          formValue[key] = res[key];
+          // formValue['responsibleDepartmentId'] = res['responsibleDepartmentId']
+          //   ? res['responsibleDepartmentId'].split(',').map((item: string) => Number(item))
+          //   : [];
+        }
+      });
+    });
+  };
+  // const getUserData = () => {
+  //   getUserList({ pageNumber: 1, pageSize: 200, queryParam: {} }).then((res: any) => {
+  //     userOptions.value = (res.records || []).map((u: any) => ({
+  //       id: u.userId || u.id,
+  //       name: u.realName || u.username,
+  //     }));
+  //   });
+  // };
+
+  onMounted(() => {
+    getDeptData();
+    handleQueryAvailableUserList('');
+    loadDetailData(Number(route.query.id as unknown as number));
+    // getUserData();
+  });
+
+  const handleSubmit = () => {
+    formRef.value?.validate((valid: boolean) => {
+      if (valid) {
+        submiting.value = true;
+        safetyHazardInventoryUpdateHazard({
+          ...formValue,
+          id: route.query.id,
+        })
+          .then(() => {
+            ElMessage.success('创建成功!');
+            router.push({ name: 'hazardManage' });
+          })
+          .finally(() => {
+            submiting.value = false;
+          });
+      }
+    });
+  };
+</script>
+<style lang="scss" scoped>
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+  .editor-container {
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    margin-right: 20px;
+    overflow: hidden;
+
+    // :deep(.w-e-text-container) {
+    //   min-height: 400px;
+    //   overflow-y: auto;
+    // }
+  }
+  // :deep(.breadcrumb .title) {
+  //   margin-left: 0;
+  // }
+
+  // .main {
+  //   display: flex;
+  //   flex-direction: column;
+  //   padding: 20px;
+  //   flex: 1;
+  //   overflow: hidden;
+  //   background-color: #fff;
+  // }
+  // .button-content {
+  //   margin-bottom: 20px;
+  // }
+
+  // .page-content {
+  //   display: flex;
+  //   justify-content: flex-end;
+  // }
+  // // :deep(.el-form) {
+  // //   flex: 1;
+  // //   overflow: hidden;
+  // //   overflow-y: auto;
+  // // }
+</style>

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

@@ -67,15 +67,15 @@
       <div class="table-content">
         <el-table :data="tableData.data">
           <el-table-column type="index" label="序号" width="80" />
-          <el-table-column label="楼号/区域" prop="buildingArea" width="180" />
-          <el-table-column label="楼宇名称" prop="buildingName" width="180" />
-          <el-table-column label="楼层/位置" prop="floorLocation" width="180" />
-          <el-table-column label="房间号(名称)" prop="roomName" width="180" />
-          <el-table-column label="安全责任人" prop="roomSafetyResponsibleName" width="180" />
-          <el-table-column label="是否存在风险点" prop="hasRiskPointName" width="180" />
-          <el-table-column label="风险点类别" prop="riskCategoryName" width="180" />
-          <el-table-column label="变更原因" prop="changeReason" width="170" />
-          <el-table-column label="状态" prop="statusName" width="100" />
+          <el-table-column label="危险源编号" prop="hazardCode" width="180" />
+          <el-table-column label="关键过程/作业单元" prop="keyProcessUnit" width="180" />
+          <el-table-column label="危险源名称" prop="hazardName" width="180" />
+          <el-table-column label="是否重大危险" prop="isMajorHazard" width="180" />
+          <el-table-column label="危险源来源" prop="hazardSource" width="180" />
+          <el-table-column label="涉及型号" prop="involvedDepartment" width="180" />
+          <el-table-column label="危险源管理的主责单位/部门" prop="riskManagementDept" width="240" />
+          <el-table-column label="状态" prop="statusName" width="80" />
+          <el-table-column label="变更原因" prop="changeReason" width="120" />
           <el-table-column fixed="right" min-width="240" label="操作">
             <template #default="scope">
               <el-button
@@ -117,7 +117,6 @@
 </template>
 <script lang="ts" setup>
   import { onMounted, reactive, ref } from 'vue';
-  import dayjs from 'dayjs';
   import { ElMessage } from 'element-plus';
   import { useRouter } from 'vue-router';
   import {

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

@@ -1 +1,568 @@
-<template>sfsdf</template>
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <div class="breadcrumb-title">
+        <BreadcrumbBack />
+        查看危险源清单
+      </div>
+    </header>
+    <main class="safety-platform-container__main">
+      <el-form ref="formRef" :inline="true" label-width="auto" :model="formValue" :rules="rules">
+        <!-- 基础信息 -->
+        <el-divider content-position="left">基础信息</el-divider>
+        <el-form-item label="危险源编号" prop="hazardCode">
+          <el-input
+            disabled
+            v-model="formValue.hazardCode"
+            size="large"
+            placeholder="例如:RD-202311-042"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="关键过程/作业单元" prop="keyProcessUnit">
+          <el-input
+            disabled
+            v-model="formValue.keyProcessUnit"
+            size="large"
+            placeholder="例如:张江快堆实验室(10号楼)"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="危险源名称" prop="hazardName">
+          <el-input
+            disabled
+            v-model="formValue.hazardName"
+            size="large"
+            placeholder="例如:高温熔融金属操作"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="是否重大危险源" prop="isMajorHazard">
+          <el-select disabled v-model="formValue.isMajorHazard" size="large" placeholder="请选择" style="width: 330px">
+            <el-option :value="1" label="是" />
+            <el-option :value="0" label="否" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="危险源来源" prop="hazardSource">
+          <el-input
+            disabled
+            v-model="formValue.hazardSource"
+            size="large"
+            placeholder="例如:作业单元划分"
+            style="width: 330px"
+          />
+        </el-form-item>
+
+        <!-- 部门责任 -->
+        <el-form-item label="涉及部门" prop="involvedDepartment">
+          <el-input
+            disabled
+            v-model="formValue.involvedDepartment"
+            size="large"
+            placeholder="例如:核材料研究部/设备管理部(支持手动输入多部门)"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="风险管理部门" prop="riskManagementDept">
+          <!-- <el-cascader
+            disabled
+            :ref="(el) => (cascaderRef['riskManagementDept'] = el)"
+            v-model="formValue.riskManagementDeptId"
+            size="large"
+            style="width: 330px"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择"
+            filterable
+            @change="() => handleChangeDept('riskManagementDept')"
+          /> -->
+          <el-input
+            disabled
+            v-model="formValue.riskManagementDept"
+            size="large"
+            placeholder="请输入"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="协作部门" prop="cooperationDept">
+          <!-- <el-cascader
+            disabled
+            v-model="formValue.cooperationDeptId"
+            :ref="(el) => (cascaderRef['cooperationDept'] = el)"
+            size="large"
+            style="width: 330px"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择"
+            filterable
+            @change="handleChangeDept('cooperationDept')"
+          /> -->
+          <el-input
+            disabled
+            v-model="formValue.cooperationDept"
+            size="large"
+            placeholder="请输入"
+            style="width: 330px"
+          />
+        </el-form-item>
+
+        <!-- 风险评估 -->
+        <el-form-item label="可能后果" prop="possibleConsequences">
+          <el-input
+            disabled
+            v-model="formValue.possibleConsequences"
+            size="large"
+            placeholder="例如:灼伤、火灾"
+            style="width: 330px"
+          />
+        </el-form-item>
+
+        <el-divider content-position="left">固有风险评估</el-divider>
+        <el-form-item label="可能性(M)" prop="inherentRiskM">
+          <!-- <el-select v-model="formValue.inherentRiskM" size="large" placeholder="1-5" style="width: 330px">
+            <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number
+            disabled
+            :step="1"
+            :precision="0"
+            v-model="formValue.inherentRiskM"
+            :min="1"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="暴露频率(E)" prop="inherentRiskE">
+          <!-- <el-select v-model="formValue.inherentRiskE" size="large" placeholder="1-6" style="width: 330px">
+            <el-option v-for="n in 6" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number
+            disabled
+            :step="1"
+            :precision="0"
+            v-model="formValue.inherentRiskE"
+            :min="1"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="严重度(S)" prop="inherentRiskS">
+          <!-- <el-select v-model="formValue.inherentRiskS" size="large" placeholder="1-5" style="width: 330px">
+                  <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+                </el-select> -->
+          <el-input-number
+            disabled
+            :step="1"
+            :precision="0"
+            v-model="formValue.inherentRiskS"
+            :min="1"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="风险值(R)" prop="inherentRiskR">
+          <el-input
+            v-model="formValue.inherentRiskR"
+            size="large"
+            disabled
+            placeholder="自动计算: M×E×S"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="风险等级" prop="inherentRiskLevel">
+          <el-select
+            disabled
+            v-model="formValue.inherentRiskLevel"
+            size="large"
+            placeholder="请选择"
+            style="width: 330px"
+          >
+            <el-option label="B" value="B" />
+            <el-option label="C" value="C" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="升级等级" prop="inherentRiskUpgrade">
+          <el-input
+            disabled
+            v-model="formValue.inherentRiskUpgrade"
+            size="large"
+            placeholder="例如:2级"
+            style="width: 330px"
+          />
+        </el-form-item>
+
+        <!-- 管控措施 -->
+        <el-divider content-position="left">管控措施</el-divider>
+        <el-form-item label="主要管控措施" prop="controlMeasures">
+          <el-input
+            disabled
+            type="textarea"
+            :rows="3"
+            v-model="formValue.controlMeasures"
+            size="large"
+            placeholder="每条措施用分号分隔,例如:1.穿戴防护服;2.设置隔离区;3.定期检查冷却系统"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="主责部门" prop="controlMainDept">
+          <!-- <el-cascader
+            disabled
+            v-model="formValue.controlMainDeptId"
+            :ref="(el) => (cascaderRef['controlMainDept'] = el)"
+            size="large"
+            style="width: 330px"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择"
+            filterable
+            @change="handleChangeDept('controlMainDept')"
+          /> -->
+          <el-input
+            disabled
+            v-model="formValue.controlMainDept"
+            size="large"
+            placeholder="请输入"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="协作部门" prop="controlCoopDept">
+          <!-- <el-cascader
+            disabled
+            v-model="formValue.controlCoopDeptId"
+            :ref="(el) => (cascaderRef['controlCoopDept'] = el)"
+            size="large"
+            style="width: 330px"
+            :options="firstLevelDepts"
+            :props="cascaderProp"
+            :show-all-levels="false"
+            placeholder="请选择"
+            filterable
+            @change="handleChangeDept('controlCoopDept')"
+          /> -->
+          <el-input
+            disabled
+            v-model="formValue.controlCoopDept"
+            size="large"
+            placeholder="请输入"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="完成期限" prop="completionDeadline">
+          <el-date-picker
+            disabled
+            v-model="formValue.completionDeadline"
+            type="date"
+            size="large"
+            placeholder="选择日期"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="完成状态" prop="completionStatus">
+          <el-input
+            disabled
+            v-model="formValue.completionStatus"
+            size="large"
+            placeholder="例如:措施1,2已完成"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="验证状态" prop="verificationStatus">
+          <el-input
+            disabled
+            v-model="formValue.verificationStatus"
+            size="large"
+            placeholder="例如:措施1已验证"
+            style="width: 330px"
+          />
+        </el-form-item>
+
+        <!-- 剩余风险 -->
+        <el-divider content-position="left">剩余风险评估</el-divider>
+        <el-form-item label="可能性(M)" prop="controlRiskM">
+          <!-- <el-select v-model="formValue.controlRiskM" size="large" placeholder="1-5" style="width: 330px">
+            <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number
+            disabled
+            :step="1"
+            :precision="0"
+            v-model="formValue.controlRiskM"
+            :min="1"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="暴露频率(E)" prop="controlRiskE">
+          <!-- <el-select v-model="formValue.controlRiskE" size="large" placeholder="1-6" style="width: 330px">
+            <el-option v-for="n in 6" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number
+            disabled
+            :step="1"
+            :precision="0"
+            v-model="formValue.controlRiskE"
+            :min="1"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="严重度(S)" prop="controlRiskS">
+          <!-- <el-select v-model="formValue.controlRiskS" size="large" placeholder="1-5" style="width: 330px">
+            <el-option v-for="n in 5" :key="n" :label="n" :value="n" />
+          </el-select> -->
+          <el-input-number
+            disabled
+            :step="1"
+            :precision="0"
+            v-model="formValue.controlRiskS"
+            :min="1"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="风险值(R)" prop="controlRiskR">
+          <el-input
+            v-model="formValue.controlRiskR"
+            size="large"
+            disabled
+            placeholder="自动计算: M×E×S"
+            style="width: 330px"
+          />
+        </el-form-item>
+        <el-form-item label="风险等级" prop="controlRiskLevel">
+          <el-select
+            disabled
+            v-model="formValue.controlRiskLevel"
+            size="large"
+            placeholder="请选择"
+            style="width: 330px"
+          >
+            <!-- <el-option label="A" value="A" />
+            <el-option label="B" value="B" />
+            <el-option label="C" value="C" /> -->
+            <el-option label="D" value="D" />
+          </el-select>
+        </el-form-item>
+
+        <!-- 备注 -->
+        <el-form-item label="备注" prop="remarks" style="width: 87.2%">
+          <el-input
+            disabled
+            type="textarea"
+            v-model="formValue.remarks"
+            size="large"
+            :rows="7"
+            placeholder="例如:需每月提交检查报告"
+          />
+        </el-form-item>
+      </el-form>
+    </main>
+    <footer class="safety-platform-container__footer">
+      <el-button @click="$router.push({ name: 'hazardManage' })">返回</el-button>
+    </footer>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { ref, reactive, onMounted, nextTick } from 'vue';
+  import { useRouter, useRoute } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import { getAllDepartments } from '@/api/auth/dept';
+  import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
+
+  import {
+    queryAvailableUserList,
+    safetyHazardInventoryQueryDetail,
+    safetyHazardInventorySaveHazard,
+  } from '@/api/production-safety/responsibility-implementation';
+
+  const router = useRouter();
+  const route = useRoute();
+  const formRef = ref<any>(null);
+  const submiting = ref(false);
+
+  const userOptions = ref<any[]>([]);
+  const firstLevelDepts = ref<any[]>([]);
+  const cascaderProp = {
+    expandTrigger: 'click',
+    checkStrictly: true,
+    value: 'id',
+    label: 'deptName',
+  };
+  const cascaderRef = ref<any>({});
+
+  const formValue = reactive({
+    hazardCode: '',
+    keyProcessUnit: '',
+    hazardName: '',
+    isMajorHazard: undefined as number | undefined,
+    hazardSource: '',
+    involvedDepartment: '',
+    riskManagementDept: '',
+    riskManagementDeptId: [] as number[],
+    cooperationDept: '',
+    cooperationDeptId: [] as number[],
+    possibleConsequences: '',
+    inherentRiskM: undefined as number | undefined,
+    inherentRiskE: undefined as number | undefined,
+    inherentRiskS: undefined as number | undefined,
+    inherentRiskR: 0, // 自动计算
+    inherentRiskLevel: '',
+    inherentRiskUpgrade: '',
+    controlMeasures: '',
+    controlMainDept: '',
+    controlMainDeptId: [] as number[],
+    controlCoopDept: '',
+    controlCoopDeptId: [] as number[],
+    completionDeadline: '',
+    completionStatus: '',
+    verificationStatus: '',
+    controlRiskM: undefined as number | undefined,
+    controlRiskE: undefined as number | undefined,
+    controlRiskS: undefined as number | undefined,
+    controlRiskR: 0,
+    controlRiskLevel: '',
+    remarks: '',
+  });
+
+  const rules = reactive({
+    hazardCode: [{ required: true, message: '请输入危险源编号', trigger: 'blur' }],
+    keyProcessUnit: [{ required: true, message: '请输入关键工艺单元', trigger: 'blur' }],
+    hazardName: [{ required: true, message: '请输入危险源名称', trigger: 'blur' }],
+    isMajorHazard: [{ required: true, message: '请选择是否重大危险源', trigger: 'change' }],
+    hazardSource: [{ required: true, message: '请输入危险源来源', trigger: 'blur' }],
+    involvedDepartment: [{ required: true, message: '请输入涉及部门', trigger: 'blur' }],
+    riskManagementDept: [{ required: true, message: '请选择风险管理部门', trigger: 'change' }],
+    cooperationDept: [{ required: true, message: '请选择协作部门', trigger: 'change' }],
+    possibleConsequences: [{ required: true, message: '请输入可能后果', trigger: 'blur' }],
+    inherentRiskM: [{ required: true, message: '请选择可能性', trigger: 'change' }],
+    inherentRiskE: [{ required: true, message: '请选择暴露频率', trigger: 'change' }],
+    inherentRiskS: [{ required: true, message: '请选择严重度', trigger: 'change' }],
+    inherentRiskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
+    controlMeasures: [{ required: true, message: '请输入管控措施', trigger: 'blur' }],
+    controlMainDept: [{ required: true, message: '请选择主责部门', trigger: 'change' }],
+    controlRiskM: [{ required: true, message: '请选择剩余风险可能性', trigger: 'change' }],
+    controlRiskE: [{ required: true, message: '请选择剩余风险暴露频率', trigger: 'change' }],
+    controlRiskS: [{ required: true, message: '请选择剩余风险严重度', trigger: 'change' }],
+    controlRiskLevel: [{ required: true, message: '请选择剩余风险等级', trigger: 'change' }],
+  });
+
+  const handleChangeDept = (prop) => {
+    const cascader = cascaderRef.value?.[prop];
+    const deptInfo = cascader?.getCheckedNodes();
+    formValue[prop] = deptInfo[0]?.label;
+    formRef.value.validateField(prop);
+
+    // nextTick(() => {
+    //   handleQueryAvailableUserList(deptInfo[0].label, prop);
+    // });
+  };
+
+  const getDeptData = () => {
+    getAllDepartments().then((res) => {
+      firstLevelDepts.value = formatDeptTree(res);
+    });
+  };
+
+  const loadDetailData = (id: number) => {
+    safetyHazardInventoryQueryDetail(id).then((res: any) => {
+      Object.keys(formValue).forEach((key) => {
+        if (res[key] !== undefined) {
+          formValue[key] = res[key];
+          formValue['responsibleDepartmentId'] = res['responsibleDepartmentId']
+            ? res['responsibleDepartmentId'].split(',').map((item: string) => Number(item))
+            : [];
+        }
+      });
+    });
+  };
+
+  const handleQueryAvailableUserList = (deptName, realname = '') => {
+    queryAvailableUserList({
+      pageNumber: 1,
+      pageSize: 200,
+      queryParam: {
+        deptName,
+        realname,
+      },
+    }).then((res: any) => {
+      userOptions.value = (res.records || []).map((u: any) => ({
+        value: u.userId || u.id,
+        label: u.realname,
+      }));
+    });
+  };
+
+  // const getUserData = () => {
+  //   getUserList({ pageNumber: 1, pageSize: 200, queryParam: {} }).then((res: any) => {
+  //     userOptions.value = (res.records || []).map((u: any) => ({
+  //       id: u.userId || u.id,
+  //       name: u.realName || u.username,
+  //     }));
+  //   });
+  // };
+
+  onMounted(() => {
+    getDeptData();
+    handleQueryAvailableUserList('');
+    loadDetailData(Number(route.query.id));
+    // getUserData();
+  });
+
+  const handleSubmit = () => {
+    formRef.value?.validate((valid: boolean) => {
+      if (valid) {
+        submiting.value = true;
+        safetyHazardInventorySaveHazard({
+          ...formValue,
+        })
+          .then(() => {
+            ElMessage.success('创建成功!');
+            router.push({ name: 'hazardManage' });
+          })
+          .finally(() => {
+            submiting.value = false;
+          });
+      }
+    });
+  };
+</script>
+<style lang="scss" scoped>
+  @use '@/styles/page-main-layout.scss' as *;
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/basic-table-action.scss' as *;
+  .editor-container {
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    margin-right: 20px;
+    overflow: hidden;
+
+    // :deep(.w-e-text-container) {
+    //   min-height: 400px;
+    //   overflow-y: auto;
+    // }
+  }
+  // :deep(.breadcrumb .title) {
+  //   margin-left: 0;
+  // }
+
+  // .main {
+  //   display: flex;
+  //   flex-direction: column;
+  //   padding: 20px;
+  //   flex: 1;
+  //   overflow: hidden;
+  //   background-color: #fff;
+  // }
+  // .button-content {
+  //   margin-bottom: 20px;
+  // }
+
+  // .page-content {
+  //   display: flex;
+  //   justify-content: flex-end;
+  // }
+  // // :deep(.el-form) {
+  // //   flex: 1;
+  // //   overflow: hidden;
+  // //   overflow-y: auto;
+  // // }
+</style>

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

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

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

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

+ 198 - 0
src/views/production-safety/risk-identification-and-control/key-site-monitor-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-monitor-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-monitor-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-monitor-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-monitor-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-monitor-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-monitor-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-monitor-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);
+  });
+};

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

@@ -0,0 +1,48 @@
+<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-monitor-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-monitor-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");
+// };

+ 1 - 0
src/views/production-safety/safetyAssessment/evaluationSystem/components/EvaluationTarget.vue

@@ -477,6 +477,7 @@
               isAdvancedGroup: item.isAdvancedGroup || false, // 是否先进集体(需要接口返回,暂时使用 false)
               departmentSort: item.scoreRank || 0, // 部门排序(使用排名)
               departmentLeader: item.deptUserName || '-', // 部门负责人
+              baseScore: 100, // 基础分,默认为100
               totalScore, // 总分数
               addScore, // 加分项分数
               subtractScore, // 减分项分数

+ 6 - 0
src/views/production-safety/safetyAssessment/evaluationSystem/configs/targetTables.ts

@@ -88,6 +88,12 @@ export const EVALUATION_ADVANCED_GROUP_TABLE_COLUMNS: TableColumnProps[] = [
     align: 'left',
     minWidth: '140px',
   },
+  {
+    label: '基础分',
+    prop: 'baseScore',
+    align: 'center',
+    minWidth: '100px',
+  },
   {
     label: '总分数',
     prop: 'totalScore',

+ 0 - 1
src/views/production-safety/safetyAssessment/pointDeduction/configs/form.ts

@@ -27,7 +27,6 @@ export const POINT_DEDUCTION_FORM_CONFIG: FormConfig[] = [
       min: 1,
       precision: 0, // 不允许小数点,只能输入整数
       placeholder: '请输入扣分值',
-      disabled: true, // 禁用扣分值字段
     },
   },
   {

+ 1 - 1
src/views/production-safety/safetyAssessment/pointDeduction/configs/tables.ts

@@ -34,7 +34,7 @@ export const POINT_DEDUCTION_TABLE_COLUMNS: TableColumnProps[] = [
   },
   {
     label: '扣分原因',
-    prop: 'dedResonList',
+    prop: 'dedResonList', 
     slot: 'dedResonList',
     align: 'left',
     minWidth: '300px',

+ 218 - 11
src/views/production-safety/safetyAssessment/receiptRecord/components/ReceiptRecordDetail.vue

@@ -52,6 +52,22 @@
           />
         </el-select>
       </template>
+      <template #approvalTemplateId>
+        <el-select
+          v-model="ruleFormData.approvalTemplateId"
+          placeholder="请选择审批流程"
+          filterable
+          clearable
+          :disabled="isViewMode || isAuditMode"
+        >
+          <el-option
+            v-for="item in approvalList"
+            :key="item.id"
+            :label="item.templateName"
+            :value="item.id"
+          />
+        </el-select>
+      </template>
     </BasicForm>
   </main>
   <footer class="safety-platform-container__footer">
@@ -64,13 +80,72 @@
       {{ isCreateMode ? '提交' : '保存' }}
     </el-button>
   </footer>
+  <BasicDialog
+    v-if="!isViewMode && !isAuditMode"
+    ref="basicDialogRef"
+    title="提交审批"
+    @refresh="closeApprovalDialog"
+  >
+    <template #form>
+      <div class="form">
+        <el-form ref="approvalFormRef" :model="approvalForm">
+          <el-form-item label="审批描述:" label-position="top">
+            <el-input v-model="approvalForm.description" placeholder="请输入审批描述" type="textarea" />
+          </el-form-item>
+          <div class="form-item">
+            <span>审批流程:</span>
+            <template v-for="item in approvalNodeList" :key="item.id">
+              <el-form-item
+                :label="`第${item.approvalOrder}步:${item.nodeDescription}(${APPROVAL_TYPE_MAP[item.approvalType]})`"
+                label-position="top"
+                :prop="item.approverType !== APPROVER_TYPE.FIX ? `approvers.${item.id}` : ''"
+                :rules="{ required: true, message: '请选择审批人员', trigger: 'change' }"
+              >
+                <el-input
+                  v-if="item.approverType === APPROVER_TYPE.FIX"
+                  :model-value="item.approverInfoList.map((info) => info.approverName).join(',')"
+                  disabled
+                ></el-input>
+                <el-select
+                  v-else
+                  v-model="approvalForm.approvers[item.id]"
+                  placeholder="请选择审批人员"
+                  value-key="id"
+                  filterable
+                  remote
+                  collapse-tags
+                  collapse-tags-tooltip
+                  :max-collapse-tags="2"
+                  :remote-method="remoteMethod"
+                  :loading="loading"
+                  multiple
+                >
+                  <el-option
+                    v-for="option in userOptions"
+                    :key="option.id"
+                    :label="`${option.realname}(${option.username})${option.deptName}`"
+                    :value="option.id"
+                  />
+                </el-select>
+              </el-form-item>
+            </template>
+          </div>
+        </el-form>
+      </div>
+    </template>
+    <template #footer>
+      <el-button type="primary" @click="handleSubmitApproval">提交</el-button>
+      <el-button @click="basicDialogRef?.closeDialog()">取消</el-button>
+    </template>
+  </BasicDialog>
 </template>
 
 <script setup lang="ts">
-  import { computed, onMounted, ref } from 'vue';
+  import { computed, onMounted, ref, reactive } from 'vue';
   import { useRoute, useRouter } from 'vue-router';
   import { ElMessage } from 'element-plus';
   import BasicForm from '@/components/BasicForm.vue';
+  import BasicDialog from '@/components/BasicDialog.vue';
   import { useFormConfigHook } from '@/hooks/useFormConfigHook';
   import {
     RECEIPT_RECORD_FORM_CONFIG,
@@ -84,12 +159,19 @@
     querInventoryManageList,
     updateClaimItemsLogAgree,
     updateClaimItemsLogReject,
+    submitReceiptRecordApprovalProcess,
   } from '@/api/receiptRecord';
   import type { InventoryItem } from '@/api/inventory';
   import { getAllDepartments } from '@/api/auth/dept';
   import type { DeptTree } from '@/types/dept/type';
   import { queryAvailableUserList } from '@/api/production-safety/responsibility-implementation';
   import type { UserLisItem } from '@/api/system/user-operate';
+  import { getAllApproval } from '@/api/approval/approval';
+  import type { ApprovalInstanceType } from '@/views/system/approval/types';
+  import { useEmergencySuppliesHook } from '@/views/emergency/emergency-supplies/src/hook';
+  import type { ApprovalNodeInstanceType } from '@/views/system/approval/types';
+  import { getApprovalNodeInstanceList } from '@/api/approval/approval';
+  import { APPROVAL_TYPE_MAP, APPROVER_TYPE } from '@/views/emergency/emergency-plan/src/constant';
 
   const router = useRouter();
   const route = useRoute();
@@ -102,6 +184,9 @@
   const isViewMode = computed(() => operate.value === 'receiptRecord-view');
   const isAuditMode = computed(() => operate.value === 'receiptRecord-audit');
 
+  // 当前记录ID(用于提交审批)
+  const receiptRecordId = ref<number>();
+
   // 部门树(复用添加应急预案的制定部门数据源)
   const cascaderRef = ref();
   const deptTree = ref<DeptTree[]>([]);
@@ -125,7 +210,7 @@
     ruleFormData.department = nodes?.[0]?.label ?? '';
     // 部门变化时,重新获取该部门的用户列表
     if (ruleFormData.deptId) {
-      getRecipientUserList(ruleFormData.deptId);
+      getRecipientUserList(ruleFormData.department);
     } else {
       recipientUserList.value = [];
       ruleFormData.recipientUserId = null;
@@ -168,13 +253,55 @@
     ruleFormData.recipient = selectedUser?.realname ?? '';
   };
 
+  // 审批流程列表(复用应急预案的审批模板)
+  const approvalList = ref<ApprovalInstanceType[]>([]);
+  const getApprovalList = async () => {
+    try {
+      const res = await getAllApproval();
+      approvalList.value = Array.isArray(res) ? res : [];
+    } catch (e) {
+      console.error('获取审批流程列表失败:', e);
+      approvalList.value = [];
+    }
+  };
+
   const { ruleFormData, formRules, ruleFormConfig, cloneRuleFormData, beforeRouteLeave } =
     useFormConfigHook(RECEIPT_RECORD_FORM_CONFIG, RECEIPT_RECORD_FORM_DATA, RECEIPT_RECORD_FORM_RULES);
 
+  // 提交审批弹窗相关
+  const basicDialogRef = ref<InstanceType<typeof BasicDialog>>();
+  const approvalFormRef = ref();
+  const approvalForm = reactive({
+    description: '',
+    approvers: {} as Record<number, any[]>,
+  });
+
+  const approvalNodeList = ref<ApprovalNodeInstanceType[]>([]);
+
+  const { userOptions, loading, remoteMethod } = useEmergencySuppliesHook();
+
+  const getApprovalNode = async (id: number) => {
+    const res = await getApprovalNodeInstanceList(id);
+    approvalNodeList.value = res.approvalNodeInfoList || [];
+  };
+
+  const resetApprovalForm = () => {
+    approvalFormRef.value?.resetFields();
+    approvalForm.description = '';
+  };
+
+  const closeApprovalDialog = () => {
+    resetApprovalForm();
+    basicDialogRef.value?.closeDialog();
+  };
+
   // 物品库存列表
   const inventoryList = ref<InventoryItem[]>([]);
 
-  // 查看模式下,所有字段设为只读
+  // 当前记录状态(用于控制“审核不通过原因”字段显示)
+  const currentStatus = ref<number | undefined>();
+
+  // 查看 / 审核模式下,所有字段设为只读
   const viewFormConfig = ref(
     RECEIPT_RECORD_FORM_CONFIG.map((item) => ({
       ...item,
@@ -186,9 +313,32 @@
   );
 
   const computedFormConfig = computed(() => {
-    if (isViewMode.value || isAuditMode.value) {
-      return viewFormConfig.value;
+    // 新增时不展示“审核不通过原因”字段
+    if (isCreateMode.value) {
+      return ruleFormConfig.value.filter((item) => item.prop !== 'returnReason');
+    }
+
+    // 查看模式:仅在“审核不通过”时展示“审核不通过原因”字段
+    if (isViewMode.value) {
+      if (currentStatus.value === -1) {
+        return viewFormConfig.value;
+      }
+      return viewFormConfig.value.filter((item) => item.prop !== 'returnReason');
     }
+
+    // 编辑模式:仅在“审核不通过”时展示“审核不通过原因”字段
+    if (isEditMode.value) {
+      if (currentStatus.value === -1) {
+        return ruleFormConfig.value;
+      }
+      return ruleFormConfig.value.filter((item) => item.prop !== 'returnReason');
+    }
+
+    // 审核模式:不展示“审核不通过原因”字段
+    if (isAuditMode.value) {
+      return viewFormConfig.value.filter((item) => item.prop !== 'returnReason');
+    }
+
     return ruleFormConfig.value;
   });
 
@@ -223,6 +373,7 @@
     try {
       const res = await queryClaimItemsLogDetail(currentId.value);
       if (res) {
+        currentStatus.value = res.status;
         ruleFormData.itemId = res.pimId;
         ruleFormData.outboundDate = res.outStoreTime ? res.outStoreTime.split('T')[0] : '';
         ruleFormData.outboundQuantity = res.claimQty;
@@ -240,6 +391,10 @@
         } else {
           ruleFormData.recipientUserId = null;
         }
+        // 后端如返回审批模板ID,则回显(字段为 templateId)
+        ruleFormData.approvalTemplateId = res.templateId ?? null;
+        // 审核不通过原因(只读展示,后端字段为 rejectReson),仅在审核不通过时展示内容
+        ruleFormData.returnReason = res.status === -1 ? res.rejectReson ?? '' : '';
       }
       cloneRuleFormData();
     } catch (e) {
@@ -251,6 +406,10 @@
   const handleSubmit = async () => {
     const res = await handleValidate();
     if (!res) return;
+
+    // 与应急预案保持一致的“添加/修改”提示文案
+    const message = isCreateMode.value ? '添加' : '修改';
+
     try {
       const selectedInventory = inventoryList.value.find((item) => item.id === ruleFormData.itemId);
       if (!selectedInventory) {
@@ -277,26 +436,74 @@
         userName: selectedUser.realname,
         userId: selectedUser.id, // 领取人用户ID
         remark: '',
+        // 审批流程ID在与后端交互时字段名为 templateId
+        templateId: ruleFormData.approvalTemplateId ?? undefined,
       };
 
       if (isCreateMode.value) {
-        await saveClaimItemsLog(basePayload);
-        ElMessage.success('创建成功');
+        const createRes = await saveClaimItemsLog(basePayload);
+        // 兼容多种返回结构,优先使用数字ID
+        receiptRecordId.value =
+          typeof createRes === 'number'
+            ? createRes
+            : createRes && typeof createRes === 'object' && 'id' in createRes
+            ? (createRes as any).id
+            : undefined;
       } else if (isEditMode.value && currentId.value) {
         await updateClaimItemsLog({
           id: currentId.value,
           ...basePayload,
         });
-        ElMessage.success('保存成功');
+        receiptRecordId.value = currentId.value;
+      }
+      ElMessage.success(`${message}成功`);
+      if (ruleFormData.approvalTemplateId) {
+        await getApprovalNode(ruleFormData.approvalTemplateId);
+        basicDialogRef.value?.openDialog();
       }
-
-      router.back();
     } catch (e) {
       console.error('保存物品领取记录失败:', e);
       ElMessage.error('保存失败,请重试');
     }
   };
 
+  const handleSubmitApproval = () => {
+    approvalFormRef.value?.validate(async (valid: boolean) => {
+      if (!valid) return;
+      if (!receiptRecordId.value) {
+        ElMessage.error('缺少物品领取记录ID,无法提交审批');
+        return;
+      }
+
+      const approvalData = {
+        planId: receiptRecordId.value,
+        approvalDescription: approvalForm.description,
+        approvalInfoList: approvalNodeList.value.map((node) => {
+          let approverIdList: number[] = [];
+          if (node.approverType === APPROVER_TYPE.FIX) {
+            approverIdList = node.approverInfoList.map((info) => info.approverId);
+          } else if (approvalForm.approvers[node.id]) {
+            approverIdList = approvalForm.approvers[node.id];
+          }
+          return {
+            approvalOrder: node.approvalOrder,
+            approverIdList,
+          };
+        }),
+      };
+
+      try {
+        await submitReceiptRecordApprovalProcess(approvalData);
+        ElMessage.success('提交成功');
+        closeApprovalDialog();
+        router.back();
+      } catch (e) {
+        console.error('提交审批失败:', e);
+        ElMessage.error('提交审批失败,请重试');
+      }
+    });
+  };
+
   const handleAuditPass = async () => {
     if (!currentId.value) return;
     try {
@@ -324,7 +531,7 @@
   onMounted(async () => {
     cloneRuleFormData();
     beforeRouteLeave();
-    await Promise.all([getDeptTreeData(), getInventoryList()]);
+    await Promise.all([getDeptTreeData(), getInventoryList(), getApprovalList()]);
     if (isEditMode.value || isViewMode.value || isAuditMode.value) {
       await getDetail();
     }

+ 25 - 3
src/views/production-safety/safetyAssessment/receiptRecord/configs/form.ts

@@ -33,10 +33,10 @@ export const RECEIPT_RECORD_FORM_CONFIG: FormConfig[] = [
   },
   {
     prop: 'receiptNumber',
-    label: '单号:',
+    label: '主动信息报告单号:',
     component: 'ElInput',
     componentProps: {
-      placeholder: '请输入单号',
+      placeholder: '请输入主动信息报告单号',
     },
   },
   {
@@ -55,6 +55,25 @@ export const RECEIPT_RECORD_FORM_CONFIG: FormConfig[] = [
       placeholder: '请选择领取人',
     },
   },
+  {
+    prop: 'approvalTemplateId',
+    label: '审批流程:',
+    slot: 'approvalTemplateId', // 复用应急预案的审批流程下拉
+    componentProps: {
+      placeholder: '请选择审批流程',
+    },
+  },
+  {
+    prop: 'returnReason',
+    label: '审核不通过原因:',
+    component: 'ElInput',
+    componentProps: {
+      type: 'textarea',
+      rows: 2,
+      placeholder: '暂无审核不通过原因',
+      disabled: true,
+    },
+  },
 ];
 
 export const RECEIPT_RECORD_FORM_DATA = {
@@ -67,6 +86,8 @@ export const RECEIPT_RECORD_FORM_DATA = {
   recipientUserId: null as number | null, // 领取人用户ID(通过queryAvailableUserList接口获取)
   recipient: '', // 领取人姓名,提交给接口的 userName
   pimId: 0, // 物品库存ID(用于提交时使用,兼容旧数据)
+  approvalTemplateId: null as number | null, // 审批流程模板ID(与应急预案保持一致)
+  returnReason: '', // 审核不通过原因(只读展示)
 };
 
 export const RECEIPT_RECORD_FORM_RULES = {
@@ -76,7 +97,8 @@ export const RECEIPT_RECORD_FORM_RULES = {
     { required: true, message: '请输入取出数量', trigger: 'blur' },
     { type: 'number', min: 1, message: '取出数量不能小于1', trigger: 'blur' },
   ],
-  receiptNumber: [{ required: true, message: '请输入单号', trigger: 'blur' }],
+  receiptNumber: [{ required: true, message: '请输入主动信息报告单号', trigger: 'blur' }],
   department: [{ required: true, message: '请选择部门', trigger: 'change' }],
   recipient: [{ required: true, message: '请选择领取人', trigger: 'change' }],
+  approvalTemplateId: [{ required: true, message: '请选择审批流程', trigger: 'change' }],
 };

+ 16 - 0
src/views/production-safety/safetyAssessment/receiptRecord/configs/status.ts

@@ -10,4 +10,20 @@ export const RECEIPT_RECORD_STATUS_LABEL: Record<string, string> = {
   '0': '待审核',
   '1': '审核通过',
   '-1': '审核不通过',
+  '2': '已领取',
+};
+
+// 管理员审核页状态选项(含已领取)
+export const ADMIN_RECEIPT_RECORD_STATUS_OPTIONS = [
+  { label: '待审核', value: 0 },
+  { label: '审核通过', value: 1 },
+  { label: '审核不通过', value: -1 },
+  // { label: '已领取', value: 2 },
+];
+
+export const ADMIN_RECEIPT_RECORD_STATUS_LABEL: Record<string, string> = {
+  '0': '待审核',
+  '1': '审核通过',
+  '-1': '审核不通过',
+  // '2': '已领取',
 };

+ 59 - 0
src/views/production-safety/safetyAssessment/receiptRecord/configs/tables.ts

@@ -66,3 +66,62 @@ export const RECEIPT_RECORD_TABLE_COLUMNS: TableColumnProps[] = [
     align: 'left',
   },
 ];
+export const ADMIN_RECEIPT_RECORD_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '物品名称',
+    prop: 'itemName',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '出库日期',
+    prop: 'outboundDate',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '取出数量',
+    prop: 'outboundQuantity',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '结余数量',
+    prop: 'remainingQuantity',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '单号',
+    prop: 'receiptNumber',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '部门',
+    prop: 'department',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '领取人',
+    prop: 'recipient',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '状态',
+    prop: 'status',
+    slot: 'status',
+    align: 'center',
+    minWidth: '100px',
+  },
+  {
+    label: '操作',
+    prop: 'action',
+    slot: 'action',
+    fixed: 'right',
+    width: '180px',
+    align: 'left',
+  },
+];

+ 2 - 1
src/views/production-safety/safetyAssessment/receiptRecord/receiptRecord.vue

@@ -74,7 +74,7 @@
               <div class="action-container--div" style="justify-content: left">
                 <!-- 0 待审核:审核 -->
                 <template v-if="Number(scope.row.status) === 0">
-                  <ActionButton text="审核" @click="handleAudit(scope.row.id)" />
+                  <!-- <ActionButton text="审核" @click="handleAudit(scope.row.id)" /> -->
                 </template>
                 <!-- 1 审核通过:删除、查看 -->
                 <template v-else-if="Number(scope.row.status) === 1">
@@ -294,6 +294,7 @@
     });
   };
 
+
   onMounted(() => {
     getTableData();
   });

+ 347 - 0
src/views/production-safety/safetyAssessment/receiptRecord/receiptRecordAdministratorReview.vue

@@ -0,0 +1,347 @@
+<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 style="position: relative">
+            <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="act-search">
+            <section class="select-box">
+              <div class="select-box--item">
+                <span>物品名称:</span>
+                <el-input
+                  v-model="tableQuery.queryParam.stuffName"
+                  placeholder="搜索物品名称"
+                  class="act-search-input"
+                />
+              </div>
+              <div class="select-box--item">
+                <span>部门名称:</span>
+                <el-input
+                  v-model="tableQuery.queryParam.deptName"
+                  placeholder="搜索部门名称"
+                  class="act-search-input"
+                />
+              </div>
+              <div class="select-box--item">
+                <span>状态:</span>
+                <el-select
+                  v-model="tableQuery.queryParam.status"
+                  placeholder="请选择状态"
+                  clearable
+                >
+                  <el-option
+                    v-for="item in ADMIN_RECEIPT_RECORD_STATUS_OPTIONS"
+                    :key="String(item.value)"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </div>
+            </section>
+            <section class="search-btn">
+              <el-button type="primary" @click="handleSearch">查询</el-button>
+              <el-button @click="handleReset">重置</el-button>
+            </section>
+          </div>
+        </header>
+
+        <div class="batch-table">
+          <BasicTable
+            ref="basicTableRef"
+            :tableData="tableData"
+            :tableConfig="tableConfig"
+            @update:pageSize="handleSizeChange"
+            @update:pageNumber="handleCurrentChange"
+          >
+            <template #status="scope">
+              <span>{{ ADMIN_RECEIPT_RECORD_STATUS_LABEL[String(scope.row.status)] ?? '-' }}</span>
+            </template>
+            <template #action="scope">
+              <div class="action-container--div" style="justify-content: left">
+                <!-- 0 待审核:审核、显示 -->
+                <template v-if="Number(scope.row.status) === 0">
+                  <ActionButton
+                    text="审核"
+                    @click="handleAudit(scope.row.id, scope.row.approvalOrder)"
+                  />
+                  <ActionButton text="显示" @click="handleView(scope.row.id)" />
+                </template>
+                <!-- 1 审核通过、-1 审核不通过、2 已领取:显示 -->
+                <template v-else>
+                  <ActionButton text="显示" @click="handleView(scope.row.id)" />
+                </template>
+              </div>
+            </template>
+          </BasicTable>
+        </div>
+      </div>
+
+    </main>
+    <BatchImport
+      v-if="batchImportVisible"
+      :visible="batchImportVisible"
+      :import-api-url="importApiUrl"
+      :template-url="templateUrl"
+      template-name="下载模板"
+      :show-template="false"
+      @close="batchImportVisible = false"
+      @update="handleUpdate"
+    />
+  </div>
+  <BasicDialog ref="basicDialogRef" title="审核" @refresh="closeDialog">
+    <template #form>
+      <el-form ref="formRef" :model="formData" style="width: 100%">
+        <el-form-item
+          label="审核:"
+          prop="approvalStatus"
+          :rules="[{ required: true, message: '请选择审核状态' }]"
+        >
+          <el-radio-group v-model="formData.approvalStatus">
+            <el-radio :value="APPROVAL_STATUS.APPROVED">通过</el-radio>
+            <el-radio :value="APPROVAL_STATUS.REJECTED">退回</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item
+          v-if="formData.approvalStatus === APPROVAL_STATUS.REJECTED"
+          label="退回原因:"
+          prop="rejectReason"
+          :rules="[{ required: true, message: '请输入退回原因' }]"
+        >
+          <el-input
+            v-model="formData.rejectReason"
+            placeholder="请输入退回原因"
+            type="textarea"
+          />
+        </el-form-item>
+      </el-form>
+    </template>
+    <template #footer>
+      <el-button type="primary" @click="handleApprovalSubmit">提交</el-button>
+      <el-button @click="basicDialogRef?.closeDialog">返回</el-button>
+    </template>
+  </BasicDialog>
+</template>
+
+<script lang="ts" setup>
+  import { onMounted, reactive, ref } from 'vue';
+  import BasicTable from '@/components/BasicTable.vue';
+  import BasicDialog from '@/components/BasicDialog.vue';
+  import useTableConfig from '@/hooks/useTableConfigHook';
+  import ActionButton from '@/components/ActionButton.vue';
+  import { TABLE_OPTIONS, ADMIN_RECEIPT_RECORD_TABLE_COLUMNS } from './configs/tables';
+  import {
+    ADMIN_RECEIPT_RECORD_STATUS_OPTIONS,
+    ADMIN_RECEIPT_RECORD_STATUS_LABEL,
+  } from './configs/status';
+  import { useRouter } from 'vue-router';
+  import type { QueryPageRequest } from '@/types/basic-query';
+  import {
+    queryClaimItemsApprovalPage,
+    deleteClaimItemsLog,
+    exportClaimItemsLog,
+    submitReceiptRecordApproval,
+  } from '@/api/receiptRecord';
+  import { downloadByData } from '@/utils/file/download';
+  import BatchImport from '@/components/batch-import/BatchImport.vue';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
+  import { ElMessage, type ElForm } from 'element-plus';
+  import { APPROVAL_STATUS } from '@/views/emergency/emergency-plan/src/constant';
+
+  const router = useRouter();
+
+  // 表格
+  const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+
+  const { tableConfig, pagination } = useTableConfig(ADMIN_RECEIPT_RECORD_TABLE_COLUMNS, TABLE_OPTIONS);
+
+  const tableData = ref<any[]>([]);
+
+  const basicDialogRef = ref<InstanceType<typeof BasicDialog>>();
+  const formRef = ref<InstanceType<typeof ElForm>>();
+  const formData = reactive({
+    approvalStatus: APPROVAL_STATUS.APPROVED,
+    rejectReason: '',
+  });
+  const currentId = ref<number>();
+  const currentOrder = ref<number>();
+
+  const tableQuery = reactive<QueryPageRequest<any>>({
+    pageNumber: pagination.pageNumber,
+    pageSize: pagination.pageSize,
+    queryParam: {
+      stuffName: '',
+      deptName: '',
+      status: undefined as number | undefined, // 0 待审核, 1 审核通过, -1 审核不通过, 2 已领取
+    },
+  });
+
+  const handleSizeChange = (value: number) => {
+    pagination.pageSize = value;
+    tableQuery.pageSize = value;
+    getTableData();
+  };
+
+  const handleCurrentChange = (value: number) => {
+    pagination.pageNumber = value;
+    tableQuery.pageNumber = value;
+    getTableData();
+  };
+
+  async function getTableData() {
+    tableConfig.loading = true;
+    try {
+      const res = await queryClaimItemsApprovalPage(tableQuery);
+      if (res) {
+        // 映射返回数据字段到表格字段
+        tableData.value = res.records.map((item) => ({
+          id: item.id,
+          itemName: item.stuffName, // 物品名称
+          outboundDate: item.outStoreTime, // 出库日期
+          outboundQuantity: item.claimQty, // 取出数量
+          remainingQuantity: item.surplusQty, // 结余数量
+          receiptNumber: item.orderNumber, // 单号
+          department: item.deptName, // 部门
+          recipient: item.userName, // 领取人
+          status: item.status, // 0 待审核 1 审核通过 -1 审核不通过 2 已领取
+          approvalOrder: item.approvalOrder, // 审批顺序
+        }));
+        pagination.total = res.totalRow;
+      }
+    } catch (e) {
+      console.error('获取物品领取记录列表失败:', e);
+      tableData.value = [];
+      pagination.total = 0;
+    } finally {
+      tableConfig.loading = false;
+    }
+  }
+
+  const handleSearch = () => {
+    pagination.pageNumber = 1;
+    tableQuery.pageNumber = 1;
+    getTableData();
+  };
+
+  const handleReset = () => {
+    tableQuery.queryParam.stuffName = '';
+    tableQuery.queryParam.deptName = '';
+    tableQuery.queryParam.status = undefined;
+    handleSearch();
+  };
+
+  // 批量导入
+  const batchImportVisible = ref(false);
+  const { urlPrefix } = useGlobSetting();
+  const importApiUrl = ref(urlJoin(urlPrefix, '/claimItemsLog/importClaimItemsLog'));
+  const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-receipt-record-template.xlsx');
+
+  const handleImport = () => {
+    batchImportVisible.value = true;
+  };
+
+  const handleUpdate = () => {
+    batchImportVisible.value = false;
+    getTableData();
+  };
+
+  const handleDownload = async () => {
+    try {
+      const exportParams = {
+        stuffName: tableQuery.queryParam.stuffName || undefined,
+        deptName: tableQuery.queryParam.deptName || undefined,
+        status: tableQuery.queryParam.status,
+      };
+      const response = await exportClaimItemsLog(exportParams);
+      if (response) {
+        const fileName = `物品领取记录_${new Date().toISOString().split('T')[0]}.xlsx`;
+        downloadByData(response, fileName);
+        ElMessage.success('导出成功');
+      }
+    } catch (e) {
+      console.error('导出物品领取记录失败:', e);
+      ElMessage.error('导出失败,请重试');
+    }
+  };
+
+  const handleEdit = (id: number) => {
+    router.push({
+      name: 'ReceiptRecordItem',
+      query: {
+        id,
+        operate: 'receiptRecord-edit',
+      },
+    });
+  };
+
+  const handleDelete = async (id: number) => {
+    try {
+      await deleteClaimItemsLog(id);
+      ElMessage.success('删除成功');
+      getTableData();
+    } catch (e) {
+      console.error('删除物品领取记录失败:', e);
+      ElMessage.error('删除失败,请重试');
+    }
+  };
+
+  const handleView = (id: number) => {
+    router.push({
+      name: 'ReceiptRecordItem',
+      query: {
+        id,
+        operate: 'receiptRecord-view',
+      },
+    });
+  };
+
+  const handleAudit = (id: number, approvalOrder: number) => {
+    currentId.value = id;
+    currentOrder.value = approvalOrder;
+    basicDialogRef.value?.openDialog();
+  };
+
+  const closeDialog = () => {
+    formRef.value?.resetFields();
+    basicDialogRef.value?.closeDialog();
+  };
+
+  const handleApprovalSubmit = () => {
+    formRef.value?.validate(async (valid) => {
+      if (valid) {
+        if (!currentId.value || !currentOrder.value) return;
+        await submitReceiptRecordApproval({
+          planId: currentId.value,
+          approvalOrder: currentOrder.value,
+          approvalStatus: formData.approvalStatus,
+          returnReason: formData.rejectReason,
+        });
+        ElMessage.success('审核成功');
+        basicDialogRef.value?.closeDialog();
+        getTableData();
+      }
+    });
+  };
+
+  onMounted(() => {
+    getTableData();
+  });
+</script>
+
+<style scoped lang="scss">
+  @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 *;
+</style>