feat: 气象预警响应

This commit is contained in:
niedongsheng 2026-04-18 10:30:07 +08:00
parent 285bc95b91
commit 7eef4cfd92
5 changed files with 331 additions and 111 deletions

View File

@ -1,6 +1,6 @@
<template>
<div class="card-item">
<slot name="header">
<slot name="header" v-if="title">
<div class="header" :style="{marginBottom: titleGap + 'px'}">
<div class="header-title">{{ title }}</div>
<div class="header-extra" v-if="$slots.headerExtra">

View File

@ -52,7 +52,7 @@ const eventTypeOptions = [
//
const handleClickBack = () => {
router.replace('/disasterManagement')
router.go(-1)
}
</script>

View File

@ -1,20 +1,26 @@
<template>
<PageContainer title="气象预警" @click-back="handleClickBack">
<SearchInput v-model="searchValue" />
<SearchInput v-model="searchValue" @update:modelValue="handleSearch" />
<CurrentSite />
<div class="list-panel">
<CardItem v-for="(item, index) in list" :key="index" :title="item.area" @click="handleClickItem(item)">
<CardItem
v-for="(item, index) in list"
:key="index"
:class="['card-item', item.status]"
:title="item.title"
@click="handleClickItem(item)"
>
<template #headerExtra>
<span class="red-ball"></span>
<span v-if="item.status === 'unread'" class="red-ball"></span>
</template>
<!-- 发布时间 block -->
<div class="time-block">
<div class="time-box">
<span class="time-label-text">发布时间</span>
<span class="time-value-text">{{ item.publishTime }}</span>
<span class="time-value-text">{{ item.onset }}</span>
</div>
<van-icon class="jump-icon" name="arrow" />
</div>
@ -27,67 +33,89 @@
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import PageContainer from '@/components/PageContainer.vue'
import SearchInput from '@/components/SearchInput.vue'
import CurrentSite from '@/components/CurrentSite.vue'
import CardItem from '@/components/CardItem.vue'
import EmptyBox from '@/components/EmptyBox.vue'
import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import PageContainer from '@/components/PageContainer.vue';
import SearchInput from '@/components/SearchInput.vue';
import CurrentSite from '@/components/CurrentSite.vue';
import CardItem from '@/components/CardItem.vue';
import EmptyBox from '@/components/EmptyBox.vue';
import warningMessageMock from './mock.json';
import { request } from '@shared/utils/request';
import { showToast } from 'vant';
const router = useRouter()
const router = useRouter();
const route = useRoute();
onMounted(() => {
getData()
})
getData();
});
//
const searchValue = ref('')
watch(
() => searchValue.value,
(newVal, oldVal) => {
if (newVal !== oldVal) {
getData(newVal)
}
}
)
const searchValue = ref('');
const handleSearch = value => {
getData(value);
};
//
const list = ref([
{
area: '合川区',
level: '红色气象预警',
publishTime: '2026/01/10 20:29'
},
{
area: '万州区',
level: '红色气象预警',
publishTime: '2025/10/10 20:29'
},
{
area: '涪陵区',
level: '红色气象预警',
publishTime: '2025/10/10 20:29'
const list = ref([]);
const getData = async (keyword = '') => {
const normalizedKeyword = keyword.trim();
if (route.query.mock) {
list.value = warningMessageMock.filter(item => {
if (!normalizedKeyword) return true;
return item.title.includes(normalizedKeyword);
});
return;
}
])
const getData = async () => {
try {
const result = await request({
url: '/snow-ops-platform/weather-warning/notice',
method: 'get',
params: {
pageSize: 999,
keyword: normalizedKeyword,
},
});
list.value = result.data.records;
} catch (_error) {
showToast('获取数据失败,请稍后重试');
list.value = [];
}
};
}
const markNoticeRead = async id => {
await request({
url: `/snow-ops-platform/weather-warning/notice/${id}/_read`,
method: 'put',
});
};
const handleClickItem = (item) => {
const handleClickItem = async item => {
try {
await markNoticeRead(item.id);
item.status = 'read';
} catch (_error) {
showToast('更新已读状态失败');
}
window.warningMessageDetail = item;
router.push({
path: '/warningMessage-detail',
})
}
name: 'WarningMessageDetail',
query: {
id: item.id,
mock: route.query.mock,
},
});
};
const handleClickBack = () => {
router.push('/')
}
router.push('/');
};
</script>
<style lang="scss" scoped>
@ -98,10 +126,6 @@ const handleClickBack = () => {
gap: 8px;
}
.card-item {
padding-right: 15px;
}
/* ==================== Block 层级 ==================== */
.time-block {
display: flex;
@ -126,9 +150,13 @@ const handleClickBack = () => {
border-radius: 100%;
}
.card-item {
padding-right: 15px;
}
.jump-icon {
font-size: 16px;
color: rgb(102, 102, 102, .4);
color: rgb(102, 102, 102, 0.4);
}
.time-label-text {
@ -142,4 +170,4 @@ const handleClickBack = () => {
font-size: 14px;
color: #666666;
}
</style>
</style>

View File

@ -1,95 +1,154 @@
<template>
<PageContainer class="page-container" title="预警详情" @click-back="handleClickBack">
<CurrentSite />
<CurrentSite />
<PanelHeader title="气象预警" />
<div class="list-panel margin">
<CardItem class="card-item" v-for="(item, index) in list" :key="index" :title="item.area" titleGap="10">
<CardItem class="card-item" :title="detail?.title">
<template #content>
<div class="time-block">
<div class="time-box">
<span class="time-label-text">发布时间</span>
<span class="time-value-text">{{ item.publishTime }}</span>
<span class="time-value-text">{{ detail?.onset }}</span>
</div>
</div>
<div class="desc-block">
<div class="desc-text">{{ item.content }}</div>
<div class="desc-text">{{ detail?.warningContent }}</div>
</div>
<van-icon class="jump-icon" name="arrow" />
</template>
</CardItem>
<!-- 空状态提示 -->
<EmptyBox v-if="list.length === 0" placeholder="暂无相关预警信息" />
<EmptyBox v-if="!detail?.warningContent" placeholder="暂无相关预警信息" />
</div>
<PanelHeader title="防御措施" />
<div class="list-panel">
<CardItem v-for="(item, index) in list" :key="index" :title="item.area">
<div class="method-block">
<div class="method-text">{{ item.method }}</div>
</div>
<van-icon class="jump-icon" name="arrow" />
<PanelHeader title="工作建议" />
<div class="list-panel margin">
<CardItem class="card-item">
<template #content>{{ detail?.workSuggestion }}</template>
</CardItem>
<!-- 空状态提示 -->
<EmptyBox v-if="list.length === 0" placeholder="暂无相关防御措施" />
<EmptyBox v-if="!detail?.workSuggestion" placeholder="暂无相关防御措施" />
</div>
<PanelHeader title="影响范围" />
<CardItem class="card-item impact-card">
<template #content>
<div class="impact-tab-block">
<div
class="impact-tab-item"
:class="{ active: activeImpactTab === item.value }"
v-for="item in impactTabs"
:key="item.value"
@click="activeImpactTab = item.value"
>
{{ item.label }}
</div>
</div>
<div class="impact-table" v-if="currentImpactList.length">
<div class="impact-table-header" :class="`cols-${currentImpactColumns.length}`">
<div class="impact-table-cell" v-for="column in currentImpactColumns" :key="column.key">
{{ column.label }}
</div>
</div>
<div class="impact-table-body">
<div class="impact-table-row" :class="`cols-${currentImpactColumns.length}`" v-for="(item, index) in currentImpactList" :key="index">
<div class="impact-table-cell" v-for="column in currentImpactColumns" :key="column.key">
{{ item[column.key] }}
</div>
</div>
</div>
</div>
<EmptyBox v-else placeholder="暂无相关影响范围" />
</template>
</CardItem>
<div class="footer-panel">
<van-button block type="primary" @click="handleClickBack()">我已知晓</van-button>
<!-- <van-button block type="primary" @click="handleClickBack()">我已知晓</van-button> -->
<van-button block type="primary" @click="goToHandle()">我要响应</van-button>
</div>
</PageContainer>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import PageContainer from '@/components/PageContainer.vue'
import CurrentSite from '@/components/CurrentSite.vue'
import PanelHeader from '@/components/PanelHeader.vue'
import CardItem from '@/components/CardItem.vue'
import EmptyBox from '@/components/EmptyBox.vue'
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import PageContainer from '@/components/PageContainer.vue';
import CurrentSite from '@/components/CurrentSite.vue';
import PanelHeader from '@/components/PanelHeader.vue';
import CardItem from '@/components/CardItem.vue';
import EmptyBox from '@/components/EmptyBox.vue';
const router = useRouter()
const router = useRouter();
const detail = ref({
id: null,
status: '',
title: '',
onset: '',
warningContent: '',
workSuggestion: '',
affectedPoints: [],
affectedProjects: [],
});
const activeImpactTab = ref('point');
const impactTabs = [
{ label: '影响点', value: 'point' },
{ label: '影响项目', value: 'project' },
];
onMounted(() => {
getData()
})
getData();
});
//
const list = ref([
{
area: '合川区',
level: '红色气象预警',
content: '今天,北京将进入大雪,请做好',
publishTime: '2026/01/10 20:29',
method: '立即启动防汛Ⅰ级应急响应,立即转移危险区'
},
{
area: '万州区',
level: '红色气象预警',
publishTime: '2025/10/10 20:29',
method: '立即启动防汛Ⅰ级应急响应,立即转移危险区'
},
{
area: '涪陵区',
level: '红色气象预警',
publishTime: '2025/10/10 20:29',
method: '立即启动防汛Ⅰ级应急响应,立即转移危险区'
const impactColumnsMap = {
point: [
{ label: '序号', key: 'index' },
{ label: '影响区域', key: 'affectedCounty' },
{ label: '线路编号', key: 'roadCode' },
{ label: '类型', key: 'type' },
{ label: '桩号', key: 'station' },
],
project: [
{ label: '序号', key: 'index' },
{ label: '影响区域', key: 'affectedCounty' },
{ label: '项目名称', key: 'projectName' },
{ label: '驻地名称', key: 'siteName' },
{ label: '线路编号', key: 'roadCode' },
],
};
const currentImpactColumns = computed(() => impactColumnsMap[activeImpactTab.value] || []);
const impactListMap = computed(() => ({
point: detail.value.affectedPoints,
project: detail.value.affectedProjects,
}));
const currentImpactList = computed(() => {
return impactListMap.value[activeImpactTab.value].map((item, index) => ({
index: index + 1,
...item,
}));
});
const getData = async () => {
if (!window.warningMessageDetail) {
return;
}
])
const getData = async () => {}
detail.value = window.warningMessageDetail;
};
const handleClickBack = () => {
router.go(-1)
}
router.go(-1);
};
const goToHandle = () => {
router.push({path: '/warningMessageHandle'})
}
router.push({ path: '/disasterReport' });
};
</script>
<style lang="scss" scoped>
@ -164,7 +223,7 @@ const goToHandle = () => {
font-weight: 400;
font-size: 14px;
color: #666666;
line-height: 13px;
line-height: 20px;
}
.method-text {
font-weight: 500;
@ -172,4 +231,75 @@ const goToHandle = () => {
color: #4a4a4a;
line-height: 20px;
}
.impact-card {
padding: 0;
}
.impact-tab-block {
display: grid;
grid-template-columns: repeat(2, 1fr);
border: 1px solid #d9d9d9;
overflow: hidden;
margin-bottom: 16px;
}
.impact-tab-item {
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
color: #4a4a4a;
background: #ffffff;
& + .impact-tab-item {
border-left: 1px solid #d9d9d9;
}
&.active {
color: #ffffff;
background: linear-gradient(180deg, #2f79f6 0%, #1f66e0 100%);
}
}
.impact-table {
overflow: hidden;
}
.impact-table-header,
.impact-table-row {
display: grid;
}
.cols-5 {
grid-template-columns: 52px 1fr 1fr 1fr 1.2fr;
}
.impact-table-header {
background: linear-gradient(180deg, #e5e5e5 0%, #d8d8d8 100%);
}
.impact-table-row {
background: #ffffff;
& + .impact-table-row {
border-top: 1px solid #f0f0f0;
}
}
.impact-table-cell {
min-height: 52px;
display: flex;
align-items: center;
justify-content: center;
padding: 8px 6px;
text-align: center;
font-weight: 500;
font-size: 14px;
color: #4a4a4a;
line-height: 20px;
word-break: break-word;
}
</style>

View File

@ -0,0 +1,62 @@
[
{
"id": 1,
"status": "unread",
"title": "潼南区气象台发布暴雨蓝色预警[Ⅳ级/一般]",
"onset": "2026/01/10 20:29",
"warningContent": "区气象台发布暴雨红色预警按照相关要求启动I级防御响应并请及时关注地质、水文等风险提示信息落实主动封闭管控/“关停撤转”措施。",
"workSuggestion": "立即启动防汛Ⅰ级应急响应,立即转移危险区,加强沿线桥梁、隧道及在建项目巡查。",
"affectedPoints": [
{
"affectedCounty": "巫溪县",
"roadCode": "G242",
"type": "桥梁",
"station": "336.800-336.850"
},
{
"affectedCounty": "万州区",
"roadCode": "G242",
"type": "隧道",
"station": "336.800-336.850"
}
],
"affectedProjects": [
{
"affectedCounty": "巫溪县",
"projectName": "G242灾害防治工程",
"siteName": "巫溪驻地",
"roadCode": "G242"
},
{
"affectedCounty": "万州区",
"projectName": "G242改扩建工程",
"siteName": "万州驻地",
"roadCode": "G242"
}
]
},
{
"id": 2,
"status": "read",
"title": "潼南区气象台发布暴雨蓝色预警[Ⅳ级/一般]",
"onset": "2026/01/09 07:10",
"warningContent": "预计未来12小时局地将出现能见度小于500米的大雾天气请注意行车安全并做好道路巡查。",
"workSuggestion": "加强重点路段值守,及时发布诱导信息,视情采取限速、分流等措施。",
"affectedPoints": [
{
"affectedCounty": "涪陵区",
"roadCode": "S105",
"type": "高边坡",
"station": "K12+300-K13+100"
}
],
"affectedProjects": [
{
"affectedCounty": "涪陵区",
"projectName": "S105养护提升工程",
"siteName": "涪陵驻地",
"roadCode": "S105"
}
]
}
]