|
@@ -4,51 +4,55 @@
|
|
|
<span class="line"></span>
|
|
<span class="line"></span>
|
|
|
<span class="title">应急演练</span>
|
|
<span class="title">应急演练</span>
|
|
|
</div>
|
|
</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-tabs">
|
|
|
<div
|
|
<div
|
|
|
class="exercise-tab"
|
|
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>
|
|
</div>
|
|
|
<div class="exercise-counts">
|
|
<div class="exercise-counts">
|
|
|
<div>
|
|
<div>
|
|
|
<span>年度演练计划</span>
|
|
<span>年度演练计划</span>
|
|
|
- <span class="exercise-count">{{ yearCount }}</span>
|
|
|
|
|
|
|
+ <span class="exercise-count">{{
|
|
|
|
|
+ exerciseCountList.find((item) => item.drillScope === activeTab)?.drillPlanCount
|
|
|
|
|
+ }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div>
|
|
<div>
|
|
|
<span>已完成演练</span>
|
|
<span>已完成演练</span>
|
|
|
- <span class="exercise-count">{{ finishedCount }}</span>
|
|
|
|
|
|
|
+ <span class="exercise-count">{{
|
|
|
|
|
+ exerciseCountList.find((item) => item.drillScope === activeTab)?.completedDrillCount
|
|
|
|
|
+ }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="exercise-list">
|
|
<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-item" v-for="item in exercisesList" :key="item.id" @click="handleJumpToDetail(item.id)">
|
|
|
<div class="exercise-info">
|
|
<div class="exercise-info">
|
|
|
- <span class="exercise-title">{{ item.title }}</span>
|
|
|
|
|
|
|
+ <span class="exercise-title">{{ item.drillContent }}</span>
|
|
|
<div class="exercise-info-content">
|
|
<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>
|
|
</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>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -56,119 +60,89 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<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 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) => {
|
|
const handleYearChange = (year: number) => {
|
|
|
yearSelected.value = year;
|
|
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) => {
|
|
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>
|
|
</script>
|
|
|
|
|
|
|
@@ -278,7 +252,6 @@
|
|
|
display: flex;
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
justify-content: space-between;
|
|
|
position: relative;
|
|
position: relative;
|
|
|
- // padding-right: 56px;
|
|
|
|
|
|
|
|
|
|
.exercise-title {
|
|
.exercise-title {
|
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
@@ -304,9 +277,6 @@
|
|
|
background: #52c41a;
|
|
background: #52c41a;
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
|
color: #ffffff;
|
|
color: #ffffff;
|
|
|
- // position: absolute;
|
|
|
|
|
- // top: 3px;
|
|
|
|
|
- // right: 0;
|
|
|
|
|
margin-left: 5px;
|
|
margin-left: 5px;
|
|
|
transform: skew(-15deg, 0deg);
|
|
transform: skew(-15deg, 0deg);
|
|
|
|
|
|
|
@@ -322,6 +292,9 @@
|
|
|
font-weight: 400;
|
|
font-weight: 400;
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
color: #666666;
|
|
color: #666666;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -336,13 +309,16 @@
|
|
|
margin-bottom: 0;
|
|
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>
|
|
</style>
|