ソースを参照

Merge branch 'feat/production-safety' of http://192.168.6.110/product-group-fe/sfy-safety-group/sfy-safety into feat/production-safety

sunqijun 2 ヶ月 前
コミット
6d8e84e17b
18 ファイル変更2206 行追加125 行削除
  1. 2 3
      src/api/drawLessons/index.ts
  2. 37 77
      src/api/safety-culture/index.ts
  3. 42 0
      src/router/routers/production-safety-router/safetyCulture.ts
  4. 23 23
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/components/OneByOneNotifyTarget.vue
  5. 57 7
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/configs/tables.ts
  6. 19 6
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/oneByOneManagement.vue
  7. 6 6
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagementDept/oneByOneManagementDept.vue
  8. 2 0
      src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagementDept/oneByOneManagementDeptItem.vue
  9. 2 3
      src/views/production-safety/safety-culture/safetyCultureActivityManagement/configs/tables.ts
  10. 478 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/components/activityRegistrationManagement.vue
  11. 401 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/components/safetyCultureActivityManagementDetail.vue
  12. 71 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/configs/activityTables.ts
  13. 181 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/configs/activityTargetTables.ts
  14. 84 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/configs/form.ts
  15. 93 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/configs/tables.ts
  16. 636 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutor.vue
  17. 26 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutorActivityRegistration.vue
  18. 46 0
      src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutorItem.vue

+ 2 - 3
src/api/drawLessons/index.ts

@@ -155,9 +155,8 @@ export function updateDrawLessons(data: UpdateDrawLessonsRequest) {
  */
 export function deleteDrawLessons(id: number) {
   return http.request({
-    url: '/drawLessons/admin/delete',
+    url: '/drawLessons/admin/delete?id=' + id,
     method: 'delete',
-    params: { id },
   });
 }
 
@@ -360,7 +359,7 @@ export function queryDrawLessonsAdminDetailPage(query: QueryPageRequest<DrawLess
 export function cancellationMessagePost(id: number) {
   return http.request({
     url: `/drawLessons/admin/invalid?id=${id}`,
-    method: 'put',
+    method: 'post',
   });
 }
 

+ 37 - 77
src/api/safety-culture/index.ts

@@ -81,6 +81,7 @@ export interface newAccidentCases {
   responsibleDeptId: number;
   responsiblePersonId: number;
   cooperateDeptIds: string;
+  attachmentUrl: string[] | string; // 文档上传(多个文件路径)
 }
 
 /**
@@ -127,9 +128,6 @@ export function accidentCaseManagementFilePage(query: safetyCultureFilePageQuery
     url: '/accidentCase/queryPage',
     method: 'post',
     data: query,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
     params: classifyName ? { classifyName } : undefined,
   });
 }
@@ -141,9 +139,6 @@ export function queryAccidentCaseDetail(id: number) {
   return http.request<safetyCultureFile>({
     url: `/accidentCase/queryDetail?id=${id}`,
     method: 'get',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -155,9 +150,6 @@ export function saveAccidentCase(data: newAccidentCases) {
     url: '/accidentCase/save',
     method: 'post',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -169,9 +161,6 @@ export function updateAccidentCase(data: newAccidentCases) {
     url: '/accidentCase/update',
     method: 'put',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -182,9 +171,6 @@ export function deleteAccidentCase(id: number) {
   return http.request({
     url: `/accidentCase/delete?id=${id}`,
     method: 'delete',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -197,9 +183,6 @@ export function querySafetyCultureMaterialsPage(query: safetyCultureFilePageQuer
     url: '/safety-culture/query-page',
     method: 'post',
     data: query,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
     params: classifyName ? { classifyName } : undefined,
   });
 }
@@ -211,9 +194,6 @@ export function deleteSafetyCultureMaterials(id: number) {
   return http.request({
     url: `/safety-culture/delete?id=${id}`,
     method: 'delete',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -223,9 +203,6 @@ export function deleteSafetyCultureMaterials(id: number) {
 export function queryAcademyFileById(id: number) {
   return http.request<safetyCultureFile>({
     url: `/safety-culture/detail?id=${id}`,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
     method: 'get',
   });
 }
@@ -237,9 +214,6 @@ export function saveAcademyFile(data: safetyCultureFile) {
   return http.request({
     url: '/safety-culture/save',
     method: 'post',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
     data,
   });
 }
@@ -251,9 +225,6 @@ export function updateAcademyFile(data: safetyCultureFile) {
   return http.request({
     url: '/safety-culture/update',
     method: 'put',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
     data,
   });
 }
@@ -266,9 +237,17 @@ export function safetyCultureActivityManagementFilePage(data: safetyCultureFileP
     url: '/safetyCulture/activity/queryPage',
     method: 'post',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
+  });
+}
+
+/**
+ * 安全文化活动执行人列表
+ */
+export function safetyCultureActivityManagementExecutorFilePage(data: safetyCultureFilePageQuery) {
+  return http.request({
+    url: '/safetyCulture/activity/queryPageIssue',
+    method: 'post',
+    data,
   });
 }
 
@@ -279,9 +258,6 @@ export function deleteSafetyCultureActivityManagement(id: number) {
   return http.request({
     url: `/safetyCulture/activity/delete?id=${id}`,
     method: 'delete',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -294,9 +270,6 @@ export function addSafetyCultureActivityManagement(data: addSafetyCultureFilePag
     url: '/safetyCulture/activity/save',
     method: 'post',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -308,9 +281,6 @@ export function updateSafetyCultureActivity(data: addSafetyCultureFilePageQuery)
     url: '/safetyCulture/activity/update',
     method: 'put',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -322,9 +292,6 @@ export function querySafetyCultureActivityDetail(id: number) {
   return http.request({
     url: `/safetyCulture/activity/queryDetail?id=${id}`,
     method: 'get',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -335,9 +302,17 @@ export function saveSafetyCultureActivityManagement(data) {
   return http.request({
     url: '/safetyCulture/registration/queryPage',
     method: 'post',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
+    data,
+  });
+}
+
+/**
+ * 查询执行人安全文化活动报名分页
+ */
+export function saveSafetyCultureActivityExecutorManagement(data) {
+  return http.request({
+    url: '/safetyCulture/registration/queryPage',
+    method: 'post',
     data,
   });
 }
@@ -345,9 +320,6 @@ export function saveSafetyCultureActivityManagement(data) {
 export function getAllDepartments(): Promise<DeptTree[]> {
   return http.request({
     url: '/admin/dept/queryAllDeptTree',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
     method: 'post',
   });
 }
@@ -361,9 +333,6 @@ export function addSafetyCultureActivityRegistration(data) {
     url: '/safetyCulture/registration/save',
     method: 'post',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -373,10 +342,7 @@ export function addSafetyCultureActivityRegistration(data) {
 export function deleteSafetyCultureActivityRegistration(id: number) {
   return http.request({
     url: `/safetyCulture/registration/delete?id=${id}`,
-    method: 'post',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
+    method: 'post'
   });
 }
 
@@ -388,9 +354,6 @@ export function updateSafetyCultureActivityManagement(data) {
     url: '/safetyCulture/registration/update',
     method: 'post',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -402,9 +365,6 @@ export function querySafetyPublicityBoardPage(data: safetyCultureFilePageQuery)
     url: '/safetypublicitybulletinboard/query-page',
     method: 'post',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -417,9 +377,6 @@ export function saveSafetyPublicityBoardPage(data) {
     url: '/safetypublicitybulletinboard/save',
     method: 'post',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -430,9 +387,6 @@ export function querySafetyPublicityBoardDetail(id: number) {
   return http.request<safetyCultureFile>({
     url: `/safetypublicitybulletinboard/detail?id=${id}`,
     method: 'get',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -444,9 +398,6 @@ export function updateSafetyPublicityBoardPage(data) {
     url: '/safetypublicitybulletinboard/update',
     method: 'put',
     data,
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -457,9 +408,6 @@ export function deleteSafetyPublicityBoardPage(id: number) {
   return http.request({
     url: `/safetypublicitybulletinboard/delete?id=${id}`,
     method: 'delete',
-    headers: {
-      'satoken': '7c1689ff62d6401483d1cb7235370b3c',
-    },
   });
 }
 
@@ -489,4 +437,16 @@ export function activityDistribution(data: IssueHiddenDangerRequest) {
     method: 'put',
     data,
   });
-}
+}
+
+
+/**
+ * 添加安全文化活动反馈
+ */
+export function feedbackSafetyCultureActivityManagement(data: addSafetyCultureFilePageQuery) {
+  return http.request({
+    url: '/safetyCulture/activity/feedback',
+    method: 'put',
+    data,
+  });
+}

+ 42 - 0
src/router/routers/production-safety-router/safetyCulture.ts

@@ -140,6 +140,48 @@ const safetyCultureRoutes: RouteComponent[] = [{
         noCache: false,
       },
     },
+    {
+      id: 900502,
+      parentId: 9005,
+      name: 'safetyCultureActivityManagementExecutor',
+      path: 'safety-culture-activity-management-executor',
+      component: '/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutor',
+      meta: {
+        title: '安全文化活动管理(执行人)',
+        icon: 'OverviewIcon',
+        isRoot: false,
+        hidden: false,
+        noCache: false,
+      },
+    },
+    {
+      id: 90050201,
+      parentId: 900502,
+      name: 'safetyCultureActivityManagementExecutorActivityRegistration',
+      path: 'safety-culture-activity-management-executor-activity-registration',
+      component: '/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutorActivityRegistration',
+      meta: {
+        title: '活动报名管理(执行人)',
+        icon: 'OverviewIcon',
+        isRoot: false,
+        hidden: true,
+        noCache: false,
+      },
+    },
+    {
+      id: 90050202,
+      parentId: 900502,
+      name: 'safetyCultureActivityManagementExecutorItem',
+      path: 'safety-culture-activity-management-executor-item',
+      component: '/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutorItem',
+      meta: {
+        title: '安全文化活动管理详情(执行人)',
+        icon: 'OverviewIcon',
+        isRoot: false,
+        hidden: true,
+        noCache: false,
+      },
+    },
   ],
 }];
 

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

@@ -133,7 +133,7 @@
     type DrawLessonsQueryParam 
   } from '@/api/drawLessons';
   import type { QueryPageRequest } from '@/types/basic-query';
-  import { DRAW_LESSONS_TABLE_COLUMNS } from '../configs/tables';
+  import { DRAW_LESSONS_TABLE_COLUMNS_TOW } from '../configs/tables';
   import { t } from '@wangeditor/editor';
   import { downloadByData } from '@/utils/file/download';
 
@@ -156,16 +156,16 @@
   const id = computed(() => Number(route.query.id));
   const tableData = ref<any[]>([]);
 
-  const { tableConfig, pagination } = useTableConfig(DRAW_LESSONS_TABLE_COLUMNS, TABLE_OPTIONS, true);
+  const { tableConfig, pagination } = useTableConfig(DRAW_LESSONS_TABLE_COLUMNS_TOW, TABLE_OPTIONS, true);
   const basicTableRef = ref<InstanceType<typeof BasicTable>>();
   const rawTableData = ref<any[]>([]);
-  const detailData = ref<{
-    problem?: string;
-    creatorName?: string;
-    createdAt?: string;
-  } | null>(null);
+  const detailData = ref({
+    problem: route.query.problem as string,
+    creatorName: route.query.creatorName as string,
+    createdAt: route.query.createdAt as string,
+  });
 
-  const activeTab = ref<string>('ALL');
+  const activeTab = ref<string>(route.query.statusId as string || 'ALL');
   const searchStatus = ref<string>('');
   const searchDeptName = ref<string>('');
   const searchDateRange = ref<string[] | null>(null);
@@ -231,7 +231,7 @@
           obligationDeptName: searchDeptName.value,
           planStartDate: searchDateRange.value?.[0],
           planEndDate: searchDateRange.value?.[1],
-          statusId: searchStatus.value ? Number(searchStatus.value) : undefined,
+          statusId: activeTab.value ? Number(activeTab.value) : undefined,
           problem: searchKeyword.value,
         },
       };
@@ -241,27 +241,27 @@
       if (id.value) {
         const current = list;
         rawTableData.value = current;
-        if (current.length) {
-          const row = current[0] as { problem?: string; creatorName?: string; createdAt?: string };
-          detailData.value = {
-            problem: row.problem,
-            creatorName: row.creatorName,
-            createdAt: row.createdAt,
-          };
-        } else {
-          detailData.value = null;
-        }
+        // if (current.length) {
+        //   const row = current[0] as { problem?: string; creatorName?: string; createdAt?: string };
+        //   detailData.value = {
+        //     problem: row.problem,
+        //     creatorName: row.creatorName,
+        //     createdAt: row.createdAt,
+        //   };
+        // } else {
+        //   detailData.value = null;
+        // }
       } else {
         rawTableData.value = list;
         const first = list[0] as { problem?: string; creatorName?: string; createdAt?: string } | undefined;
-        detailData.value = first
-          ? { problem: first.problem, creatorName: first.creatorName, createdAt: first.createdAt }
-          : null;
+        // detailData.value = first
+        //   ? { problem: first.problem, creatorName: first.creatorName, createdAt: first.createdAt }
+        //   : null;
       }
     } catch (e) {
       console.error('获取举一反三列表失败:', e);
       rawTableData.value = [];
-      detailData.value = null;
+      // detailData.value = null;
     } finally {
       tableConfig.loading = false;
     }

+ 57 - 7
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/configs/tables.ts

@@ -44,13 +44,13 @@ export const DRAW_LESSONS_TABLE_COLUMNS: TableColumnProps[] = [
     minWidth: '140px',
     showOverflowTooltip: true,
   },
-    {
-    label: '部门负责人',
-    prop: 'deptUserByName',
-    align: 'left',
-    minWidth: '140px',
-    showOverflowTooltip: true,
-  },
+  //   {
+  //   label: '部门负责人',
+  //   prop: 'deptUserByName',
+  //   align: 'left',
+  //   minWidth: '140px',
+  //   showOverflowTooltip: true,
+  // },
   {
     label: '举一反三要求',
     prop: 'associationOneThree',
@@ -94,3 +94,53 @@ export const DRAW_LESSONS_TABLE_COLUMNS: TableColumnProps[] = [
     align: 'left',
   },
 ];
+
+export const DRAW_LESSONS_TABLE_COLUMNS_TOW: TableColumnProps[] = [
+  {
+    label: '编号',
+    type: 'index',
+    align: 'center',
+    width: '80px',
+  },
+  {
+    label: '隐患问题',
+    prop: 'problem',
+    align: 'left',
+    minWidth: '200px',
+  },
+  {
+    label: '状态',
+    prop: 'statusName',
+    slot: 'status',
+    align: 'center',
+    minWidth: '100px',
+  },
+  {
+    label: '部门名称',
+    prop: 'associationOtObligationDeptName',
+    align: 'left',
+    minWidth: '140px',
+    showOverflowTooltip: true,
+  },
+  {
+    label: '举一反三要求',
+    prop: 'associationOneThree',
+    align: 'left',
+    minWidth: '140px',
+    showOverflowTooltip: true,
+  },
+  {
+    label: '计划完成时间',
+    prop: 'associationOtTimeLimit',
+    align: 'left',
+    minWidth: '160px',
+  },
+  {
+    label: '操作',
+    prop: 'action',
+    slot: 'action',
+    fixed: 'right',
+    width: '320px',
+    align: 'left',
+  },
+];

+ 19 - 6
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagement/oneByOneManagement.vue

@@ -106,7 +106,7 @@
                 <!-- 待审核 statusId=4 -->
                 <template v-else-if="scope.row.statusId === 4">
                   <ActionButton text="通知对象" @click="handleNotifyTarget(scope.row)" />
-                  <ActionButton text="审核" @click="handleAudit(scope.row.id)" />
+                  <ActionButton text="审核" @click="handleAudit(scope.row)" />
                   <ActionButton
                     text="作废"
                     :popconfirm="{ title: '确定要作废该记录?' }"
@@ -180,7 +180,7 @@
         <el-button type="primary" @click="handleIssueSubmit">保存</el-button>
       </template>
     </el-dialog>
-    <BatchImport
+    <!-- <BatchImport
       v-if="batchImportVisible"
       :visible="batchImportVisible"
       :import-api-url="importApiUrl"
@@ -189,7 +189,7 @@
       :show-template="false"
       @close="batchImportVisible = false"
       @update="handleUpdate"
-    />
+    /> -->
   </div>
 </template>
 
@@ -279,6 +279,8 @@
           feedbackCount: item.feedbackCount ?? 0,
           feedbackRatio: item.feedbackRatio,
           associationOtTimeLimit: item.associationOtTimeLimit,
+          creatorName: item.creatorName,
+          createdAt: item.createdAt,
         }));
         pagination.total = (res as any).totalRow ?? (res as any).total ?? 0;
       }
@@ -361,12 +363,23 @@
   };
 
   /** 审核:跳转审核详情页 */
-  const handleAudit = (id: number) => {
+  const handleAudit = (row: { id: number; statusId: number }) => {
+    // router.push({
+    //   name: 'oneByOneManagementItem',
+    //   query: {
+    //     id: String(id),
+    //     operate: 'one-by-one-audit-detail',
+    //   },
+    // });
     router.push({
       name: 'oneByOneManagementItem',
       query: {
-        id: String(id),
-        operate: 'one-by-one-audit-detail',
+        id: String(row.id),
+        statusId: row.statusId,
+        problem: row.problem,
+        creatorName: row.creatorName,
+        createdAt: row.createdAt,
+        operate: 'one-by-one-notify-target',
       },
     });
   };

+ 6 - 6
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagementDept/oneByOneManagementDept.vue

@@ -72,11 +72,11 @@
                 <ActionButton
                   v-if="scope.row.statusId === 3"
                   text="反馈"
-                  @click="handleFeedback(scope.row.associationOtId)"
+                  @click="handleFeedback(scope.row.id)"
                 />
                 <ActionButton
                   text="查看"
-                  @click="handleView(scope.row.associationOtId)"
+                  @click="handleView(scope.row.id)"
                 />
               </div>
             </template>
@@ -84,7 +84,7 @@
         </div>
       </div>
     </main>
-    <BatchImport
+    <!-- <BatchImport
       v-if="batchImportVisible"
       :visible="batchImportVisible"
       :import-api-url="importApiUrl"
@@ -93,7 +93,7 @@
       :show-template="false"
       @close="batchImportVisible = false"
       @update="handleUpdate"
-    />
+    /> -->
   </div>
 </template>
 
@@ -228,11 +228,11 @@
 
   /** 反馈:跳转反馈编辑页,操作仅反馈不根据状态显示 */
   const handleFeedback = (id: number) => {
-    router.push({
+    router.push({ 
       name: 'oneByOneManagementDeptItem',
       query: {
         id: String(id),
-        operate: 'one-by-one-dept-edit',
+        operate: 'one-by-one-dept-feedback',
       },
     });
   };

+ 2 - 0
src/views/production-safety/hiddenTroubleInvestigationAndGovernance/oneByOneManagementDept/oneByOneManagementDeptItem.vue

@@ -25,6 +25,8 @@
         return '编辑举一反三';
       case 'one-by-one-dept-view':
         return '查看举一反三';
+      case 'one-by-one-dept-feedback':
+        return '反馈举一反三';
       default:
         return '未知操作';
     }

+ 2 - 3
src/views/production-safety/safety-culture/safetyCultureActivityManagement/configs/tables.ts

@@ -16,8 +16,7 @@ export const INVENTORY_TABLE_COLUMNS: TableColumnProps[] = [
   },
   {
     label: '行动项内容',
-    prop: 'actionContent',
-    slot: 'actionContent',
+    prop: 'planName',
     align: 'left',
     minWidth: '150px',
     // showOverflowTooltip: true,
@@ -36,7 +35,7 @@ export const INVENTORY_TABLE_COLUMNS: TableColumnProps[] = [
   },
   {
     label: '安全文化活动计划名称',
-    prop: 'planName',
+    prop: 'actionContent',
     align: 'left',
     minWidth: '120px',
     // showOverflowTooltip: true,

+ 478 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/components/activityRegistrationManagement.vue

@@ -0,0 +1,478 @@
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <div class="evaluation-header">
+        <h1 class="evaluation-title">{{ activityRegistrationDetail.planName || '' }}</h1>
+        <div class="evaluation-meta">
+          <span>考核部门: {{ activityRegistrationDetail.responsibleDeptName || '-' }}</span>
+          <span>创建人: {{ activityRegistrationDetail.createdByName || '-' }}</span>
+          <span>创建时间: {{ formatDateTime(activityRegistrationDetail.startTime) || '-' }}</span>
+        </div>
+      </div>
+    </header>
+    <main class="safety-platform-container__main">
+      <div class="search-table-container">
+        <header>
+          <div class="act-search">
+            <section class="select-box">
+              <div class="select-box--item">
+                <span>员工工号/名称:</span>
+                <el-input v-model="tableQuery.queryParam.keyword" placeholder="搜索员工工号或名称" class="act-search-input" />
+              </div>
+              <div class="select-box--item">
+                <span>报名日期:</span>
+                <el-date-picker v-model="tableQuery.queryParam.dateRange" type="daterange" range-separator="至"
+                  start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" format="YYYY-MM-DD" />
+              </div>
+            </section>
+            <section class="search-btn">
+              <el-button type="primary" @click="handleSearch">查询</el-button>
+              <el-button @click="handleReset">重置</el-button>
+              <el-button type="primary" style="margin:0 10px;" @click="selectDeptType">
+                添加
+              </el-button>
+              <!-- <el-button plain @click="handleExport">
+                导出
+              </el-button> -->
+            </section>
+          </div>
+        </header>
+
+        <div class="batch-table">
+          <BasicTable ref="basicTableRef" :tableData="tableData" :tableConfig="tableConfig"
+            @update:pageSize="handleSizeChange" @update:pageNumber="handleCurrentChange">
+            <template #action="scope">
+              <div class="action-container--div" style="justify-content: left">
+                <ActionButton text="编辑" @click="handleEdit(scope.row)" />
+                <ActionButton text="删除" @click="handleDelete(scope.row)" />
+              </div>
+            </template>
+          </BasicTable>
+        </div>
+      </div>
+    </main>
+
+    <!-- 添加/编辑先进个人对话框 -->
+    <el-dialog v-model="addDialogVisible" :title="`${isEditMode && normalForm.id ? '编辑' : '添加'}活动报名信息`" width="800px"
+      :close-on-click-modal="false" @close="handleDialogClose">
+      <div class="add-dialog-content">
+
+        <!-- 普通部门表单 -->
+        <el-form ref="normalFormRef" :model="normalForm" :rules="normalFormRules" label-width="140px">
+          <!-- <el-form-item label="部门名称" prop="employeeDeptName">
+            <el-cascader style="width: 100%" size="large" :ref="(el) => (cascaderRef['employeeDeptName'] = el)"
+              :options="firstLevelDepts" :props="cascaderProp" :show-all-levels="false" placeholder="请选择安全责任部门"
+              filterable v-model="normalForm.deptId" @change="(val) => handleChangeDept(val, 'employeeDeptName')" />
+          </el-form-item> -->
+          <el-form-item label="员工工号:" prop="employeeCode">
+            <el-input v-model="normalForm.employeeCode" placeholder="请输入员工工号" maxlength="50" />
+          </el-form-item>
+          <el-form-item label="员工姓名:" prop="employeeName">
+            <el-input v-model="normalForm.employeeName" placeholder="请输入员工姓名" maxlength="50" />
+          </el-form-item>
+          <el-form-item label="员工联系方式:" prop="employeeContact">
+            <el-input v-model="normalForm.employeeContact" placeholder="请输入11位手机号码" maxlength="11"
+              @input="handlePhoneInput" />
+          </el-form-item>
+          <el-form-item label="个人先进描述:" prop="remark">
+            <el-input v-model="normalForm.remark" type="textarea" :rows="4" placeholder="请填写个人先进获取内容描述。" maxlength="300"
+              show-word-limit />
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleDialogClose">取消</el-button>
+          <el-button type="primary" @click="handleSave">保存</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, reactive, ref } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
+import BasicTable from '@/components/BasicTable.vue';
+import useTableConfig from '@/hooks/useTableConfigHook';
+import ActionButton from '@/components/ActionButton.vue';
+import { TABLE_OPTIONS } from '../configs/activityTables';
+import { ACTIVITY_REGISTRATION_ADVANCED_PERSON_TABLE_COLUMNS } from '../configs/activityTargetTables';
+import type { QueryPageRequest } from '@/types/basic-query';
+import {
+  querySafetyCultureActivityDetail,
+  saveSafetyCultureActivityExecutorManagement,
+  getAllDepartments,
+  addSafetyCultureActivityRegistration,
+  deleteSafetyCultureActivityRegistration,
+  updateSafetyCultureActivityManagement,
+  type ActivityRegistrationTargetItem,
+  type safetyCultureFilePageQuery,
+} from '@/api/safety-culture';
+import { formatDeptTree } from '@/views/disaster/utils/formatDeptTree';
+import { id } from 'element-plus/es/locale';
+
+const route = useRoute();
+
+const currentId = computed(() => Number(route.query.id));
+
+// 表格
+const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+
+const { tableConfig, pagination } = useTableConfig(ACTIVITY_REGISTRATION_ADVANCED_PERSON_TABLE_COLUMNS, TABLE_OPTIONS);
+
+const tableData = ref<any[]>([]);
+
+const tableQuery = reactive<QueryPageRequest<any>>({
+  pageNumber: pagination.pageNumber,
+  pageSize: pagination.pageSize,
+  queryParam: {
+    keyword: '',
+    dateRange: null as any,
+    startTime: '',
+    endTime: '',
+  },
+});
+
+const activityRegistrationId = computed(() => {
+  const id = route.query.id;
+  return id ? Number(id) : undefined;
+});
+
+// 考核表详情
+const activityRegistrationDetail = ref<Partial<ActivityRegistrationTargetItem>>({});
+
+// 添加对话框相关
+const addDialogVisible = ref(false);
+const isEditMode = ref(false);
+const currentEditRow = ref<any>(null);
+const editDetailData = ref(null);
+
+const normalFormRef = ref<FormInstance>();
+const normalForm = reactive({
+  id: '',
+  // employeeDeptName: '',
+  employeeCode: '',
+  employeeName: '',
+  employeeContact: '',
+  remark: '',
+  deptId: '',
+});
+
+const firstLevelDepts = ref<any[]>([]);
+
+const normalFormRules: FormRules = {
+  // employeeDeptName: [
+  //   { required: true, message: '请选择部门名称', trigger: 'blur' },
+  // ],
+  employeeCode: [
+    { required: true, message: '请输入员工工号', trigger: 'blur' },
+  ],
+  employeeName: [
+    { required: true, message: '请输入员工姓名', trigger: 'blur' },
+  ],
+  employeeContact: [
+    { required: true, message: '请输入员工联系方式', trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' },
+  ],
+  remark: [
+    { required: true, message: '请输入个人先进描述', trigger: 'blur' },
+    { max: 300, message: '最多输入300个字符', trigger: 'blur' },
+  ],
+};
+
+const cascaderProp = {
+  expandTrigger: 'click',
+  checkStrictly: true,
+  // emitPath: false,
+  value: 'id',
+  label: 'deptName',
+};
+
+const cascaderRef = ref({});
+
+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 params = {
+      pageNumber: tableQuery.pageNumber,
+      pageSize: tableQuery.pageSize,
+      queryParam: {
+        keyword: tableQuery.queryParam.keyword,
+        startDate: tableQuery.queryParam.startTime,
+        endDate: tableQuery.queryParam.endTime,
+        activityId: activityRegistrationId.value,
+      },
+    };
+
+    const res = await saveSafetyCultureActivityExecutorManagement(params);
+    if (res) {
+      tableData.value = res.records
+    }
+  } catch (e) {
+    console.error('获取执行人安全文化活动报名列表失败:', e);
+    tableData.value = [];
+    pagination.total = 0;
+  } finally {
+    tableConfig.loading = false;
+  }
+}
+
+function handleSearch() {
+  if (tableQuery.queryParam.dateRange && tableQuery.queryParam.dateRange.length === 2) {
+    tableQuery.queryParam.startTime = tableQuery.queryParam.dateRange[0];
+    tableQuery.queryParam.endTime = tableQuery.queryParam.dateRange[1];
+  } else {
+    tableQuery.queryParam.startTime = '';
+    tableQuery.queryParam.endTime = '';
+  }
+  pagination.pageNumber = 1;
+  tableQuery.pageNumber = 1;
+  getTableData();
+}
+
+const handleReset = () => {
+  tableQuery.queryParam.keyword = '';
+  tableQuery.queryParam.dateRange = null;
+  handleSearch();
+};
+
+// 添加注册活动
+const selectDeptType = () => {
+  isEditMode.value = true;
+  addDialogVisible.value = true;
+};
+
+// 处理电话号码输入(只允许数字)
+const handlePhoneInput = (value: string) => {
+  // 只保留数字
+  normalForm.employeeContact = value.replace(/\D/g, '');
+};
+
+// 关闭对话框
+const handleDialogClose = () => {
+  addDialogVisible.value = false;
+  isEditMode.value = false;
+  currentEditRow.value = null;
+  editDetailData.value = null;
+  // 重置表单
+  if (normalFormRef.value) {
+    normalFormRef.value.resetFields();
+    normalForm.deptId = '';
+  }
+};
+
+// 验证普通部门表单
+const validateNormalForm = (): Promise<boolean> => {
+  return new Promise((resolve) => {
+    if (!normalFormRef.value) {
+      resolve(false);
+      return;
+    }
+    normalFormRef.value.validate((valid) => {
+      resolve(valid);
+    });
+  });
+};
+
+// 保存
+const handleSave = async () => {
+
+  type ActivityUserParams = {
+    id?: number; // 编辑时必传,新增时可选
+    activityId?: number;
+    deptId?: number;
+    employeeId: string;
+    employeeName: string;
+    employeeContact: string;
+    remark: string;
+  };
+  
+  const isValid = await validateNormalForm();
+  if (!isValid) {
+    return;
+  }
+  const addUsers: ActivityUserParams = {
+    ...(normalForm.id && { id: Number(normalForm.id) }),
+    activityId: activityRegistrationDetail.value.id,
+    deptId: normalForm.deptId ? Number(normalForm.deptId) : undefined,
+    employeeId: normalForm.employeeCode.trim(),
+    employeeName: normalForm.employeeName.trim(),
+    employeeContact: normalForm.employeeContact.trim(),
+    remark: normalForm.remark.trim(),
+  };
+  try {
+    if (isEditMode.value && normalForm.id) {
+      await updateSafetyCultureActivityManagement(addUsers);
+      ElMessage.success('更新成功');
+    } else {
+      await addSafetyCultureActivityRegistration(addUsers);
+      ElMessage.success('添加成功');
+    }
+    handleDialogClose();
+    getTableData();
+  } catch (e) {
+    console.error('保存失败:', e);
+    ElMessage.error(isEditMode.value ? '更新失败' : '保存失败');
+  }
+};
+
+const handleEdit = async (row: any) => {
+  if (!row.id) {
+    ElMessage.error('缺少记录ID,无法编辑');
+    return;
+  }
+
+  try {
+    isEditMode.value = true;
+    addDialogVisible.value = true;
+    normalForm.id = row.id;
+    normalForm.deptId = row.deptId;
+    normalForm.employeeCode = row.employeeId;
+    normalForm.employeeName = row.employeeName;
+    normalForm.employeeContact = row.employeeContact;
+    normalForm.remark = row.remark;
+  } catch (e) {
+    console.error('获取详情失败:', e);
+    ElMessage.error('获取详情失败');
+  }
+};
+
+const handleDelete = async (row: any) => {
+  try {
+    await ElMessageBox.confirm('确定要删除这条记录吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+    });
+
+    if (!row.id) {
+      ElMessage.error('缺少记录ID,无法删除');
+      return;
+    }
+
+    await deleteSafetyCultureActivityRegistration(row.id);
+    ElMessage.success('删除成功');
+    getTableData();
+  } catch (e: any) {
+    // 用户取消删除或删除失败
+    if (e !== 'cancel') {
+      console.error('删除失败:', e);
+      ElMessage.error('删除失败');
+    }
+  }
+};
+
+const handleExport = () => {
+  console.log('export advanced person list', tableQuery);
+};
+
+const formatDateTime = (dateTimeStr: string | undefined): string => {
+  if (!dateTimeStr) return '';
+  try {
+    const date = new Date(dateTimeStr);
+    const year = date.getFullYear();
+    const month = String(date.getMonth() + 1).padStart(2, '0');
+    const day = String(date.getDate()).padStart(2, '0');
+    const hours = String(date.getHours()).padStart(2, '0');
+    const minutes = String(date.getMinutes()).padStart(2, '0');
+    const seconds = String(date.getSeconds()).padStart(2, '0');
+    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+  } catch (e) {
+    return dateTimeStr;
+  }
+};
+
+const getActivityRegistrationDetail = async () => {
+  if (!activityRegistrationId.value) return;
+
+  try {
+    const res = await querySafetyCultureActivityDetail(activityRegistrationId.value);
+    if (res) {
+      activityRegistrationDetail.value = res;
+    }
+  } catch (e) {
+    console.error('获取考核表详情失败:', e);
+  }
+};
+
+const getDeptData = () => {
+  getAllDepartments().then((res) => {
+    firstLevelDepts.value = formatDeptTree(res);
+    console.log('@res:', res);
+  });
+};
+
+const handleChangeDept = (val, prop) => {
+  // const cascader = cascaderRef.value?.[prop];
+  // const deptInfo = cascader?.getCheckedNodes();
+  // if (deptInfo && deptInfo.length > 0) {
+  //   normalForm.employeeDeptName = deptInfo[0].label;
+  //   normalForm.deptId = val[val.length - 1]
+  // } else {
+  //   normalForm.employeeDeptName = '';
+  // }
+};
+
+onMounted(() => {
+  getTableData();
+  getActivityRegistrationDetail();
+  getDeptData();
+});
+</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 '@/styles/basic-table-file.scss' as *;
+@use '@/views/traffic/violation/style/act-search-table.scss' as *;
+
+.safety-platform-container__header {
+  padding-bottom: 0 !important;
+}
+
+.evaluation-header {
+  width: 100%;
+}
+
+.evaluation-title {
+  font-size: 20px;
+  font-weight: bold;
+  margin: 0 0 12px 0;
+  color: #333;
+}
+
+.evaluation-meta {
+  margin-bottom: 12px;
+  display: flex;
+  gap: 24px;
+  font-size: 14px;
+  color: #666;
+}
+
+.evaluation-meta span {
+  white-space: nowrap;
+}
+
+
+.dialog-footer {
+  text-align: right;
+}
+</style>

+ 401 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/components/safetyCultureActivityManagementDetail.vue

@@ -0,0 +1,401 @@
+<template>
+  <main class="safety-platform-container__main">
+    <BasicForm
+      ref="basicFormRef"
+      :formData="ruleFormData"
+      :formRules="isViewMode ? undefined : formRules"
+      :formConfig="computedFormConfig"
+    >
+      <template #categoryName>
+        <el-select v-model="ruleFormData.categoryName" placeholder="请选择分类名称" :disabled="isViewMode || isFeedbackMode">
+          <el-option v-for="item in classifyNameOptions" :key="item.value" :label="item.label" :value="item.value" />
+        </el-select>
+      </template>
+      <template #responsibleDeptId>
+        <el-cascader
+          v-model="ruleFormData.responsibleDeptId"
+          :options="deptTree"
+          :props="responsibleDeptCascaderProp"
+          clearable
+          :show-all-levels="false"
+          placeholder="请选择责任部门"
+          style="width: 100%"
+          :disabled="isViewMode || isFeedbackMode"
+        />
+      </template>
+      <template #cooperateDeptIds>
+        <el-cascader
+          v-model="ruleFormData.cooperateDeptIds"
+          :options="deptTree"
+          :props="cooperateDeptCascaderProp"
+          clearable
+          collapse-tags
+          :show-all-levels="false"
+          :max-collapse-tags="3"
+          popper-class="cascader-popper--custom"
+          placeholder="请选择配合部门"
+          style="width: 100%"
+          :disabled="isViewMode || isFeedbackMode"
+        />
+      </template>
+      <template #responsiblePersonId>
+        <el-select
+          v-model="ruleFormData.responsiblePersonId"
+          placeholder="请选择责任人"
+          :disabled="isViewMode || isFeedbackMode"
+          filterable
+        >
+          <el-option
+            v-for="user in responsiblePersonOptions"
+            :key="user.value"
+            :label="user.label"
+            :value="user.value"
+          />
+        </el-select>
+      </template>
+      <template #fileUrl>
+        <UploadFiles
+          v-if="isFeedbackMode"
+          label="上传文件"
+          :maxCount="1"
+          :file-list="ruleFormData.fileUrlList"
+          :allow-all-file-types="true"
+          @uploadSuccess="(list: FileItem[]) => handleUploadSuccess(list)"
+          @preview="handlePreview"
+        />
+        <div class="file-list" v-else>
+          <div class="file-item" v-for="file in ruleFormData.fileUrlList" :key="file.fileId">
+            <span class="file-item--name">{{ file.fileName }}</span>
+            <div class="file-item--footer">
+              <el-button link type="primary" @click="previewOnline(file.fileUrl, file.fileType)"
+                >预览</el-button
+              >
+              <!-- <el-button link type="primary" @click.stop="downloadFile(file.fileUrl, file.fileName)"
+                >下载</el-button
+              > -->
+            </div>
+          </div>
+        </div>
+      </template>
+    </BasicForm>
+    <PreviewOnline ref="previewOnlineRef" />
+  </main>
+  <footer class="safety-platform-container__footer">
+    <el-button @click="router.back()">返回</el-button>
+    <el-button v-if="!isViewMode && !isFeedbackMode" type="primary" @click="handleSubmit">
+      {{ isCreateMode ? '提交' : '保存' }}
+    </el-button>
+    <el-button v-if="!isViewMode && isFeedbackMode" type="primary" @click="handleFeedbackSubmit">
+      反馈
+    </el-button>
+  </footer>
+</template>
+
+<script setup lang="ts">
+  import { computed, onMounted, ref } from 'vue';
+  import { useRoute, useRouter } from 'vue-router';
+  import { ElMessage } from 'element-plus';
+  import BasicForm from '@/components/BasicForm.vue';
+  // import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
+  // import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
+  import '@wangeditor/editor/dist/css/style.css';
+  import { useFormConfigHook } from '@/hooks/useFormConfigHook';
+  import { ACADEMY_FILE_FORM_CONFIG, ACADEMY_FILE_FORM_DATA, ACADEMY_FILE_FORM_RULES } from '../configs/form';
+  import {
+    querySafetyCultureActivityDetail,
+    addSafetyCultureActivityManagement,
+    updateSafetyCultureActivity,
+    getAllDepartments,
+    feedbackSafetyCultureActivityManagement
+  } from '@/api/safety-culture';
+  import { queryAvailableUserList } from '@/api/system/person-group';
+  import type { addSafetyCultureFilePageQuery } from '@/api/safety-culture';
+  import type { DeptTree } from '@/types/dept/type';
+  import type { FileItem } from '@/components/UploadFiles/types';
+  import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
+  import UploadFiles from '@/components/UploadFiles/UploadFiles.vue';
+  import { formatAttachmentList } from '@/components/UploadFiles/utils';
+
+  const router = useRouter();
+  const route = useRoute();
+  const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
+
+  const operate = computed(() => (route.query.operate as string) || 'safety-culture-material-create');
+  const currentId = computed(() => Number(route.query.id));
+
+  const isCreateMode = computed(() => operate.value === 'safety-culture-material-create');
+  const isEditMode = computed(() => operate.value === 'safety-culture-material-edit');
+  const isViewMode = computed(() => operate.value === 'safety-culture-material-view');
+  const isFeedbackMode = computed(() => operate.value === 'safety-culture-material-feedback');
+
+  const { ruleFormData, formRules, ruleFormConfig, cloneRuleFormData } = 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 || isFeedbackMode.value) {
+      return viewFormConfig.value;
+    }
+    return ruleFormConfig.value;
+  });
+
+  const basicFormRef = ref<InstanceType<typeof BasicForm>>();
+
+  const responsibleDeptCascaderProp = {
+    expandTrigger: 'hover' as const,
+    checkStrictly: true,
+    emitPath: false,
+    value: 'id',
+    label: 'deptName',
+  };
+
+  const cooperateDeptCascaderProp = {
+    multiple: true,
+    expandTrigger: 'hover' as const,
+    checkStrictly: true,
+    emitPath: false,
+    value: 'id',
+    label: 'deptName',
+  };
+
+  const deptTree = ref<DeptTree[]>([]);
+  const loadDeptTreeData = async () => {
+    const result = await getAllDepartments();
+    deptTree.value = result?.[0]?.children || [];
+  };
+
+  const classifyNameOptions = ref<Array<{ label: string; value: string }>>([
+    { label: '安全综合工作', value: '安全综合工作' },
+    { label: '生产安全工作', value: '生产安全工作' },
+  ]);
+
+  const responsiblePersonOptions = ref<Array<{ label: string; value: number }>>([]);
+  const loadResponsiblePersonOptions = async () => {
+    const result = await queryAvailableUserList({
+      pageNumber: 1,
+      pageSize: 1000,
+      queryParam: {},
+    });
+    responsiblePersonOptions.value = (result.records || []).map((item) => ({
+      label: item.realname || item.staffNo,
+      value: item.id,
+    }));
+  };
+
+  const normalizeToNumberArray = (value: unknown): number[] => {
+    if (Array.isArray(value)) {
+      return value.map((item) => Number(item)).filter((item) => Number.isFinite(item));
+    }
+
+    if (value == null || value === '') {
+      return [];
+    }
+
+    if (typeof value === 'string' && value.includes(',')) {
+      return value
+        .split(',')
+        .map((item) => Number(item.trim()))
+        .filter((item) => Number.isFinite(item));
+    }
+
+    const numericValue = Number(value);
+    return Number.isFinite(numericValue) ? [numericValue] : [];
+  };
+
+  const normalizeCooperateDeptIds = (value: unknown): string => {
+    return normalizeToNumberArray(value)
+      .map((item) => String(item))
+      .join(',');
+  };
+
+  const buildPayload = (): addSafetyCultureFilePageQuery => {
+    const responsibleDeptId = Number(ruleFormData.responsibleDeptId);
+    const responsiblePersonId = Number(ruleFormData.responsiblePersonId);
+
+    return {
+      id: currentId.value,
+      planName: String(ruleFormData.planName || ''),
+      actionContent: String(ruleFormData.actionContent || ''),
+      categoryName: String(ruleFormData.categoryName || ''),
+      responsibleDeptId,
+      responsiblePersonId,
+      cooperateDeptIds: normalizeCooperateDeptIds(ruleFormData.cooperateDeptIds),
+      attachmentUrl: JSON.stringify(ruleFormData.fileUrlList || []),
+    };
+  };
+
+  const handleValidate = async () => {
+    if (!basicFormRef.value) return;
+    const res = await basicFormRef.value.validateForm();
+    return res;
+  };
+
+  const getDetail = async () => {
+    if (!currentId.value) return;
+
+    try {
+      const res = await querySafetyCultureActivityDetail(currentId.value);
+      if (res) {
+        ruleFormData.planName = res.planName || '';
+        ruleFormData.actionContent = res.actionContent || '';
+        ruleFormData.categoryName = res.categoryName != null ? String(res.categoryName) : '';
+        ruleFormData.responsibleDeptId = res.responsibleDeptId != null ? Number(res.responsibleDeptId) : undefined;
+        ruleFormData.responsiblePersonId =
+        res.responsiblePersonId != null ? Number(res.responsiblePersonId) : undefined;
+        ruleFormData.cooperateDeptIds = normalizeToNumberArray(res.cooperateDeptIds);
+        ruleFormData.responsibleDeptName = res.responsibleDeptName || '';
+        ruleFormData.responsiblePersonName = res.responsiblePersonName || '';
+        ruleFormData.fileUrlList = JSON.parse(res.attachmentUrl || '[]') || [];
+      }
+      cloneRuleFormData();
+    } catch (e) {
+      console.error('获取安全文化活动详情失败:', e);
+      ElMessage.error('获取详情失败');
+    }
+  };
+
+  const handleSubmit = async () => {
+    const res = await handleValidate();
+    if (!res) return;
+
+    console.log('ruleFormData', ruleFormData);
+    try {
+      const payload = buildPayload();
+      if (isCreateMode.value) {
+        await addSafetyCultureActivityManagement(payload);
+        ElMessage.success('创建成功');
+      } else if (isEditMode.value && currentId.value) {
+        await updateSafetyCultureActivity({
+          id: currentId.value,
+          ...payload,
+        });
+        ElMessage.success('保存成功');
+      }
+
+      router.back();
+    } catch (e) {
+      console.error('保存安全文化活动失败:', e);
+      ElMessage.error('保存失败,请重试');
+    }
+  };
+
+  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 handleUploadSuccess = async (files: FileItem[]) => {
+    ruleFormData.fileUrlList = await formatAttachmentList(files);
+    // 更新fileUrl字段以触发表单验证
+    ruleFormData.fileUrl = JSON.stringify(ruleFormData.fileUrlList) || '';
+  };
+
+  const previewOnline = (url: string | undefined, type) => {
+    debugger
+    if (url) {
+      previewOnlineRef.value?.open(url, type);
+    }
+  };
+
+  // 反馈
+  const handleFeedbackSubmit = async () => {
+
+    // 验证文件上传(必填)
+    if (!ruleFormData.fileUrlList || ruleFormData.fileUrlList.length === 0) {
+      ElMessage.warning('请上传文件');
+      return;
+    }
+
+    try {
+      const payload = buildPayload();
+      await feedbackSafetyCultureActivityManagement(payload);
+      ElMessage.success('反馈成功');
+      cloneRuleFormData();
+      router.back();
+    } catch (e) {
+      console.error('反馈安全文化活动失败:', e);
+      ElMessage.error('反馈失败,请重试');
+    }
+  };
+
+  onMounted(() => {
+    cloneRuleFormData();
+    loadDeptTreeData();
+    loadResponsiblePersonOptions();
+    // beforeRouteLeave();
+    if (isEditMode.value || isViewMode.value || isFeedbackMode.value) {
+      getDetail();
+    }
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+
+  .editor-container {
+    width: 100%;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    overflow: hidden;
+  }
+
+  .content-display {
+    min-height: 200px;
+    padding: 12px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    background-color: #f5f7fa;
+  }
+
+  .file-display {
+    .file-link {
+      color: #409eff;
+      text-decoration: none;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+
+  .no-file {
+    color: rgba(0, 0, 0, 0.65);
+  }
+
+  .image-uploader {
+    :deep(.el-upload--picture-card) {
+      width: 80px !important;
+      height: 80px !important;
+      line-height: 80px;
+    }
+
+    :deep(.el-upload-list--picture-card .el-upload-list__item) {
+      width: 80px !important;
+      height: 80px !important;
+    }
+  }
+</style>

+ 71 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/configs/activityTables.ts

@@ -0,0 +1,71 @@
+import type { TableColumnProps } from '@/types/basic-table';
+
+// 基础表格样式配置
+export const TABLE_OPTIONS = {
+  emptyText: '暂无数据',
+  loading: true,
+  maxHeight: 'calc(70vh - 150px)',
+};
+
+export const EVALUATION_SYSTEM_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '考核表名称',
+    // prop: 'evaluationTableName',
+    slot: 'evaluationTableName',
+    align: 'left',
+    minWidth: '150px',
+    showOverflowTooltip: true,
+  },
+  {
+    label: '状态',
+    prop: 'status',
+    slot: 'status',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '下发部门',
+    prop: 'issueDepartment',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '下发数',
+    prop: 'issueCount',
+    align: 'center',
+    minWidth: '100px',
+  },
+  {
+    label: '反馈数',
+    prop: 'feedbackCount',
+    align: 'center',
+    minWidth: '100px',
+  },
+  {
+    label: '反馈比例',
+    prop: 'feedbackRatio',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '考核文档',
+    // prop: 'evaluationDocument',
+    slot: 'evaluationDocument',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '计划完成时间',
+    prop: 'plannedCompletionTime',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '操作',
+    prop: 'action',
+    slot: 'action',
+    fixed: 'right',
+    minWidth: '300px',
+    align: 'left',
+  },
+];

+ 181 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/configs/activityTargetTables.ts

@@ -0,0 +1,181 @@
+import type { TableColumnProps } from '@/types/basic-table';
+
+export const ACTIVITY_REGISTRATION_TARGET_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '考核表名称',
+    prop: 'evaluationTableName',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '状态',
+    prop: 'status',
+    slot: 'status',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '下发部门',
+    prop: 'issueDepartment',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '部门负责人',
+    prop: 'departmentLeader',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '联系方式',
+    prop: 'contactPhone',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '考核文档',
+    prop: 'evaluationDocument',
+    slot: 'evaluationDocument',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '计划完成时间',
+    prop: 'plannedCompletionTime',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '操作',
+    // prop: 'action',
+    slot: 'action',
+    fixed: 'right',
+    minWidth: '300px',
+    align: 'left',
+  },
+];
+
+// 先进集体排名表格列
+export const ACTIVITY_REGISTRATION_ADVANCED_GROUP_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '编号',
+    type: 'index',
+    align: 'center',
+    width: '80px',
+  },
+  {
+    label: '部门名称',
+    prop: 'departmentName',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '是否先进集体',
+    prop: 'isAdvancedGroup',
+    slot: 'isAdvancedGroup',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '部门排序',
+    prop: 'departmentSort',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '部门负责人',
+    prop: 'departmentLeader',
+    align: 'left',
+    minWidth: '140px',
+  },
+  {
+    label: '基础分',
+    prop: 'baseScore',
+    align: 'center',
+    minWidth: '100px',
+  },
+  {
+    label: '总分数',
+    prop: 'totalScore',
+    align: 'center',
+    minWidth: '100px',
+  },
+  {
+    label: '加分项分数',
+    prop: 'addScore',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '减分项分数',
+    prop: 'subtractScore',
+    align: 'center',
+    minWidth: '120px',
+  },
+  {
+    label: '操作',
+    slot: 'action',
+    fixed: 'right',
+    minWidth: '100px',
+    align: 'left',
+  },
+];
+
+// 先进个人信息表格列(部门端)
+export const ACTIVITY_REGISTRATION_ADVANCED_PERSON_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '编号',
+    type: 'index',
+    align: 'left',
+    width: '80px',
+  },
+  {
+    label: '员工工号',
+    prop: 'employeeId',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '员工姓名',
+    prop: 'employeeName',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '员工联系方式',
+    prop: 'employeeContact',
+    align: 'left',
+    minWidth: '140px',
+  },
+  {
+    label: '所属部门',
+    prop: 'deptName',
+    align: 'left',
+    minWidth: '150px',
+  },
+  // {
+  //   label: '部门负责人',
+  //   prop: 'departmentLeader',
+  //   align: 'left',
+  //   minWidth: '140px',
+  // },
+  {
+    label: '先进个人描述',
+    prop: 'remark',
+    align: 'left',
+    minWidth: '160px',
+  },
+  {
+    label: '上报日期',
+    prop: 'createdAt',
+    align: 'left',
+    minWidth: '150px',
+  },
+  {
+    label: '操作',
+    slot: 'action',
+    fixed: 'right',
+    minWidth: '150px',
+    align: 'left',
+  },
+];

+ 84 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/configs/form.ts

@@ -0,0 +1,84 @@
+import { FormConfig } from '@/types/basic-form';
+
+export const ACADEMY_FILE_FORM_CONFIG: FormConfig[] = [
+  {
+    prop: 'planName',
+    label: '安全文化活动计划名称:',
+    component: 'ElInput',
+    componentProps: {
+      placeholder: '请输入文件名称',
+    },
+  },
+  {
+    prop: 'actionContent',
+    label: '行动项内容:',
+    component: 'ElInput',
+    componentProps: {
+      placeholder: '请输入行动项内容',
+    },
+  },
+  {
+    prop: 'categoryName',
+    label: '分类名称:',
+    slot: 'categoryName',
+  },
+  {
+    prop: 'responsibleDeptId',
+    label: '责任部门:',
+    slot: 'responsibleDeptId',
+  },
+  {
+    prop: 'responsiblePersonId',
+    label: '责任人:',
+    slot: 'responsiblePersonId',
+  },
+  {
+    prop: 'cooperateDeptIds',
+    label: '配合部门:',
+    slot: 'cooperateDeptIds',
+  },
+  {
+    prop: 'responsibleDeptName',
+    label: '具体负责人部门:',
+    component: 'ElInput',
+    componentProps: {
+      placeholder: '请输入行动项内容',
+    },
+  },
+  {
+    prop: 'responsiblePersonName',
+    label: '具体负责人姓名:',
+    component: 'ElInput',
+    componentProps: {
+      placeholder: '请输入行动项内容',
+    },
+  },
+  {
+    prop: 'fileUrl',
+    label: '附件上传:',
+    slot: 'fileUrl',
+  },
+];
+
+export const ACADEMY_FILE_FORM_DATA = {
+  planName: '',
+  actionContent: '',
+  categoryName: '',
+  responsibleDeptId: undefined as number | undefined,
+  responsiblePersonId: undefined as number | undefined,
+  cooperateDeptIds: [] as number[],
+  responsibleDeptName: '',
+  responsiblePersonName: '',
+  fileUrl: '',
+  fileUrlList: [] as any[],
+};
+
+export const ACADEMY_FILE_FORM_RULES = {
+  planName: [{ required: true, message: '请输入安全活动计划名称', trigger: 'blur' }],
+  actionContent: [{ required: true, message: '请选择行动项内容', trigger: 'change' }],
+  categoryName: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
+  responsibleDeptId: [{ required: true, message: '请选择责任部门', trigger: 'change' }],
+  responsiblePersonId: [{ required: true, message: '请选择责任人', trigger: 'change' }],
+  cooperateDeptIds: [{ required: true, message: '请选择配合部门', trigger: 'change' }],
+  fileUrl: [{ required: true, message: '请上传附件', trigger: 'change' }],
+};

+ 93 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/configs/tables.ts

@@ -0,0 +1,93 @@
+import type { TableColumnProps } from '@/types/basic-table';
+
+// 基础表格样式配置
+export const TABLE_OPTIONS = {
+  emptyText: '暂无数据',
+  loading: true,
+  maxHeight: 'calc(70vh - 150px)',
+};
+
+export const INVENTORY_TABLE_COLUMNS: TableColumnProps[] = [
+  {
+    label: '编号',
+    type: 'index',
+    align: 'left',
+    width: '80px',
+  },
+  {
+    label: '行动项内容',
+    prop: 'planName',
+    align: 'left',
+    minWidth: '150px',
+    // showOverflowTooltip: true,
+  },
+  {
+    label: '状态',
+    prop: 'statusName',
+    align: 'left',
+    minWidth: '100px',
+  },
+  {
+    label: '分类名称',
+    prop: 'categoryNameDisplay',
+    align: 'left',
+    minWidth: '120px',
+  },
+  {
+    label: '安全文化活动计划名称',
+    prop: 'actionContent',
+    align: 'left',
+    minWidth: '120px',
+    // showOverflowTooltip: true,
+  },
+  {
+    label: '责任部门',
+    prop: 'responsibleDeptNameDisplay',
+    align: 'left',
+    minWidth: '160px',
+  },
+  {
+    label: '责任人',
+    prop: 'responsiblePersonNameDisplay',
+    align: 'left',
+    minWidth: '160px',
+  },
+  {
+    label: '配合部门',
+    prop: 'cooperateDeptNameDisplay',
+    align: 'left',
+    minWidth: '160px',
+  },
+  {
+    label: '计划完成时间',
+    prop: 'createdAt',
+    align: 'left',
+    minWidth: '160px',
+  },
+  // {
+  //   label: '完成形式',
+  //   prop: 'createdAt',
+  //   align: 'left',
+  //   minWidth: '160px',
+  // },
+  {
+    label: '具体负责人',
+    prop: 'specificPersonName',
+    align: 'left',
+    minWidth: '160px',
+  },
+  // {
+  //   label: '工作规划/进展',
+  //   prop: 'createdAt',
+  //   align: 'left',
+  //   minWidth: '160px',
+  // },
+  {
+    label: '操作',
+    prop: 'action',
+    slot: 'action',
+    fixed: 'right',
+    width: '250px',
+    align: 'left',
+  },
+];

+ 636 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutor.vue

@@ -0,0 +1,636 @@
+<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 type="primary" class="search-table-container--button" @click="handleCreate">
+              新增
+            </el-button> -->
+            <!-- <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="queryParams.keyword" placeholder="搜索行动项内容/计划名称" class="act-search-input" />
+              </div>
+              <div class="select-box--item">
+                <span>状态:</span>
+                <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
+                  <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="queryParams.classifyName" placeholder="请选择分类" clearable>
+                  <el-option label="安全综合工作" value="安全综合工作" />
+                  <el-option label="生产安全工作" value="生产安全工作" />
+                </el-select>
+              </div>
+              <div class="select-box--item">
+                <span>计划日期范围:</span>
+                <el-date-picker v-model="uploadDateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
+                  end-placeholder="结束日期" value-format="YYYY-MM-DD" format="YYYY-MM-DD" />
+              </div>
+            </section>
+            <section class="search-btn">
+              <el-button type="primary" @click="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 #actionContent="scope">
+              <div class="file-item" v-for="file in scope.row.fileUrlList" :key="file.fileId">
+                <span class="file-item--name">{{ file.fileName }}</span>
+                <div class="file-item--footer">
+                  <el-button link type="primary" @click="previewOnline(file.fileUrl, file.fileType)"
+                    >预览</el-button
+                  >
+                  <el-button link type="primary" @click.stop="downloadFile(file.fileUrl, file.fileName)"
+                    >下载</el-button
+                  >
+                </div>
+              </div>
+            </template> -->
+            <template #action="scope">
+              <div class="action-container--div" style="justify-content: left">
+                <ActionButton text="编辑" v-if="scope.row.status === 1" @click="handleEdit(scope.row.id)" />
+                <ActionButton text="删除" v-if="scope.row.status === 1" :popconfirm="{
+                  title: '确定要删除?',
+                }" @confirm="handleDelete(scope.row.id)" />
+                <ActionButton text="查看" @click="handleView(scope.row.id)" />
+                <ActionButton text="反馈" @click="handleFeedback(scope.row.id)" v-if="scope.row.status == 2"/>
+                <!-- <ActionButton text="下发" @click="handleDispatch(scope.row.id)" v-if="scope.row.status === 1"/> -->
+                <ActionButton text="活动报名" @click="activityRegistration(scope.row.id)" v-if="scope.row.status !== 1"/>
+              </div>
+            </template>
+          </BasicTable>
+        </div>
+      </div>
+      
+      <el-dialog
+        v-model="showIssueDialog"
+        title="安全文化活动下发"
+        width="500px"
+        destroy-on-close
+        @open="onIssueDialogOpen"
+      >
+        <el-form ref="issueFormRef" :model="issueForm" :rules="issueRules" label-width="140px">
+          <el-form-item label="具体负责人部门" prop="rectificationDepartmentId">
+            <el-cascader
+              v-model="issueForm.rectificationDepartmentId"
+              :options="issueDeptTree"
+              :props="cascaderDeptProp"
+              :show-all-levels="false"
+              placeholder="请选择具体负责人部门"
+              filterable
+              clearable
+              style="width: 100%"
+              @change="onIssueDeptChange"
+            />
+          </el-form-item>
+          <el-form-item label="具体负责人" prop="rectificationResponsibleUserId">
+            <el-select
+              v-model="issueForm.rectificationResponsibleUserId"
+              placeholder="选择具体负责人"
+              filterable
+              clearable
+              style="width: 100%"
+            >
+              <el-option
+                v-for="user in issueUserList"
+                :key="user.id"
+                :label="user.realname ?? user.username"
+                :value="user.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="计划开始日期:" prop="startDate">
+            <el-date-picker
+              v-model="issueForm.startDate"
+              type="date"
+              placeholder="请选择计划开始日期"
+              value-format="YYYY-MM-DD"
+              format="YYYY-MM-DD"
+              style="width: 100%"
+              :disabled-date="(date: Date) => {
+                if (issueForm.endDate) {
+                  return date > new Date(issueForm.endDate);
+                }
+                return false;
+              }"
+            />
+          </el-form-item>
+          <el-form-item label="计划结束日期:" prop="endDate">
+            <el-date-picker
+              v-model="issueForm.endDate"
+              type="date"
+              placeholder="请选择计划结束日期"
+              value-format="YYYY-MM-DD"
+              format="YYYY-MM-DD"
+              style="width: 100%"
+              :disabled-date="(date: Date) => {
+                if (issueForm.startDate) {
+                  return date < new Date(issueForm.startDate);
+                }
+                return false;
+              }"
+            />
+          </el-form-item>
+        </el-form>
+        <template #footer>
+          <el-button @click="showIssueDialog = false">取消</el-button>
+          <el-button type="primary" @click="handleIssueSave">保存</el-button>
+        </template>
+      </el-dialog>
+
+      <PreviewOnline ref="previewOnlineRef" />
+
+    </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>
+</template>
+
+<script setup lang="ts">
+import { onMounted, reactive, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import BasicTable from '@/components/BasicTable.vue';
+import useTableConfig from '@/hooks/useTableConfigHook';
+import ActionButton from '@/components/ActionButton.vue';
+import { TABLE_OPTIONS, INVENTORY_TABLE_COLUMNS } from './configs/tables';
+import { useRouter } from 'vue-router';
+import {
+  safetyCultureActivityManagementExecutorFilePage,
+  deleteSafetyCultureActivityManagement,
+  getAllDepartments,
+  activityDistribution,
+  type safetyCultureFileQuery,
+  type safetyCultureFilePageQuery,
+} from '@/api/safety-culture';
+import type { DeptTree } from '@/types/dept/type';
+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 { http } from '@/utils/http/axios';
+import { queryAvailableUserList } from '@/api/production-safety/responsibility-implementation';
+import PreviewOnline from '@/views/disaster/components/PreviewOnline.vue';
+const router = useRouter();
+
+const previewOnlineRef = ref<InstanceType<typeof PreviewOnline>>();
+
+/** 下发弹窗:点击下发打开弹窗,弹窗内部门用 queryAllDeptTree、负责人用 queryAvailableUserList,点击保存才触发下发接口 */
+const showIssueDialog = ref(false);
+const distributionId = ref<number>(0);
+const issueFormRef = ref();
+const issueForm = ref({
+  rectificationDepartmentId: undefined as number | undefined,
+  rectificationResponsibleUserId: undefined as number | undefined,
+  rectificationResponsiblePersonName: '' as string,
+  startDate: '' as string,
+  endDate: '' as string,
+});
+const issueRules = {
+  rectificationDepartmentId: [{ required: true, message: '请选择整改责任部门', trigger: 'change' }],
+  rectificationResponsibleUserId: [{ required: true, message: '请选择整改负责人', trigger: 'change' }],
+  startDate: [{ required: true, message: '请选择计划开始日期', trigger: 'change' }],
+  endDate: [{ required: true, message: '请选择计划结束日期', trigger: 'change' }],
+};
+/** 下发弹窗部门树,与新增隐患台账复查人员所属部门一致(getAllDepartments 取第一级 children) */
+const cascaderDeptProp = {
+  checkStrictly: true,
+  expandTrigger: 'hover' as const,
+  value: 'id',
+  label: 'deptName',
+  emitPath: false,
+};
+const issueDeptTree = ref<DeptTree[]>([]);
+const issueUserList = ref<Array<{ id: number; realname?: string; username?: string }>>([]);
+
+// 表格
+const basicTableRef = ref<InstanceType<typeof BasicTable>>();
+
+const { tableConfig, pagination } = useTableConfig(INVENTORY_TABLE_COLUMNS, TABLE_OPTIONS);
+
+const tableData = ref<any[]>([]);
+const deptNameMap = ref<Record<string, string>>({});
+
+const CATEGORY_NAME_MAP: Record<string, string> = {
+  '0': '外部院级文件',
+  '1': '内部院级文件',
+  '2': '内部院级文件',
+};
+
+const normalizeCategoryName = (row: any): string => {
+  const raw = row?.categoryName ?? row?.classifyName ?? row?.category;
+  if (raw == null || raw === '') {
+    return '-';
+  }
+  const stringValue = String(raw);
+  if (stringValue.includes('外部') || stringValue.includes('内部')) {
+    return stringValue;
+  }
+  return CATEGORY_NAME_MAP[stringValue] || stringValue;
+};
+
+const normalizeListText = (value: unknown): string => {
+  if (Array.isArray(value)) {
+    const list = value.map((item) => String(item).trim()).filter((item) => item);
+    return list.length ? list.join('、') : '-';
+  }
+  if (value == null || value === '') {
+    return '-';
+  }
+  const text = String(value).trim();
+  if (!text) {
+    return '-';
+  }
+  return text.includes(',') ? text.split(',').map((item) => item.trim()).filter((item) => item).join('、') || '-' : text;
+};
+
+const parseIdList = (value: unknown): string[] => {
+  if (Array.isArray(value)) {
+    return value.map((item) => String(item).trim()).filter((item) => item);
+  }
+  if (value == null || value === '') {
+    return [];
+  }
+  const text = String(value).trim();
+  if (!text) {
+    return [];
+  }
+  return text.split(',').map((item) => item.trim()).filter((item) => item);
+};
+
+const flattenDeptTree = (tree: DeptTree[]): DeptTree[] => {
+  const result: DeptTree[] = [];
+  const walk = (nodes: DeptTree[]) => {
+    nodes.forEach((node) => {
+      result.push(node);
+      if (node.children?.length) {
+        walk(node.children);
+      }
+    });
+  };
+  walk(tree || []);
+  return result;
+};
+
+const loadDeptNameMap = async () => {
+  try {
+    const deptTree = await getAllDepartments();
+    const flatList = flattenDeptTree(deptTree || []);
+    const map: Record<string, string> = {};
+    flatList.forEach((dept) => {
+      if (dept.id != null) {
+        map[String(dept.id)] = dept.deptName;
+      }
+    });
+    deptNameMap.value = map;
+  } catch (error) {
+    console.error('加载部门字典失败:', error);
+    deptNameMap.value = {};
+  }
+};
+
+const normalizeCooperateDeptName = (row: any): string => {
+  const rawName = row?.cooperateDeptName || row?.specificDeptName;
+  if (rawName) {
+    return normalizeListText(rawName);
+  }
+
+  const ids = parseIdList(row?.cooperateDeptIds);
+  if (!ids.length) {
+    return '-';
+  }
+
+  const names = ids.map((id) => deptNameMap.value[id] || id).filter((item) => item);
+  return names.length ? names.join('、') : '-';
+};
+
+const normalizeResponsibleDeptName = (row: any): string => {
+  const rawName = row?.responsibleDeptName;
+  if (rawName) {
+    return normalizeListText(rawName);
+  }
+
+  const ids = parseIdList(row?.responsibleDeptId);
+  if (!ids.length) {
+    return '-';
+  }
+
+  const names = ids.map((id) => deptNameMap.value[id] || id).filter((item) => item);
+  return names.length ? names.join('、') : '-';
+};
+
+const queryParams = reactive<safetyCultureFileQuery>({
+  keyword: '', // 文件名称/编号(模糊查询)
+  status: undefined, // 状态:1-启用,0-禁用
+  classifyName: '', // 分类名称:外部院级文件/内部院级文件
+  startDate: '', // 上传日期范围-开始日期
+  endDate: '', // 上传日期范围-结束日期
+});
+
+// 上传日期范围(用于日期选择器)
+const uploadDateRange = ref<[string, string] | null>(null);
+
+const handleSizeChange = (value: number) => {
+  pagination.pageSize = value;
+  getTableData();
+};
+
+const handleCurrentChange = (value: number) => {
+  pagination.pageNumber = value;
+  getTableData();
+};
+
+
+async function getTableData() {
+  tableConfig.loading = true;
+  try {
+    const pageQuery: safetyCultureFilePageQuery = {
+      pageNumber: pagination.pageNumber,
+      pageSize: pagination.pageSize,
+      queryParam: {
+        keyword: queryParams.keyword || undefined,
+        status: queryParams.status,
+        classifyName: queryParams.classifyName || undefined,
+        startDate: queryParams.startDate || undefined,
+        endDate: queryParams.endDate || undefined,
+      },
+    };
+    const res = await safetyCultureActivityManagementExecutorFilePage(pageQuery);
+    if (res) {
+      tableData.value = (res.records || []).map((item: any) => ({
+        ...item,
+        categoryNameDisplay: normalizeCategoryName(item),
+        responsibleDeptNameDisplay: normalizeResponsibleDeptName(item),
+        responsiblePersonNameDisplay: normalizeListText(
+          item.responsiblePersonName || item.specificPersonName || item.responsiblePersonId,
+        ),
+        cooperateDeptNameDisplay: normalizeCooperateDeptName(item),
+        fileUrlList: JSON.parse(item.attachmentUrl || '[]'),
+      }));
+      pagination.total = res.totalRow || 0;
+    }
+  } catch (e) {
+    console.error('获取院级文件列表失败:', e);
+    tableData.value = [];
+    pagination.total = 0;
+  } finally {
+    tableConfig.loading = false;
+  }
+}
+
+const handleSearch = () => {
+  // 处理日期范围
+  if (uploadDateRange.value && uploadDateRange.value.length === 2) {
+    queryParams.startDate = uploadDateRange.value[0];
+    queryParams.endDate = uploadDateRange.value[1];
+  } else {
+    queryParams.startDate = '';
+    queryParams.endDate = '';
+  }
+
+  pagination.pageNumber = 1;
+  getTableData();
+};
+
+const handleReset = () => {
+  queryParams.keyword = '';
+  queryParams.status = undefined;
+  queryParams.classifyName = '';
+  queryParams.startDate = '';
+  queryParams.endDate = '';
+  uploadDateRange.value = null;
+  handleSearch();
+};
+
+// 批量导入
+const batchImportVisible = ref(false);
+const { urlPrefix } = useGlobSetting();
+const importApiUrl = ref(urlJoin(urlPrefix, '/productionSafety/academyFile/import'));
+const templateUrl = ref('./skyeye-file-upload/sfysecurity/TEMPLATE/import-academy-file-template.xlsx');
+
+const handleImport = () => {
+  batchImportVisible.value = true;
+};
+
+const handleUpdate = () => {
+  batchImportVisible.value = false;
+  getTableData();
+};
+
+const handleDownload = async () => {
+  try {
+    const exportParams: safetyCultureFileQuery = {
+      keyword: queryParams.keyword || undefined,
+      status: queryParams.status,
+      classifyName: queryParams.classifyName || undefined,
+      startDate: queryParams.startDate || undefined,
+      endDate: queryParams.endDate || undefined,
+    };
+    // const response = await exportAcademyFile(exportParams, queryParams.classifyName || undefined);
+    // if (response) {
+    //   const fileName = `院级文件管理_${new Date().toISOString().split('T')[0]}.xlsx`;
+    //   downloadByData(response, fileName);
+    //   ElMessage.success('导出成功');
+    // }
+  } catch (e) {
+    console.error('导出院级文件失败:', e);
+    ElMessage.error('导出失败,请重试');
+  }
+};
+
+const handleCreate = () => {
+  router.push({
+    name: 'safetyCultureActivityManagementExecutorActivityRegistration',
+    query: {
+      operate: 'safety-culture-material-create',
+    },
+  });
+};
+
+const handleEdit = (id: number) => {
+  router.push({
+    name: 'safetyCultureActivityManagementExecutorActivityRegistration',
+    query: {
+      id,
+      operate: 'safety-culture-material-edit',
+    },
+  });
+};
+
+const handleDelete = async (id: number) => {
+  try {
+    await deleteSafetyCultureActivityManagement(id);
+    ElMessage.success('删除成功');
+    getTableData();
+  } catch (e) {
+    console.error('删除安全文化活动失败:', e);
+    ElMessage.error('删除失败,请重试');
+  }
+};
+
+const handleView = (id: number) => {
+   router.push({
+    name: 'safetyCultureActivityManagementExecutorItem',
+    query: {
+      id,
+      operate: 'safety-culture-material-view',
+    },
+  });
+};
+
+const activityRegistration = async (id: number) => {
+  router.push({
+    name: 'safetyCultureActivityManagementExecutorActivityRegistration',
+    query: {
+      id,
+      operate: 'safety-culture-material-view',
+    },
+  });
+};
+
+const handleFeedback = (id: number) => {
+  router.push({
+    name: 'safetyCultureActivityManagementExecutorItem',
+    query: {
+      id,
+      operate: 'safety-culture-material-feedback',
+    },
+  });
+};
+
+const onIssueDialogOpen = async () => {
+  try {
+    const [deptRes, userRes] = await Promise.all([
+      getAllDepartments(),
+      queryAvailableUserList({ pageNumber: 1, pageSize: 9999, queryParam: {} }),
+    ]);
+    const fullTree = (deptRes as DeptTree[]) ?? [];
+    issueDeptTree.value = Array.isArray(fullTree) && fullTree[0]?.children ? fullTree[0].children : [];
+    issueUserList.value = (userRes as any)?.records ?? [];
+  } catch (e) {
+    console.error('获取部门/用户列表失败:', e);
+    ElMessage.error(e?.message || e?.data || '加载部门或负责人列表失败');
+    issueDeptTree.value = [];
+    issueUserList.value = [];
+  }
+};
+
+const onIssueDeptChange = () => {
+  issueForm.value.rectificationResponsibleUserId = undefined;
+  issueForm.value.rectificationResponsiblePersonName = '';
+};
+
+const handleDispatch = (id: number) => {
+  distributionId.value = id;
+  showIssueDialog.value = true;
+};
+
+/** 从部门树中根据 id 查找部门名称 */
+function findDeptNameById(nodes: DeptTree[] | undefined, id: number): string {
+  if (!nodes?.length) return '';
+  for (const n of nodes) {
+    if (n.id === id) return n.deptName ?? '';
+    if (n.children?.length) {
+      const found = findDeptNameById(n.children, id);
+      if (found) return found;
+    }
+  }
+  return '';
+};
+
+const handleIssueSave = async () => {
+  await issueFormRef.value?.validate?.().catch(() => {});
+  const { rectificationDepartmentId, rectificationResponsibleUserId } = issueForm.value;
+  if (rectificationDepartmentId == null || rectificationResponsibleUserId == null) {
+    ElMessage.warning('请选择整改责任部门和整改负责人');
+    return;
+  }
+  if (!issueForm.value.startDate || !issueForm.value.endDate) {
+    ElMessage.warning('请选择计划开始日期和计划结束日期');
+    return;
+  }
+  const selectedUser = issueUserList.value.find((u) => u.id === rectificationResponsibleUserId);
+  const personName = selectedUser?.realname ?? selectedUser?.username ?? '';
+  const deptName = findDeptNameById(issueDeptTree.value, rectificationDepartmentId);
+  try {
+    await activityDistribution({
+      id: distributionId.value,
+      specificDeptId: String(rectificationDepartmentId),
+      specificPersonId: String(rectificationResponsibleUserId),
+      startTime: issueForm.value.startDate,
+      endTime: issueForm.value.endDate,
+    });
+    ElMessage.success('下发成功');
+    showIssueDialog.value = false;
+    getTableData();
+  } catch (e) {
+    console.error('下发失败:', e);
+    ElMessage.error(e?.message || e?.data || '下发失败,请重试');
+  }
+};
+
+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 previewOnline = (url: string | undefined, type) => {
+  if (url) {
+    previewOnlineRef.value?.open(url, type);
+  }
+};
+
+onMounted(() => {
+  loadDeptNameMap().finally(() => {
+    getTableData();
+  });
+  // loginSw();
+});
+</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 *;
+
+.action-content {
+  color: #409eff;
+  cursor: pointer;
+}
+
+
+</style>

+ 26 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutorActivityRegistration.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <BreadcrumbBack />
+      <span class="breadcrumb-title">活动报名管理</span>
+    </header>
+    <AtivityRegistrationManagement />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import BreadcrumbBack from '@/components/BreadcrumbBack.vue';
+  import AtivityRegistrationManagement from './components/activityRegistrationManagement.vue';
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/page-main-layout.scss' as *;
+
+  .safety-platform-container__header {
+    flex-direction: row !important;
+    justify-content: flex-start !important;
+    gap: 8px !important;
+  }
+</style>
+

+ 46 - 0
src/views/production-safety/safety-culture/safetyCultureActivityManagementExecutor/safetyCultureActivityManagementExecutorItem.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <BreadcrumbBack />
+      <span class="breadcrumb-title">{{ headerTitle }}</span>
+    </header>
+    <SafetyCultureActivityManagementDetail />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed } from 'vue';
+  import { useRoute } from 'vue-router';
+  import BreadcrumbBack from '@/components/BreadcrumbBack.vue';
+  import SafetyCultureActivityManagementDetail from './components/safetyCultureActivityManagementDetail.vue';
+
+  const route = useRoute();
+  const operate = route.query.operate as string;
+
+  const headerTitle = computed(() => {
+    switch (operate) {
+      case 'safety-culture-material-create':
+        return '新增安全文化活动';
+      case 'safety-culture-material-edit':
+        return '编辑安全文化活动';
+      case 'safety-culture-material-view':
+        return '查看安全文化活动';
+      case 'safety-culture-material-feedback':
+        return '安全文化活动反馈';
+      default:
+        return '未知操作';
+    }
+  });
+</script>
+
+<style scoped lang="scss">
+  @use '@/styles/page-details-layout.scss' as *;
+  @use '@/styles/page-main-layout.scss' as *;
+
+  .safety-platform-container__header {
+    flex-direction: row !important;
+    justify-content: flex-start !important;
+    gap: 8px !important;
+  }
+</style>
+