feat: 气象预警

This commit is contained in:
niedongsheng 2026-04-02 15:39:27 +08:00
parent d769327701
commit f24ce93621
12 changed files with 774 additions and 686 deletions

View File

@ -0,0 +1,62 @@
<template>
<div class="card-item">
<slot name="header">
<div class="header" :style="{marginBottom: titleGap + 'px'}">
<div class="header-title">{{ title }}</div>
<div class="header-extra" v-if="$slots.headerExtra">
<slot name="headerExtra"></slot>
</div>
</div>
</slot>
<div class="content" v-if="$slots.content" :style="{gap: contentGap + 'px'}">
<slot name="content"></slot>
</div>
<slot />
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
titleGap: {
type: [Number, String],
default: 16
},
contentGap: {
type:[ Number, String],
default: 8
}
})
</script>
<style scoped lang="scss">
.card-item {
position: relative;
width: 100%;
padding: 21px 50px 17px 10px;
background-color: #fff;
border-radius: 8px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.header-title {
font-weight: 500;
font-size: 15px;
color: #4a4a4a;
line-height: 16px;
}
.content {
display: flex;
flex-direction: column;
}
</style>

View File

@ -0,0 +1,39 @@
<!-- 当前站点 -->
<template>
<div class="current-site">
<van-icon class="position-icon" name="location-o" />
<span class="site-name">当前站点{{ name || yhzStore.getYHZInfo?.mc }}</span>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useYHZStore } from '../stores/yhzStore';
const yhzStore = useYHZStore()
const props = defineProps({
name: {
type: String,
default: ''
}
})
</script>
<style scoped lang="scss">
.current-site {
display: flex;
align-items: center;
height: 50px;
background-color: transparent;
padding: 0 5px;
color: #979797;
}
.position-icon {
margin-right: 5px;
font-size: 16px;
}
.site-name {
font-size: 13px;
}
</style>

View File

@ -0,0 +1,24 @@
<template>
<div class="empty">
{{ props.placeholder }}
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
const props = defineProps({
placeholder: {
type: String,
default: '暂无相关信息'
}
});
</script>
<style scoped lang="scss">
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 30px;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="page-container">
<van-nav-bar title="气象预警" fixed left-arrow @click-left="onClickLeft" />
<div class="page-content-wrapper">
<slot></slot>
</div>
<div v-if="$slots.footer" class="page-footer-wrapper"><slot name="footer" /></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const emit = defineEmits(['back'])
const onClickLeft = () => {
emit('click-back')
}
</script>
<style scoped lang="scss">
.page-container {
display: flex;
flex-direction: column;
padding-top: var(--van-nav-bar-height); /* 自动匹配导航栏高度 */
padding-left: 8px;
padding-right: 8px;
width: 100dvw;
height: 100dvh;
background-color: #f6f6f7;
}
.page-content-wrapper {
flex: 1;
min-height: 0;
overflow: auto;
}
.page-footer-wrapper {
padding-top: 10px;
flex-shrink: 0;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div class="panel-header">
<span class="icon"></span>
<span class="title-text">{{ title }}</span>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
}
})
</script>
<style scoped lang="scss">
.panel-header {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.icon {
display: inline-block;
width: 5px;
height: 18px;
background: linear-gradient(180deg, #91c4f1 0%, #5892e0 100%);
border-radius: 2px;
margin-right: 4px;
}
.title-text {
font-weight: 500;
font-size: 16px;
color: #4a4a4a;
line-height: 16px;
}
</style>

View File

@ -0,0 +1,98 @@
<template>
<div class="search-input">
<div class="input-wrapper">
<div class="input-block">
<van-icon class="search-icon" name="search" />
<input class="inner-input" v-model="modelValue" :placeholder="placeholder" />
<van-icon class="close-icon" name="clear" v-if="modelValue !== ''" @click="modelValue = ''" />
</div>
<!-- 占位符 -->
<!-- <div class="placeholder-block" v-if="modelValue === '111'">
<van-icon class="search-icon" name="search" />
<span class="placeholder-text">{{ placeholder }}</span>
</div> -->
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
const modelValue = defineModel('modelValue')
const props = defineProps({
placeholder: {
type: String,
default: '请输入关键词'
}
})
</script>
<style scoped lang="scss">
.search-input {
position: relative;
height: 40px;
width: 100%;
margin-top: 8px;
box-sizing: border-box;
}
.input-wrapper {
width: 100%;
height: 100%;
background-color: #fff;
border-radius: 4px;
}
.input-block {
position: relative;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
padding: 0 20px;
box-sizing: border-box;
.search-icon, .close-icon {
position: absolute;
top: 50%;
left: 20px;
transform: translateY(-50%);
font-size: 20px;
color: #9b9b9b;
}
.close-icon {
left: unset;
right: 20px;
}
}
.inner-input {
outline: none;
border: none;
padding: 0;
margin: 0;
height: 100%;
text-align: center;
font-size: 14px;
}
.placeholder-block {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
pointer-events: none;
.search-icon {
font-size: 20px;
margin-right: 3px;
color: #9b9b9b;
}
.placeholder-text {
color: #9b9b9b;
}
}
</style>

View File

@ -72,10 +72,15 @@ const routes = [
component: () => import('../views/WarningMessage/WarningMessage.vue')
},
{
path: '/warningMessage-detail/:data',
path: '/warningMessage-detail',
name: 'WarningMessageDetail',
component: () => import('../views/WarningMessage/WarningMessageDetail.vue')
},
{
path: '/warningMessageHandle',
name: 'WarningMessageHandle',
component: () => import('../views/WarningMessage/WarningMessageHandle.vue')
}
]
const router = createRouter({

View File

@ -0,0 +1,29 @@
import { defineStore } from 'pinia'
export const useYHZStore = defineStore('config', {
state: () => ({
yhzInfo: null
}),
getters: {
getYHZInfo: (state) => {
if(state.yhzInfo) {
return state.yhzInfo
}
try {
const yhzInfo = JSON.parse(localStorage.getItem('yhzInfo'))
if(yhzInfo) {
return yhzInfo
}
} catch (error) {
console.log(error)
}
}
},
actions: {
setYHZInfo(yhzInfo) {
this.yhzInfo = yhzInfo
localStorage.setItem('yhzInfo', JSON.stringify(yhzInfo))
}
}
})

View File

@ -77,11 +77,13 @@ import "vant/es/toast/style";
import "vant/es/popup/style";
import { ref, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useYHZStore } from "../stores/yhzStore";
import { showToast } from "vant";
import { request } from "../../../shared/utils/request";
const router = useRouter();
const yhzinfo = ref({});
const yhzStore = useYHZStore();
const route = useRoute();
const token = route.query.token;
@ -96,6 +98,7 @@ const getYHZinfo = async () => {
});
if (res.code === "00000") {
yhzinfo.value = res.data[0];
yhzStore.setYHZInfo(res.data[0]);
} else {
throw new Error(res.message);
}

View File

@ -1,360 +1,145 @@
<template>
<div class="warning-message-page">
<!-- 顶部导航栏 -->
<div class="nav-bar">
<div class="back-btn" @click="goBack">
<van-icon name="arrow-left" />
</div>
<div class="title">气象预警</div>
<div class="placeholder"></div>
</div>
<PageContainer title="气象预警" @click-back="handleClickBack">
<SearchInput v-model="searchValue" />
<!-- 搜索栏 -->
<div class="search-section">
<div class="search-box">
<span class="search-text">关键词</span>
<van-icon name="search" class="search-icon" />
</div>
</div>
<CurrentSite />
<!-- 当前站点 -->
<div class="current-site">
<span class="site-label">当前站点</span>
<span class="site-name">{{ currentSite }}</span>
</div>
<div class="list-panel">
<CardItem v-for="(item, index) in list" :key="index" :title="item.area" @click="handleClickItem(item)">
<template #headerExtra>
<span class="red-ball"></span>
</template>
<!-- 预警列表 -->
<div class="warning-list">
<div
v-for="(item, index) in warningList"
:key="index"
class="warning-item"
:class="{ unread: !item.isRead }"
@click="viewDetail(item)"
>
<div class="item-content">
<div class="warning-title">{{ item.title }}</div>
<div class="warning-time">
<span class="time-label">发布时间</span>
<span class="time-value">{{ item.publishTime }}</span>
<!-- 发布时间 block -->
<div class="time-block">
<div class="time-box">
<span class="time-label-text">发布时间</span>
<span class="time-value-text">{{ item.publishTime }}</span>
</div>
<van-icon class="jump-icon" name="arrow" />
</div>
<div class="item-right">
<div v-if="!item.isRead" class="unread-dot"></div>
<van-icon name="arrow" class="arrow-icon" />
</div>
</div>
</div>
</CardItem>
<!-- 空状态 -->
<div v-if="warningList.length === 0" class="empty-state">
<van-icon name="warning-o" class="empty-icon" />
<div class="empty-text">暂无预警消息</div>
<!-- 空状态提示 -->
<EmptyBox v-if="list.length === 0" placeholder="暂无相关预警信息" />
</div>
</div>
</PageContainer>
</template>
<script setup>
import { ref } from "vue";
import { useRouter, useRoute } from "vue-router";
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'
const router = useRouter();
const route = useRoute();
const router = useRouter()
//
const currentSite = ref("李家坝仓库");
onMounted(() => {
getData()
})
//
const initSiteInfo = () => {
const { data } = route.params;
if (data) {
try {
const yhzinfo = JSON.parse(decodeURIComponent(data));
currentSite.value = yhzinfo.mc || "李家坝仓库";
} catch (e) {
console.error("解析站点信息失败", e);
//
const searchValue = ref('')
watch(
() => searchValue.value,
(newVal, oldVal) => {
if (newVal !== oldVal) {
getData(newVal)
}
}
};
)
initSiteInfo();
//
const warningList = ref([
const list = ref([
{
id: 1,
title: "合川区暴雨红色预警",
publishTime: "2025/10/10 20:29",
isRead: false,
level: "red",
type: "rain",
area: '合川区',
level: '红色气象预警',
publishTime: '2026/01/10 20:29'
},
{
id: 2,
title: "合川区红色气象预警",
publishTime: "2025/10/09 20:29",
isRead: true,
level: "red",
type: "weather",
area: '万州区',
level: '红色气象预警',
publishTime: '2025/10/10 20:29'
},
]);
{
area: '涪陵区',
level: '红色气象预警',
publishTime: '2025/10/10 20:29'
}
])
//
const goBack = () => {
router.back();
};
const getData = async () => {
//
const viewDetail = (item) => {
//
item.isRead = true;
//
}
const handleClickItem = (item) => {
router.push({
name: "WarningMessageDetail",
params: {
id: item.id,
data: route.params.data,
},
});
};
path: '/warningMessage-detail',
})
}
const handleClickBack = () => {
router.push('/')
}
</script>
<style lang="scss" scoped>
//
$primary-color: #409eff;
$text-color: #333;
$text-secondary: #666;
$text-tertiary: #999;
$bg-color: #f5f5f5;
$border-color: #e0e0e0;
$unread-color: #f56c6c;
.warning-message-page {
min-height: 100vh;
background-color: $bg-color;
padding-bottom: 20px;
//
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
background-color: #fff;
padding: 0 15px;
position: sticky;
top: 0;
z-index: 100;
.back-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: flex-start;
color: $text-color;
font-size: 20px;
}
.title {
flex: 1;
text-align: center;
font-size: 17px;
font-weight: 500;
color: $text-color;
}
.placeholder {
width: 40px;
}
}
//
.search-section {
padding: 12px 15px;
background-color: #fff;
.search-box {
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
background-color: #e8e8e8;
border-radius: 18px;
padding: 0 15px;
.search-text {
font-size: 14px;
color: $text-tertiary;
}
.search-icon {
font-size: 18px;
color: $text-tertiary;
}
}
}
//
.current-site {
padding: 10px 15px;
background-color: #fff;
text-align: center;
font-size: 13px;
border-bottom: 1px solid $border-color;
.site-label {
color: $text-secondary;
}
.site-name {
color: $primary-color;
}
}
//
.warning-list {
padding: 10px 15px;
.warning-item {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
&:active {
background-color: #f9f9f9;
}
.item-content {
flex: 1;
min-width: 0;
.warning-title {
font-size: 15px;
font-weight: 500;
color: $text-color;
margin-bottom: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.warning-time {
font-size: 12px;
color: $text-tertiary;
.time-label {
color: $text-secondary;
}
}
}
.item-right {
display: flex;
align-items: center;
gap: 8px;
margin-left: 10px;
.unread-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $unread-color;
flex-shrink: 0;
}
.arrow-icon {
font-size: 16px;
color: $text-tertiary;
flex-shrink: 0;
}
}
//
&.unread {
.warning-title {
font-weight: 600;
}
}
}
}
//
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
.empty-icon {
font-size: 48px;
color: $text-tertiary;
margin-bottom: 12px;
}
.empty-text {
font-size: 14px;
color: $text-secondary;
}
}
/* ==================== Panel 层级 ==================== */
.list-panel {
display: flex;
flex-direction: column;
gap: 8px;
}
// iPhone
@supports (padding-top: env(safe-area-inset-top)) {
.warning-message-page {
.nav-bar {
padding-top: env(safe-area-inset-top);
height: calc(44px + env(safe-area-inset-top));
}
}
.card-item {
padding-right: 15px;
}
//
@media screen and (max-width: 320px) {
.warning-message-page {
.warning-list {
.warning-item {
.item-content {
.warning-title {
font-size: 14px;
}
}
}
}
}
/* ==================== Block 层级 ==================== */
.time-block {
display: flex;
align-items: center;
justify-content: space-between;
}
@media screen and (min-width: 414px) {
.warning-message-page {
.nav-bar {
height: 48px;
.title {
font-size: 18px;
}
}
.warning-list {
.warning-item {
padding: 18px;
.item-content {
.warning-title {
font-size: 16px;
}
.warning-time {
font-size: 13px;
}
}
}
}
}
/* ==================== Box 层级 ==================== */
.time-box {
display: flex;
align-items: baseline;
flex-wrap: wrap;
}
</style>
/* ==================== Text / Num 层级 ==================== */
.red-ball {
display: inline-block;
width: 13px;
height: 13px;
background: linear-gradient(180deg, #fd4646 0%, #fb2222 100%);
box-shadow: 0px 1px 4px 0px rgba(91, 8, 8, 0.34);
border-radius: 100%;
}
.jump-icon {
font-size: 16px;
color: rgb(102, 102, 102, .4);
}
.time-label-text {
font-weight: 400;
font-size: 14px;
color: #666666;
}
.time-value-text {
font-weight: 400;
font-size: 14px;
color: #666666;
}
</style>

View File

@ -1,395 +1,175 @@
<template>
<div class="warning-detail-page">
<!-- 顶部导航栏 -->
<div class="nav-bar">
<div class="back-btn" @click="goBack">
<van-icon name="arrow-left" />
</div>
<div class="title">气象预警</div>
<div class="placeholder"></div>
<PageContainer class="page-container" title="预警详情" @click-back="handleClickBack">
<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">
<template #content>
<div class="time-block">
<div class="time-box">
<span class="time-label-text">发布时间</span>
<span class="time-value-text">{{ item.publishTime }}</span>
</div>
</div>
<div class="desc-block">
<div class="desc-text">{{ item.content }}</div>
</div>
<van-icon class="jump-icon" name="arrow" />
</template>
</CardItem>
<!-- 空状态提示 -->
<EmptyBox v-if="list.length === 0" placeholder="暂无相关预警信息" />
</div>
<!-- 当前站点 -->
<div class="current-site">
<span class="site-label">当前站点</span>
<span class="site-name">{{ currentSite }}</span>
</div>
<!-- 预警详情内容 -->
<div class="detail-content" v-if="warningDetail">
<!-- 预警标题区域 -->
<div class="warning-header">
<div class="header-title">气象预警</div>
</div>
<!-- 预警信息卡片 -->
<div class="info-card">
<div class="warning-name">{{ warningDetail.title }}</div>
<div class="publish-time">
<span class="time-label">发布时间</span>
<span class="time-value">{{ warningDetail.publishTime }}</span>
<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>
</div>
<van-icon class="jump-icon" name="arrow" />
</CardItem>
<!-- 预警描述 -->
<div class="description-card">
<div class="desc-text">{{ warningDetail.description }}</div>
</div>
<!-- 防御措施 -->
<div class="measures-section">
<div class="section-title">防御措施</div>
<div class="measures-card">
<div class="measures-text">{{ warningDetail.measures }}</div>
</div>
</div>
<!-- 空状态提示 -->
<EmptyBox v-if="list.length === 0" placeholder="暂无相关防御措施" />
</div>
<!-- 底部按钮 -->
<div class="bottom-action">
<van-button type="primary" block round @click="handleResponse">
立即响应
</van-button>
<div class="footer-panel">
<van-button block type="primary" @click="handleClickBack()">我已知晓</van-button>
<van-button block type="primary" @click="goToHandle()">我要响应</van-button>
</div>
</div>
</PageContainer>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { showToast } from "vant";
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'
const route = useRoute();
const router = useRouter();
//
const currentSite = ref("李家坝仓库");
//
const warningDetail = ref({
id: 1,
title: "合川区暴雨红色预警",
publishTime: "2025/10/10 20:29",
description:
"区气象台发布暴雨红色预警按照相关要求启动I级防御响应并请及时关注地质、水文等风险提示信息落实主动封闭管控/“关停撤转”措施",
measures:
"请立即按照2小时一次频率对你管养的重点路段/重点部位进行巡查,重点巡查较高及以上风险路段、涉灾隐患点、地质条件复杂路段、临河临崖路段/两区三厂、大型设施设备、取弃土(渣)场、砂石料场、涉水桥梁、富水隧道、围堰、支架脚手架、高切坡、滑坡处置等部位,重点关注涉水桥梁基础及墩台、不良地质隧道、隧道洞口边仰坡及侧切结构、高陡边坡支挡防护以及防排水设施,发现异常情况,立即向上报告,采取紧急排危、告警阻拦、吹哨撤转等措施,并及时报送工作开展情况。",
level: "red",
type: "rain",
});
//
const goBack = () => {
router.back();
};
//
const handleResponse = () => {
showToast({
message: "响应成功",
type: "success",
});
// API
console.log("立即响应", warningDetail.value);
};
const router = useRouter()
onMounted(() => {
// ID
const { id } = route.params;
if (id) {
console.log("获取预警详情 ID:", id);
// API
// fetchWarningDetail(id);
}
getData()
})
//
const { data } = route.params;
if (data) {
try {
const yhzinfo = JSON.parse(decodeURIComponent(data));
currentSite.value = yhzinfo.mc || "李家坝仓库";
} catch (e) {
console.error("解析站点信息失败", e);
}
//
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 getData = async () => {}
const handleClickBack = () => {
router.go(-1)
}
const goToHandle = () => {
router.push({path: '/warningMessageHandle'})
}
</script>
<style lang="scss" scoped>
//
$primary-color: #1989fa;
$text-color: #333;
$text-secondary: #666;
$text-tertiary: #999;
$bg-color: #f5f5f5;
$border-color: #e0e0e0;
$card-bg: #fff;
$section-title-color: #666;
.page-container {
position: relative;
padding-bottom: 74px;
}
.warning-detail-page {
min-height: 100vh;
background-color: $bg-color;
padding-bottom: 80px;
/* ==================== Panel 层级 ==================== */
.list-panel {
display: flex;
flex-direction: column;
gap: 8px;
//
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
background-color: #fff;
padding: 0 15px;
position: sticky;
top: 0;
z-index: 100;
.back-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: flex-start;
color: $text-color;
font-size: 20px;
}
.title {
flex: 1;
text-align: center;
font-size: 17px;
font-weight: 500;
color: $text-color;
}
.placeholder {
width: 40px;
}
}
//
.current-site {
padding: 10px 15px;
background-color: #fff;
text-align: center;
font-size: 13px;
border-bottom: 1px solid $border-color;
.site-label {
color: $text-secondary;
}
.site-name {
color: $primary-color;
}
}
//
.detail-content {
padding: 15px;
//
.warning-header {
margin-bottom: 12px;
.header-title {
font-size: 16px;
font-weight: 600;
color: $text-color;
position: relative;
padding-left: 12px;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: $primary-color;
border-radius: 2px;
}
}
}
//
.info-card {
background-color: $card-bg;
border-radius: 8px;
padding: 15px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
.warning-name {
font-size: 16px;
font-weight: 600;
color: $text-color;
margin-bottom: 10px;
line-height: 1.4;
}
.publish-time {
font-size: 13px;
color: $text-tertiary;
.time-label {
color: $text-secondary;
}
}
}
//
.description-card {
background-color: $card-bg;
border-radius: 8px;
padding: 15px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
.desc-text {
font-size: 14px;
color: $text-secondary;
line-height: 1.8;
text-align: justify;
}
}
//
.measures-section {
.section-title {
font-size: 16px;
font-weight: 600;
color: $text-color;
margin-bottom: 12px;
position: relative;
padding-left: 12px;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: $primary-color;
border-radius: 2px;
}
}
.measures-card {
background-color: $card-bg;
border-radius: 8px;
padding: 15px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
.measures-text {
font-size: 14px;
color: $text-secondary;
line-height: 1.8;
text-align: justify;
}
}
}
}
//
.bottom-action {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 10px 15px 20px;
background-color: #fff;
border-top: 1px solid $border-color;
.van-button {
height: 44px;
font-size: 16px;
}
&.margin {
margin-bottom: 20px;
}
}
// iPhone
@supports (padding-top: env(safe-area-inset-top)) {
.warning-detail-page {
.nav-bar {
padding-top: env(safe-area-inset-top);
height: calc(44px + env(safe-area-inset-top));
}
.bottom-action {
padding-bottom: calc(20px + env(safe-area-inset-bottom));
}
}
.footer-panel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 64px;
background: #ffffff;
box-shadow: 0px -5px 12px 0px rgba(200, 200, 200, 0.37);
opacity: 0.97;
display: flex;
align-items: center;
gap: 5px;
padding: 0 16px;
}
//
@media screen and (max-width: 320px) {
.warning-detail-page {
.detail-content {
.info-card {
.warning-name {
font-size: 15px;
}
}
.description-card,
.measures-section {
.desc-text,
.measures-text {
font-size: 13px;
}
}
}
}
/* ==================== Block 层级 ==================== */
.time-block {
display: flex;
align-items: center;
justify-content: space-between;
}
@media screen and (min-width: 414px) {
.warning-detail-page {
.nav-bar {
height: 48px;
/* ==================== Box 层级 ==================== */
.time-box {
display: flex;
align-items: baseline;
flex-wrap: wrap;
}
.title {
font-size: 18px;
}
}
/* ==================== Text 层级 ==================== */
.detail-content {
.warning-header {
.header-title {
font-size: 17px;
}
}
.jump-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 15px;
font-size: 16px;
color: rgb(102, 102, 102, 0.4);
}
.info-card {
padding: 18px;
.time-label-text {
font-weight: 400;
font-size: 14px;
color: #999999;
}
.warning-name {
font-size: 17px;
}
.publish-time {
font-size: 14px;
}
}
.description-card,
.measures-card {
padding: 18px;
.desc-text,
.measures-text {
font-size: 15px;
}
}
.measures-section {
.section-title {
font-size: 17px;
}
}
}
}
.time-value-text {
font-weight: 400;
font-size: 14px;
color: #999999;
}
.desc-text {
font-weight: 400;
font-size: 14px;
color: #666666;
line-height: 13px;
}
.method-text {
font-weight: 500;
font-size: 15px;
color: #4a4a4a;
line-height: 20px;
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<PageContainer class="page-container" title="预警详情" @click-back="handleClickBack">
<CurrentSite />
<div class="form-panel">
<van-field v-model="value" label="现场情况描述" placeholder="请输入备注" label-align="top" rows="3" type="textarea" />
<van-field class="mt-8" v-model="value" label="备注(非必填)" placeholder="请填写" input-align="right" />
<div class="image-upload-block">
<div class="label-box">
<span class="main-text">现场照片</span>
<span class="sub-text">最多上传6张</span>
</div>
<div class="com-">
<van-uploader
v-model="fileList"
@delete="handleDelete"
name="photos"
:file-list="fileList"
:file-type="['image/jpeg', 'image/png']"
:after-read="afterRead"
multiple
:max-count="6"
/>
</div>
</div>
</div>
<div class="footer-panel">
<van-button block type="primary" @click="confirm()">确认</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 { showToast, showLoadingToast, showImagePreview } from 'vant'
const router = useRouter()
//
const fileList = ref([])
onMounted(() => {
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 handleDelete = (file) => {
if (file.serverUrl) {
const index = editForm.photos.findIndex((p) => p.photoUrl === file.serverUrl)
if (index !== -1) {
editForm.photos.splice(index, 1)
}
}
}
//
const afterRead = async (file) => {
const toast = showLoadingToast({
message: '上传中...',
forbidClick: true,
duration: 0 // 0
})
try {
const formData = new FormData()
formData.append('file', file.file)
const res = await request({
url: '/snow-ops-platform/file/upload',
method: 'post',
data: formData
})
toast.close()
if (res.code === '00000') {
editForm.photos.push({ photoUrl: res.data })
const index = fileList.value.findIndex((f) => f.file === file.file)
if (index !== -1) {
fileList.value[index].serverUrl = res.data
}
console.log('editForm.photos', toRaw(editForm.photos))
console.log('fileList.value', fileList.value)
} else {
throw new Error(res.message)
}
} catch (error) {
toast.close()
showToast({
type: 'fail',
message: error.message
})
}
}
const getData = async () => {}
const handleClickBack = () => {
router.go(-1)
}
const confirm = () => {
router.push({ path: '/warningMessageHandle' })
}
</script>
<style lang="scss" scoped>
.page-container {
position: relative;
padding-left: 0;
padding-right: 0;
padding-bottom: 74px;
}
.mt-8 {
margin-top: 8px;
}
/* ==================== Panel 层级 ==================== */
.footer-panel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 64px;
background: #ffffff;
box-shadow: 0px -5px 12px 0px rgba(200, 200, 200, 0.37);
opacity: 0.97;
display: flex;
align-items: center;
gap: 5px;
padding: 0 16px;
}
.image-upload-block {
background-color: #fff;
padding: 16px;
.label-box {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.main-text {
font-weight: 600;
font-size: 15px;
color: #4a4a4a;
line-height: 16px;
margin-right: 2px;
}
.sub-text {
font-weight: 400;
font-size: 12px;
color: #4a4a4a;
line-height: 17px;
}
}
</style>