Pārlūkot izejas kodu

Merge branch 'dev-bxy' into 'dev'

feat: 应急管理总览完成

See merge request product-group-fe/sfy-safety-group/sfy-safety!169
毕欣怡 9 mēneši atpakaļ
vecāks
revīzija
fa6d38bcae

+ 110 - 0
src/api/emergency-overview/index.ts

@@ -18,3 +18,113 @@ export const getOverviewSupplyCount = () => {
     method: 'get',
   });
 };
+
+/**
+ * @description: 查询应急预案
+ */
+export interface PlanInfoList {
+  count: number; // 数量
+  planType: string; // 预案类型
+  planDetailList: PlanDetail[] | [];
+}
+export interface PlanDetail {
+  id?: number; // 自增主键
+  planName?: string; // 预案名称
+  planType?: string; // 预案类型(字典)
+  eventType?: string; // 事件类型(字典)
+  deptId?: number; // 制定部门id
+  deptName?: string; // 制定部门名称
+  status?: number; // 状态: 0-未审批,1-预案审批中,2-预案已退回,3-已公示
+  approvalTemplateId?: number; // 审批模板id
+  appendix?: string; // 附件
+  approvalDescription?: string; // 审批描述
+  createdBy?: number; // 提交人
+  createdAt?: string; // 创建时间
+  updatedAt?: string; // 更新时间
+  isDeleted?: number; // 0-未删除,大于0(时间戳)-已删除
+}
+export const getOverviewEmergencyPlan = () => {
+  return http.request({
+    url: '/overview/queryEmergencyPlanOverview',
+    method: 'get',
+  });
+};
+
+/**
+ * @description: 查询应急演练
+ */
+export interface QueryEmergencyDrillOverviewRes {
+  year: number;
+  drillPlanByScopeList: DrillPlanByScopeList[];
+}
+export interface DrillPlanByScopeList {
+  drillScope: string; // 演练规模
+  drillPlanCount: number; // 演练计划数量
+  completedDrillCount: number; // 已完成演练数量
+  drillPlanList: DrillPlanList[]; // 演练计划列表
+}
+export interface DrillPlanList {
+  id: number; // 自增主键
+  drillScope: string; // 演练规模(字典)
+  drillContent: string; // 演练内容
+  dueCompleteTime: string; // 计划完成时间
+  responsibleDeptIdList: string; // 责任部门id列表
+  responsibleDeptNameList: string; // 责任部门名称列表
+  coordinateDeptIdList: string; // 配合部门id列表
+  coordinateDeptNameList: string; // 配合部门名称列表
+  emergencyPlanId: number; // 应急预案id
+  approvalTemplateId: number; // 审批模板id
+  status: number; // 状态: 1-待传脚本,2-脚本会签,3-待执行,4-待记录,5-记录待审批,6-已退回,7-已完成
+  approvalDescription: string; // 审批描述
+  drillTime: string; // 演练时间
+  drillLocation: string; // 演练地点
+  personInChargeId: number; // 演练负责人id
+  drillDeptIdList: string; // 演练部门id列表
+  drillDeptNameList: string; // 演练部门名称列表
+  drillScript: string; // 演练脚本
+  createdBy: number; // 提交人
+  createdAt: string; // 创建时间
+  updatedAt: string; // 更新时间
+  isDeleted: number; // 0-未删除,大于0(时间戳)-已删除
+}
+export const getOverviewEmergencyExercise = () => {
+  return http.request({
+    url: '/overview/queryEmergencyDrillOverview',
+    method: 'get',
+  });
+};
+
+/**
+ * @description: 查询应急处置
+ */
+export interface QueryEmergencyHandleOverviewRes {
+  year: number; // 年份
+  currentMonthCount: number; // 本月应急事件数量
+  currentYearCount: number; // 本年应急事件数量
+  handleTaskByMonthList: HandleTaskByMonthList[]; //处置任务按月份列表
+  handleTaskList: HandleTaskList[]; // 处置任务列表
+}
+export interface HandleTaskByMonthList {
+  month: number; // 月份
+  count: number; // 数量
+}
+export interface HandleTaskList {
+  id: number; // 自增主键
+  eventType: string; // 事件类型(字典)
+  eventLocation: string; // 事件地点
+  eventName: string; // 事件名称
+  emergencyPlanId: number; // 应急预案id
+  startTime: string; // 启动时间
+  status: number; // 状态: 1-启动中,2-已结束,3-已关闭
+  completeTime: string; // 处置完成时间
+  suggestion: string; // 建议
+  createdAt: string; // 创建时间
+  updatedAt: string; // 更新时间
+  isDeleted: number; // 0-未删除,大于0(时间戳)-已删除
+}
+export const getOverviewEmergencyProcedure = () => {
+  return http.request({
+    url: '/overview/queryEmergencyHandleOverview',
+    method: 'get',
+  });
+};

+ 4 - 2
src/views/emergency/emergency-drill/constants.ts

@@ -5,7 +5,8 @@ export const EMERGENCY_DRILL_STATUS = {
   WAIT_EXECUTE: 3, // 待执行
   WAIT_RECORD: 4, // 待记录
   WAIT_CHECK: 5, // 记录待审批
-  COMPLETE: 6, // 已完成
+  RETURN: 6, // 已退回
+  COMPLETE: 7, // 已完成
 };
 
 export const EMERGENCY_DRILL_STATUS_DICT = {
@@ -14,7 +15,8 @@ export const EMERGENCY_DRILL_STATUS_DICT = {
   3: '待执行', // 待执行
   4: '待记录', // 待记录
   5: '记录待审批', // 待审批
-  6: '已完成', // 已完成
+  6: '已退回', // 已退回
+  7: '已完成', // 已完成
 };
 
 export const EMERGENCY_DRILL_DETAIL_SUBPAGE = [

+ 97 - 48
src/views/emergency/overview/components/EmergencyEventsChart.vue

@@ -3,66 +3,115 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted, onBeforeUnmount } from 'vue';
+  import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
   import * as echarts from 'echarts';
+  import { HandleTaskByMonthList } from '@/api/emergency-overview';
+
+  const props = defineProps<{
+    data: HandleTaskByMonthList[];
+  }>();
 
   const chartRef = ref<HTMLDivElement | null>(null);
   let chartInstance: echarts.ECharts | null = null;
 
-  const option: echarts.EChartsOption = {
-    grid: {
-      top: 20,
-      bottom: 40,
-      left: 45,
-      right: 30,
-    },
-    tooltip: {
-      trigger: 'axis',
-    },
-    xAxis: {
-      type: 'category',
-      data: [
-        '1\n月',
-        '2\n月',
-        '3\n月',
-        '4\n月',
-        '5\n月',
-        '6\n月',
-        '7\n月',
-        '8\n月',
-        '9\n月',
-        '10\n月',
-        '11\n月',
-        '12\n月',
-      ],
-    },
-    yAxis: {
-      type: 'value',
-    },
-    series: [
-      {
-        name: '事件数',
-        type: 'line',
-        showSymbol: false,
-        data: [3, 10, 2, 22, 1, 28, 23, 190, 15, 12, 5, 30],
-      },
-    ],
+  // 生成12个月的X轴标签
+  const xAxisData = [
+    '1\n月',
+    '2\n月',
+    '3\n月',
+    '4\n月',
+    '5\n月',
+    '6\n月',
+    '7\n月',
+    '8\n月',
+    '9\n月',
+    '10\n月',
+    '11\n月',
+    '12\n月',
+  ];
+
+  // 将传入的数据映射为长度为12的数组,缺失的月份补0
+  const formatData = (rawData: HandleTaskByMonthList[]): number[] => {
+    const map = new Map<number, number>();
+    if (!rawData || rawData.length === 0) {
+      return Array(12).fill(0);
+    }
+    rawData.forEach((item) => {
+      const month = item.month;
+      if (month >= 1 && month <= 12) {
+        map.set(month, item.count);
+      }
+    });
+    // 生成12个月的数据,不存在的设为0
+    return Array.from({ length: 12 }, (_, i) => map.get(i + 1) || 0);
   };
 
-  onMounted(() => {
-    if (chartRef.value) {
-      chartInstance = echarts.init(chartRef.value);
-      chartInstance.setOption(option);
-      window.addEventListener('resize', resizeChart);
+  // 初始化图表
+  const initChart = () => {
+    if (!chartRef.value) return;
+
+    // 销毁旧实例
+    if (chartInstance) {
+      chartInstance.dispose();
     }
-  });
 
-  function resizeChart() {
+    chartInstance = echarts.init(chartRef.value);
+
+    const seriesData = formatData(props.data);
+
+    const option: echarts.EChartsOption = {
+      grid: {
+        top: 20,
+        bottom: 40,
+        left: 45,
+        right: 30,
+      },
+      tooltip: {
+        trigger: 'axis',
+      },
+      xAxis: {
+        type: 'category',
+        data: xAxisData,
+      },
+      yAxis: {
+        type: 'value',
+      },
+      series: [
+        {
+          name: '事件数',
+          type: 'line',
+          showSymbol: false,
+          data: seriesData,
+        },
+      ],
+    };
+
+    chartInstance.setOption(option);
+    window.addEventListener('resize', resizeChart);
+  };
+
+  // 调整图表大小
+  const resizeChart = () => {
     chartInstance?.resize();
-  }
+  };
+
+  watch(
+    () => props.data,
+    () => {
+      initChart();
+    },
+    { deep: true },
+  );
+
+  onMounted(() => {
+    initChart();
+  });
 
   onBeforeUnmount(() => {
     window.removeEventListener('resize', resizeChart);
-    chartInstance?.dispose();
+    if (chartInstance) {
+      chartInstance.dispose();
+      chartInstance = null;
+    }
   });
 </script>

+ 110 - 134
src/views/emergency/overview/components/EmergencyExercise.vue

@@ -4,51 +4,55 @@
       <span class="line"></span>
       <span class="title">应急演练</span>
     </div>
-    <YearSelector class="year-selector" :years="yearsList" :defaultYear="defaultYear" @year-change="handleYearChange" />
-    <div class="container-exercise">
+    <YearSelector
+      class="year-selector"
+      :years="yearsList"
+      :default-year="yearSelected"
+      @year-change="handleYearChange"
+    />
+    <div class="container-exercise" v-loading="loading">
       <div class="exercise-tabs">
         <div
           class="exercise-tab"
-          :class="{ 'active-tab': item.name === activeTab }"
-          v-for="item in exerciseTabs"
-          :key="item.name"
-          @click="handleChangeTab(item.name)"
+          :class="{ 'active-tab': item.itemCode === activeTab }"
+          v-for="item in drillScopeDice"
+          :key="item.itemCode"
+          @click="handleChangeTab(item.itemCode)"
         >
-          {{ item.name }}
+          {{ getDrillScope(item.itemCode) }}
         </div>
       </div>
       <div class="exercise-counts">
         <div>
           <span>年度演练计划</span>
-          <span class="exercise-count">{{ yearCount }}</span>
+          <span class="exercise-count">{{
+            exerciseCountList.find((item) => item.drillScope === activeTab)?.drillPlanCount
+          }}</span>
         </div>
         <div>
           <span>已完成演练</span>
-          <span class="exercise-count">{{ finishedCount }}</span>
+          <span class="exercise-count">{{
+            exerciseCountList.find((item) => item.drillScope === activeTab)?.completedDrillCount
+          }}</span>
         </div>
       </div>
       <div class="exercise-list">
+        <div v-if="exercisesList.length === 0" class="exercise-list-no-data">暂无演练计划</div>
         <div class="exercise-item" v-for="item in exercisesList" :key="item.id" @click="handleJumpToDetail(item.id)">
           <div class="exercise-info">
-            <span class="exercise-title">{{ item.title }}</span>
+            <span class="exercise-title">{{ item.drillContent }}</span>
             <div class="exercise-info-content">
-              <span class="exercise-date">{{ item.date }}</span>
-              <div class="exercise-label" v-if="item.label">
-                <div class="label-content">{{ item.label }}</div>
+              <span class="exercise-date">{{
+                item.status === EMERGENCY_DRILL_STATUS.COMPLETE ? item.drillTime : item.dueCompleteTime
+              }}</span>
+              <div class="exercise-label" v-if="item.status === EMERGENCY_DRILL_STATUS.COMPLETE">
+                <div class="label-content">已演练</div>
               </div>
             </div>
           </div>
-          <span class="exercise-dept">{{ item.dept }}</span>
-        </div>
-
-        <!-- 哨兵 -->
-        <div ref="sentinelRef" style="height: 1px"></div>
-
-        <div v-if="isLoading" class="loading">
-          <span>加载中...</span>
-        </div>
-        <div v-if="!hasMore && exercisesList.length > 0" class="no-more">
-          <span>暂无更多应急演练</span>
+          <div class="exercise-dept">{{
+            mergeAndFormat(item.responsibleDeptNameList, item.coordinateDeptNameList)
+          }}</div>
         </div>
       </div>
     </div>
@@ -56,119 +60,89 @@
 </template>
 
 <script setup lang="ts">
-  import { onMounted, ref } from 'vue';
+  import { onMounted, ref, watch } from 'vue';
+  import { useRouter } from 'vue-router';
   import YearSelector from './YearSelector.vue';
-  import { useInfiniteScroll } from '../hooks/useInfiniteScroll';
-
-  interface Item {
-    id: number;
-    title: string;
-    label?: string;
-    date: string;
-    dept: string;
-  }
+  import { useEmergencyDrillHook } from '@/views/emergency/emergency-drill/hook';
+  import { EMERGENCY_DRILL_STATUS } from '@/views/emergency/emergency-drill/constants';
+  import {
+    QueryEmergencyDrillOverviewRes,
+    DrillPlanByScopeList,
+    DrillPlanList,
+    getOverviewEmergencyExercise,
+  } from '@/api/emergency-overview';
 
-  const defaultYear = ref<number>(new Date().getFullYear()); // 默认为当前年份
-  const yearsList = ref<number[]>([2021, 2022, 2023, 2024, 2025]);
-  const yearSelected = ref<number>(defaultYear.value);
-
-  const exerciseTabs = ref([{ name: '院级演练' }, { name: '部门级演练' }]);
-  const activeTab = ref('院级演练');
-
-  const yearCount = ref<number>(0);
-  const finishedCount = ref<number>(0);
-  const exercisesList = ref<Item[]>([]);
-  const page = ref<number>(1);
-  const pageSize = ref<number>(10);
-  const hasMore = ref<boolean>(true);
-  const isLoading = ref<boolean>(false);
-  const sentinelRef = ref<HTMLElement | null>(null); // 哨兵元素引用
-
-  const params = ref({
-    year: yearSelected.value,
-    tab: activeTab.value,
-    page: page.value,
-    pageSize: pageSize.value,
-  });
-  // 请求后端接口获取数据
-  const fetchExerciseData = async () => {
-    if (isLoading.value || !hasMore.value) return;
-
-    isLoading.value = true;
-
-    try {
-      // TODO: 替换为实际的 API 请求
-      // const res = await axios.get('/api/items', params.value);
-      const res = {
-        data: {
-          items: [
-            { id: 1, title: '火灾爆炸事故专项应急预案', label: '已演练', date: '2025-01-01', dept: '安全与应急管理部' },
-            { id: 2, title: '消防安全综合应急预案', label: '已演练', date: '2025-01-01', dept: '安全与应急管理部' },
-            { id: 3, title: '化学品泄漏应急处置方案', label: '已演练', date: '2025-01-01', dept: '环境保护部' },
-            { id: 4, title: '自然灾害应急响应预案', label: '已演练', date: '2025-01-01', dept: '自然灾害管理局' },
-            { id: 5, title: '公共卫生事件应急预案', label: '已演练', date: '2025-01-01', dept: '公共卫生部门' },
-            { id: 6, title: '交通事故应急处理方案', date: '2025-01-01', dept: '交通运输部' },
-            { id: 1, title: '火灾爆炸事故专项应急预案', date: '2025-01-01', dept: '安全与应急管理部' },
-            { id: 2, title: '消防安全综合应急预案', date: '2025-01-01', dept: '安全与应急管理部' },
-            { id: 3, title: '化学品泄漏应急处置方案', date: '2025-01-01', dept: '环境保护部' },
-            { id: 4, title: '自然灾害应急响应预案', date: '2025-01-01', dept: '自然灾害管理局' },
-            { id: 5, title: '公共卫生事件应急预案', date: '2025-01-01', dept: '公共卫生部门' },
-            { id: 6, title: '交通事故应急处理方案', date: '2025-01-01', dept: '交通运输部' },
-            { id: 7, title: '电力系统故障应急预案', date: '2025-01-01', dept: '电力公司' },
-            { id: 8, title: '信息安全事件应急预案', date: '2025-01-01', dept: '信息技术部' },
-            { id: 9, title: '食品安全事故应急预案', date: '2025-01-01', dept: '食品药品监督管理局' },
-            { id: 10, title: '建筑工程事故应急预案', date: '2025-01-01', dept: '建设局' },
-          ],
-        },
-      };
-
-      // TODO: 年度演练计划和已完成演练数量赋值
-      const newItems = res.data.items as Item[];
-
-      if (newItems.length === 0) {
-        hasMore.value = false;
-      } else {
-        exercisesList.value.push(...newItems);
-        page.value++;
-      }
-    } catch (error) {
-      console.error('请求失败:', error);
-    } finally {
-      isLoading.value = false;
-    }
-  };
+  const { drillScopeDice, getDrillScopeDict, getDrillScope } = useEmergencyDrillHook();
 
-  // 切换标签/年份都有的状态重置
-  const resetStateAndFetchData = () => {
-    hasMore.value = true;
-    page.value = 1;
-    exercisesList.value = []; // 清空当前列表
-    yearCount.value = 0;
-    finishedCount.value = 0;
-    console.log('params', params.value);
-    fetchExerciseData();
-  };
+  const router = useRouter();
+
+  const loading = ref(true);
+  const emergencyExerciseData = ref<QueryEmergencyDrillOverviewRes[]>([]);
+  const exerciseCountList = ref<DrillPlanByScopeList[]>([]);
+  const exercisesList = ref<DrillPlanList[]>([]);
+
+  const yearsList = ref<number[]>([]);
+  const yearSelected = ref<number | undefined>();
 
-  // 切换标签时重置状态
-  const handleChangeTab = (tabName: string) => {
-    activeTab.value = tabName;
-    resetStateAndFetchData();
+  const activeTab = ref<string>();
+
+  const handleChangeTab = (tabCode: string) => {
+    activeTab.value = tabCode;
   };
 
   const handleYearChange = (year: number) => {
     yearSelected.value = year;
-    resetStateAndFetchData();
   };
 
-  // 跳转到详情页
+  function mergeAndFormat(A: string = '', B: string = ''): string {
+    if (!A) A = '[]';
+    if (!B) B = '[]';
+
+    const partsA = A.replace(/[\[\]]/g, '')
+      .split(',')
+      .map((s) => s.trim())
+      .filter((s) => s.length > 0);
+
+    const partsB = B.replace(/[\[\]]/g, '')
+      .split(',')
+      .map((s) => s.trim())
+      .filter((s) => s.length > 0);
+
+    return [...partsA, ...partsB].join('、');
+  }
+
   const handleJumpToDetail = (id: number) => {
-    // TODO: 替换为实际的路由跳转
-    console.log(`跳转到详情页,ID: ${id}`);
+    router.push({
+      path: '/emergency-management/emergency-drill/emergency-drill-plan-view',
+      query: {
+        id: id,
+      },
+    });
   };
 
-  onMounted(() => {
-    fetchExerciseData();
-    useInfiniteScroll(sentinelRef, fetchExerciseData);
+  watch(
+    [() => yearSelected.value, () => activeTab.value],
+    (newValues) => {
+      loading.value = true;
+      exerciseCountList.value =
+        emergencyExerciseData.value.find((item) => item.year === newValues[0])?.drillPlanByScopeList || [];
+      exercisesList.value =
+        exerciseCountList.value.find((item) => item.drillScope === newValues[1])?.drillPlanList || [];
+      loading.value = false;
+    },
+    { immediate: true },
+  );
+
+  onMounted(async () => {
+    await getDrillScopeDict();
+    activeTab.value = drillScopeDice.value[0].itemCode;
+    getOverviewEmergencyExercise().then((res) => {
+      emergencyExerciseData.value = res.drillPlanByYearList;
+      if (emergencyExerciseData.value && emergencyExerciseData.value.length > 0) {
+        yearsList.value = emergencyExerciseData.value.map((item) => item.year);
+        yearSelected.value = emergencyExerciseData.value[emergencyExerciseData.value.length - 1].year;
+      }
+    });
   });
 </script>
 
@@ -278,7 +252,6 @@
             display: flex;
             justify-content: space-between;
             position: relative;
-            // padding-right: 56px;
 
             .exercise-title {
               font-weight: 500;
@@ -304,9 +277,6 @@
               background: #52c41a;
               font-size: 10px;
               color: #ffffff;
-              // position: absolute;
-              // top: 3px;
-              // right: 0;
               margin-left: 5px;
               transform: skew(-15deg, 0deg);
 
@@ -322,6 +292,9 @@
             font-weight: 400;
             font-size: 14px;
             color: #666666;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
           }
         }
 
@@ -336,13 +309,16 @@
           margin-bottom: 0;
         }
       }
-    }
 
-    .loading,
-    .no-more {
-      text-align: center;
-      padding: 10px;
-      color: gray;
+      .exercise-list-no-data {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: 14px;
+        color: #666666;
+      }
     }
   }
 </style>

+ 53 - 99
src/views/emergency/overview/components/EmergencyPlan.vue

@@ -4,33 +4,25 @@
       <span class="line"></span>
       <span class="title">应急预案</span>
     </div>
-    <div class="container-plan">
+    <div class="container-plan" v-loading="loading">
       <div class="plan-tabs">
         <div
           class="plan-tab"
-          :class="{ 'active-tab': item.name === activeTab }"
-          v-for="item in planTabs"
-          :key="item.name"
-          @click="handleChangeTab(item.name)"
+          :class="{ 'active-tab': item.planType === activeTab }"
+          v-for="item in emergencyPlanList"
+          :key="item.planType"
+          @click="handleChangeTab(item.planType)"
         >
-          {{ item.name }}
-          <span v-if="item.name === activeTab">({{ activeSum }})</span>
+          {{ getPlanType(item.planType) }}({{ item.count }})
         </div>
       </div>
       <div class="plan-list">
-        <div class="plan-item" v-for="item in plansList" :key="item.id" @click="handleJumpToDetail(item.id)">
-          <span class="plan-title">{{ item.title }}</span>
-          <span class="plan-dept">{{ item.dept }}</span>
+        <div v-if="activePlanDetailList.length === 0" class="plan-list-no-data">
+          <span>暂无应急预案</span>
         </div>
-
-        <!-- 哨兵 -->
-        <div ref="sentinelRef" style="height: 1px"></div>
-
-        <div v-if="isLoading" class="loading">
-          <span>加载中...</span>
-        </div>
-        <div v-if="!hasMore && plansList.length > 0" class="no-more">
-          <span>暂无更多应急预案</span>
+        <div class="plan-item" v-for="item in activePlanDetailList" :key="item.id" @click="handleJumpToDetail(item.id)">
+          <span class="plan-title">{{ item.planName }}</span>
+          <span class="plan-dept">{{ item.deptName }}</span>
         </div>
       </div>
     </div>
@@ -38,91 +30,50 @@
 </template>
 
 <script setup lang="ts">
-  import { onMounted, ref } from 'vue';
-  import { useInfiniteScroll } from '../hooks/useInfiniteScroll';
+  import { onMounted, ref, watch } from 'vue';
+  import { useRouter } from 'vue-router';
+  import { useEmergencyPlanHook } from '@/views/emergency/emergency-plan/src/hook';
+  import { PlanInfoList, PlanDetail, getOverviewEmergencyPlan } from '@/api/emergency-overview';
 
-  interface Item {
-    id: number;
-    title: string;
-    dept: string;
-  }
+  const { getPlanTypeDict, getPlanType } = useEmergencyPlanHook();
 
-  const planTabs = ref([{ name: '综合预案' }, { name: '专项预案' }, { name: '现场处置方案' }]);
-  const activeTab = ref('综合预案');
-  const activeSum = ref(0);
-
-  const plansList = ref<Item[]>([]);
-  const page = ref<number>(1);
-  // const pageSize = ref<number>(10);
-  const hasMore = ref<boolean>(true);
-  const isLoading = ref<boolean>(false);
-  const sentinelRef = ref<HTMLElement | null>(null); // 哨兵元素引用
-
-  // 请求后端接口获取数据
-  const fetchPlanData = async () => {
-    if (isLoading.value || !hasMore.value) return;
-
-    isLoading.value = true;
-
-    try {
-      // TODO: 替换为实际的 API 请求
-      // const res = await axios.get('/api/items', {
-      //   params: {
-      //     page: page.value,
-      //     pageSize: pageSize.value,
-      //     tab: activeTab.value,
-      //   },
-      // });
-      const res = {
-        data: {
-          items: [
-            { id: 1, title: '火灾爆炸事故专项应急预案', dept: '安全与应急管理部' },
-            { id: 2, title: '消防安全综合应急预案', dept: '安全与应急管理部' },
-            { id: 3, title: '化学品泄漏应急处置方案', dept: '环境保护部' },
-            { id: 4, title: '自然灾害应急响应预案', dept: '自然灾害管理局' },
-            { id: 5, title: '公共卫生事件应急预案', dept: '公共卫生部门' },
-            { id: 6, title: '交通事故应急处理方案', dept: '交通运输部' },
-            { id: 7, title: '电力系统故障应急预案', dept: '电力公司' },
-            { id: 8, title: '信息安全事件应急预案', dept: '信息技术部' },
-            { id: 9, title: '食品安全事故应急预案', dept: '食品药品监督管理局' },
-            { id: 10, title: '建筑工程事故应急预案', dept: '建设局' },
-          ],
-        },
-      };
-
-      const newItems = res.data.items as Item[];
-
-      if (newItems.length === 0) {
-        hasMore.value = false;
-      } else {
-        plansList.value.push(...newItems);
-        page.value++;
-      }
-    } catch (error) {
-      console.error('请求失败:', error);
-    } finally {
-      isLoading.value = false;
-    }
-  };
+  const router = useRouter();
+  const loading = ref(true);
+
+  const emergencyPlanList = ref<PlanInfoList[]>([]);
+  const activeTab = ref('');
+  const activePlanDetailList = ref<PlanDetail[]>([]);
 
-  // 切换标签时重置状态
   const handleChangeTab = (tabName: string) => {
     activeTab.value = tabName;
-    hasMore.value = true;
-    page.value = 1;
-    plansList.value = []; // 清空当前列表
-    fetchPlanData(); // 重新请求数据
   };
 
-  // 跳转到详情页
-  const handleJumpToDetail = (id: number) => {
-    // TODO: 替换为实际的路由跳转
-    console.log(`跳转到详情页,ID: ${id}`);
+  const handleJumpToDetail = (id: number | undefined) => {
+    router.push({
+      path: '/emergency-management/emergency-plan/plan-management-detail',
+      query: {
+        id: id,
+        type: 'view',
+      },
+    });
   };
 
+  watch(
+    () => activeTab.value,
+    () => {
+      activePlanDetailList.value =
+        emergencyPlanList.value.find((item) => item.planType === activeTab.value)?.planDetailList || [];
+    },
+    { immediate: true },
+  );
+
   onMounted(() => {
-    fetchPlanData();
-    useInfiniteScroll(sentinelRef, fetchPlanData);
+    getPlanTypeDict();
+    getOverviewEmergencyPlan().then((res) => {
+      emergencyPlanList.value = res.planInfoList;
+      activeTab.value = emergencyPlanList.value[0].planType;
+      loading.value = false;
+    });
   });
 </script>
 
@@ -231,11 +182,14 @@
       }
     }
 
-    .loading,
-    .no-more {
-      text-align: center;
-      padding: 10px;
-      color: gray;
+    .plan-list-no-data {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      font-size: 14px;
+      color: #666666;
     }
   }
 </style>

+ 53 - 98
src/views/emergency/overview/components/EmergencyProcedure.vue

@@ -4,7 +4,12 @@
       <span class="line"></span>
       <span class="title">应急处置</span>
     </div>
-    <YearSelector class="year-selector" :years="yearsList" :defaultYear="defaultYear" @year-change="handleYearChange" />
+    <YearSelector
+      class="year-selector"
+      :years="yearsList"
+      :default-year="yearSelected"
+      @year-change="handleYearChange"
+    />
     <div class="event-counts">
       <div v-if="yearSelected === defaultYear">
         <span>本月应急事件</span>
@@ -15,126 +20,73 @@
         <span class="event-count">{{ yearCount }}</span>
       </div>
     </div>
-    <EmergencyEventsChart class="events-echarts" />
+    <EmergencyEventsChart class="events-echarts" :data="chartData" v-loading="loading" />
     <div class="container-title">
       <span class="line"></span>
       <span class="title">应急事件</span>
     </div>
-    <div class="events-list">
+    <div class="events-list" v-loading="loading">
       <div class="event-item" v-for="(item, index) in eventsList" :key="index">
-        <div class="event-name">{{ item.name }}</div>
-        <div class="event-time">{{ item.time }}</div>
-        <div class="event-status" v-if="item.status">{{ item.status }}</div>
-      </div>
-
-      <!-- 哨兵 -->
-      <div ref="sentinelRef" style="height: 1px"></div>
-
-      <div v-if="isLoading" class="loading">
-        <span>加载中...</span>
-      </div>
-      <div v-if="!hasMore && eventsList.length > 0" class="no-more">
-        <span>暂无更多应急预案</span>
+        <div class="event-name" :title="item.eventName">{{ item.eventName }}</div>
+        <div class="event-time">{{ item.startTime }}</div>
+        <div class="event-status" v-if="item.status === EMERGENCY_PROCEDURE_STATUS.INPROGRESS">启动中</div>
       </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { onMounted, ref } from 'vue';
+  import { onMounted, ref, watch } from 'vue';
   import YearSelector from './YearSelector.vue';
   import EmergencyEventsChart from './EmergencyEventsChart.vue';
-  import { useInfiniteScroll } from '../hooks/useInfiniteScroll';
+  import { EMERGENCY_PROCEDURE_STATUS } from '@/views/emergency/emergency-procedure/constant';
+  import {
+    QueryEmergencyHandleOverviewRes,
+    HandleTaskByMonthList,
+    HandleTaskList,
+    getOverviewEmergencyProcedure,
+  } from '@/api/emergency-overview';
 
-  interface EventItem {
-    name: string;
-    time: string;
-    status?: string; // 是否是启动中状态
-  }
+  const loading = ref(true);
 
-  const defaultYear = ref<number>(new Date().getFullYear()); // 默认为当前年份
-  const yearsList = ref<number[]>([2021, 2022, 2023, 2024, 2025]);
-  const yearSelected = ref<number>(defaultYear.value);
-
-  const monthCount = ref<number>(0);
-  const yearCount = ref<number>(0);
-
-  const eventsList = ref<EventItem[]>([]);
-  const page = ref<number>(1);
-  const pageSize = ref<number>(10);
-  const hasMore = ref<boolean>(true);
-  const isLoading = ref<boolean>(false);
-  const sentinelRef = ref<HTMLElement | null>(null); // 哨兵元素引用
-
-  const params = ref({
-    year: yearSelected.value,
-    page: page.value,
-    pageSize: pageSize.value,
-  });
+  const emergencyProcedureData = ref<QueryEmergencyHandleOverviewRes[]>([]);
+  const chartData = ref<HandleTaskByMonthList[]>([]);
+  const eventsList = ref<HandleTaskList[]>([]);
 
-  // 请求后端接口获取数据
-  const fetchExerciseData = async () => {
-    if (isLoading.value || !hasMore.value) return;
-
-    isLoading.value = true;
-
-    try {
-      // TODO: 替换为实际的 API 请求
-      // const res = await axios.get('/api/items', params.value);
-      const res = {
-        data: {
-          items: [
-            { name: '事件1', time: '2023-10-10', status: '启动中' },
-            { name: '事件2', time: '2023-10-10', status: '启动中' },
-            { name: '事件3', time: '2023-10-10', status: '启动中' },
-            { name: '事件4', time: '2023-10-10', status: '启动中' },
-            { name: '事件5', time: '2023-10-10' },
-            { name: '事件6', time: '2023-10-10' },
-            { name: '事件7', time: '2023-10-10' },
-            { name: '事件8', time: '2023-10-10' },
-            { name: '事件9', time: '2023-10-10' },
-            { name: '事件10', time: '2023-10-10' },
-            { name: '事件11', time: '2023-10-10' },
-            { name: '事件12', time: '2023-10-10' },
-          ],
-        },
-      };
-
-      // TODO: 赋值:本月应急事件,本年应急事件,事件列表,折线图
-      const newItems = res.data.items as EventItem[];
-
-      if (newItems.length === 0) {
-        hasMore.value = false;
-      } else {
-        eventsList.value.push(...newItems);
-        page.value++;
-      }
-    } catch (error) {
-      console.error('请求失败:', error);
-    } finally {
-      isLoading.value = false;
-    }
-  };
+  const defaultYear = ref<number>(new Date().getFullYear()); // 默认为当前年份
+  const yearsList = ref<number[]>([]);
+  const yearSelected = ref<number | undefined>();
 
-  // 切换年份的状态重置
-  const resetStateAndFetchData = () => {
-    hasMore.value = true;
-    page.value = 1;
-    eventsList.value = []; // 清空当前列表
-    monthCount.value = 0;
-    yearCount.value = 0;
-    console.log('params', params.value);
-    fetchExerciseData();
-  };
+  const monthCount = ref<number | undefined>(0);
+  const yearCount = ref<number | undefined>(0);
 
   const handleYearChange = (year: number) => {
     yearSelected.value = year;
-    resetStateAndFetchData();
   };
 
+  watch(
+    () => yearSelected.value,
+    (newValue) => {
+      loading.value = true;
+      const targetYearData = emergencyProcedureData.value.find((item) => item.year === newValue);
+      if (!targetYearData) return;
+      monthCount.value = targetYearData.currentMonthCount;
+      yearCount.value = targetYearData.currentYearCount;
+      chartData.value = targetYearData.handleTaskByMonthList;
+      eventsList.value = targetYearData.handleTaskList;
+      loading.value = false;
+    },
+    { immediate: true },
+  );
+
   onMounted(() => {
-    fetchExerciseData();
-    useInfiniteScroll(sentinelRef, fetchExerciseData);
+    getOverviewEmergencyProcedure().then((res) => {
+      emergencyProcedureData.value = res.handleTaskByYearList;
+      if (emergencyProcedureData.value && emergencyProcedureData.value.length > 0) {
+        yearsList.value = emergencyProcedureData.value.map((item) => item.year);
+        yearSelected.value = emergencyProcedureData.value[emergencyProcedureData.value.length - 1].year;
+      }
+    });
   });
 </script>
 
@@ -216,6 +168,9 @@
           font-size: 16px;
           color: #333333;
           margin-bottom: 8px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
         }
 
         .event-time {

+ 1 - 1
src/views/emergency/overview/components/YearSelector.vue

@@ -20,7 +20,7 @@
 
   interface Props {
     years: number[]; // 年份数组
-    defaultYear: number; // 默认年份
+    defaultYear: number | undefined; // 默认年份
   }
 
   const props = withDefaults(defineProps<Props>(), {