feat: 灾害填报
This commit is contained in:
parent
63fc4898d1
commit
4ab272934c
223
packages/mobile/src/components/TagFilter.vue
Normal file
223
packages/mobile/src/components/TagFilter.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="tag-filter" v-show="visible">
|
||||
<div class="filter-mask" @click="close"></div>
|
||||
<div class="filter-container">
|
||||
<div class="filter-header">
|
||||
<span class="filter-title">类型筛选</span>
|
||||
<van-icon name="close" class="close-icon" @click="close" />
|
||||
</div>
|
||||
<div class="filter-content">
|
||||
<div class="tag-list">
|
||||
<span
|
||||
v-for="tag in tagList"
|
||||
:key="tag.value"
|
||||
class="tag-item"
|
||||
:class="{ active: tempSelectedTag === tag.value }"
|
||||
@click="selectTag(tag.value)"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-footer">
|
||||
<van-button class="reset-btn" size="small" @click="resetFilter">重置</van-button>
|
||||
<van-button type="primary" class="confirm-btn" size="small" @click="confirmFilter">确定</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Icon as VanIcon, Button as VanButton } from 'vant'
|
||||
|
||||
const props = defineProps({
|
||||
// 筛选选中的值(支持 v-model)
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: 'all'
|
||||
},
|
||||
// 控制显示隐藏
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标签列表
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '边坡坍塌', value: '边坡坍塌' },
|
||||
{ label: '泥石流', value: '泥石流' },
|
||||
{ label: '路基沉陷', value: '路基沉陷' },
|
||||
{ label: '山体滑坡', value: '山体滑坡' },
|
||||
{ label: '行道树倒塌', value: '行道树倒塌' },
|
||||
{ label: '积水', value: '积水' },
|
||||
{ label: '积雪', value: '积雪' },
|
||||
{ label: '其他', value: '其他' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:visible', 'confirm'])
|
||||
|
||||
// 临时选中的标签(用于确认前暂存)
|
||||
const tempSelectedTag = ref(props.modelValue)
|
||||
|
||||
// 标签列表
|
||||
const tagList = computed(() => props.tags)
|
||||
|
||||
// 监听 modelValue 变化,同步临时选中值
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
tempSelectedTag.value = newVal
|
||||
})
|
||||
|
||||
// 监听 visible 变化,打开时同步临时选中值
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
tempSelectedTag.value = props.modelValue
|
||||
}
|
||||
})
|
||||
|
||||
// 选择标签(临时)
|
||||
const selectTag = (value) => {
|
||||
tempSelectedTag.value = value
|
||||
}
|
||||
|
||||
// 重置筛选
|
||||
const resetFilter = () => {
|
||||
tempSelectedTag.value = 'all'
|
||||
}
|
||||
|
||||
// 确认筛选
|
||||
const confirmFilter = () => {
|
||||
emit('update:modelValue', tempSelectedTag.value)
|
||||
emit('confirm', tempSelectedTag.value)
|
||||
close()
|
||||
}
|
||||
|
||||
// 关闭筛选弹窗
|
||||
const close = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tag-filter {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.filter-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
border-radius: 0 0 16px 16px;
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 16px 12px;
|
||||
border-bottom: 1px solid #ebedf0;
|
||||
|
||||
.filter-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #323233;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 22px;
|
||||
color: #969799;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
padding: 16px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 6px 16px;
|
||||
background-color: #f7f8fa;
|
||||
border-radius: 24px;
|
||||
font-size: 14px;
|
||||
color: #646566;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #1989fa;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 16px 20px;
|
||||
border-top: 1px solid #ebedf0;
|
||||
|
||||
.reset-btn,
|
||||
.confirm-btn {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background-color: #f7f8fa;
|
||||
border: none;
|
||||
color: #646566;
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -100,6 +100,11 @@ const routes = [
|
||||
path: '/disasterManagement',
|
||||
name: 'DisasterManagement',
|
||||
component: () => import('../views/DisasterManagement/DisasterManagement.vue')
|
||||
},
|
||||
{
|
||||
path: '/disasterReport',
|
||||
name: 'DisasterReport',
|
||||
component: () => import('../views/DisasterManagement/DisasterReport.vue')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -1,21 +1,10 @@
|
||||
<template>
|
||||
<PageContainer title="灾毁阻断" @click-back="handleClickBack">
|
||||
<SearchInput v-model="searchValue" placeholder="请输入地点关键词" @search="handleSearch">
|
||||
<template #extra>
|
||||
<!-- 全部按钮(激活状态) -->
|
||||
<van-button type="default" size="small" :class="{ active: activeFilter === 'all' }" @click="activeFilter = 'all'"> 全部 </van-button>
|
||||
|
||||
<!-- 筛选按钮(带图标) -->
|
||||
<van-button type="default" size="small" icon="filter-o" @click="showFilter = true"></van-button>
|
||||
</template>
|
||||
</SearchInput>
|
||||
|
||||
<PageContainer title="灾害管理" @click-back="handleClickBack">
|
||||
<CurrentSite />
|
||||
|
||||
<div class="list-panel">
|
||||
<CardItem v-for="(item, index) in list" :key="index" :title="item.title" @click="handleClickItem(item)">
|
||||
<template #headerExtra>
|
||||
<!-- 使用 Vant Tag 组件显示状态 -->
|
||||
<van-tag :type="item.status === '未解除' ? 'danger' : 'success'" plain size="medium">
|
||||
{{ item.status }}
|
||||
</van-tag>
|
||||
@ -30,27 +19,30 @@
|
||||
<span class="info-label">预计恢复时间:</span>
|
||||
<span class="info-value">{{ item.estimateRecoverTime }}</span>
|
||||
</div>
|
||||
<!-- 使用 Vant Tag 组件显示灾毁类型 -->
|
||||
<div class="disaster-type-wrapper">
|
||||
<van-tag type="primary" size="medium" plain>{{ item.disasterType }}</van-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用绝对定位的箭头 -->
|
||||
<van-icon class="jump-icon-absolute" name="arrow" />
|
||||
</CardItem>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<van-loading size="24px" vertical>加载中...</van-loading>
|
||||
</div>
|
||||
|
||||
<!-- 空状态提示 -->
|
||||
<EmptyBox v-if="!loading && list.length === 0" :placeholder="emptyText" />
|
||||
</div>
|
||||
|
||||
<!-- 灾毁填报按钮 -->
|
||||
<van-button type="primary" class="fab-btn" icon="plus" @click="handleAdd"> 冰雪填报 </van-button>
|
||||
<van-button type="primary" class="footer-btn" icon="plus" @click="handleAdd"> 冰雪填报 </van-button>
|
||||
|
||||
<!-- 筛选组件:v-model 绑定选中的值,visible 控制显示隐藏 -->
|
||||
<TagFilter
|
||||
v-model="selectedDisasterType"
|
||||
:visible="showFilter"
|
||||
@update:visible="showFilter = $event"
|
||||
@confirm="handleFilterConfirm"
|
||||
/>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
@ -63,6 +55,7 @@ import SearchInput from '@/components/SearchInput.vue'
|
||||
import CardItem from '@/components/CardItem.vue'
|
||||
import EmptyBox from '@/components/EmptyBox.vue'
|
||||
import CurrentSite from '@/components/CurrentSite.vue'
|
||||
import TagFilter from '@/components/TagFilter.vue'
|
||||
import mockDataJSON from './mockData.json'
|
||||
|
||||
const router = useRouter()
|
||||
@ -79,45 +72,90 @@ const loading = ref(false)
|
||||
// 空状态文本
|
||||
const emptyText = ref('暂无相关灾毁信息')
|
||||
|
||||
// 获取灾毁列表数据(后端接口)
|
||||
const getDisasterList = async (keyword = '') => {
|
||||
// 筛选弹窗显示状态
|
||||
const showFilter = ref(false)
|
||||
|
||||
// 当前选中的灾毁类型(通过 v-model 传给 TagFilter)
|
||||
const selectedDisasterType = ref('all')
|
||||
|
||||
// 灾毁类型列表
|
||||
const disasterTypes = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '边坡坍塌', value: '边坡坍塌' },
|
||||
{ label: '泥石流', value: '泥石流' },
|
||||
{ label: '路基沉陷', value: '路基沉陷' },
|
||||
{ label: '山体滑坡', value: '山体滑坡' },
|
||||
{ label: '行道树倒塌', value: '行道树倒塌' },
|
||||
{ label: '积水', value: '积水' },
|
||||
{ label: '积雪', value: '积雪' },
|
||||
{ label: '其他', value: '其他' }
|
||||
]
|
||||
|
||||
// 获取简短类型名称
|
||||
const getShortTypeName = (type) => {
|
||||
const typeMap = {
|
||||
'边坡坍塌': '边坡',
|
||||
'泥石流': '泥石',
|
||||
'路基沉陷': '路基',
|
||||
'山体滑坡': '滑坡',
|
||||
'行道树倒塌': '树倒',
|
||||
'积水': '积水',
|
||||
'积雪': '积雪',
|
||||
'其他': '其他'
|
||||
}
|
||||
return typeMap[type] || type.substring(0, 2)
|
||||
}
|
||||
|
||||
// 获取灾毁列表数据
|
||||
const getDisasterList = async (keyword = '', disasterType = 'all') => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// TODO: 替换为实际的后端接口地址
|
||||
const response = await fetch('/api/disaster/list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
keyword: keyword.trim()
|
||||
})
|
||||
})
|
||||
|
||||
// 模拟接口响应格式,实际使用时根据后端返回结构调整
|
||||
// const response = await fetch('/api/disaster/list', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json'
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// keyword: keyword.trim(),
|
||||
// disasterType: disasterType === 'all' ? '' : disasterType
|
||||
// })
|
||||
// })
|
||||
// const result = await response.json()
|
||||
|
||||
// 模拟异步请求
|
||||
// if (result.code === 200) {
|
||||
// list.value = result.data
|
||||
// emptyText.value = keyword ? '未搜索到相关灾毁信息' : '暂无相关灾毁信息'
|
||||
// } else {
|
||||
// showToast(result.message || '获取数据失败')
|
||||
// list.value = []
|
||||
// }
|
||||
|
||||
// ========== 模拟数据(实际开发时删除此部分) ==========
|
||||
// ========== 模拟数据 ==========
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
const mockData = mockDataJSON
|
||||
|
||||
// 模拟后端搜索过滤(实际由后端完成)
|
||||
let filteredData = [...mockData]
|
||||
|
||||
if (keyword) {
|
||||
const filtered = mockData.filter((item) => item.title.toLowerCase().includes(keyword.toLowerCase()))
|
||||
list.value = filtered
|
||||
emptyText.value = filtered.length === 0 ? '未搜索到相关灾毁信息' : '暂无相关灾毁信息'
|
||||
filteredData = filteredData.filter((item) =>
|
||||
item.title.toLowerCase().includes(keyword.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
if (disasterType !== 'all') {
|
||||
filteredData = filteredData.filter((item) =>
|
||||
item.disasterType === disasterType
|
||||
)
|
||||
}
|
||||
|
||||
list.value = filteredData
|
||||
|
||||
if (keyword && filteredData.length === 0) {
|
||||
emptyText.value = '未搜索到相关灾毁信息'
|
||||
} else if (disasterType !== 'all' && filteredData.length === 0) {
|
||||
const typeLabel = disasterTypes.find(t => t.value === disasterType)?.label || disasterType
|
||||
emptyText.value = `暂无${typeLabel}类型灾毁信息`
|
||||
} else {
|
||||
list.value = mockData
|
||||
emptyText.value = '暂无相关灾毁信息'
|
||||
}
|
||||
// ========== 模拟数据结束 ==========
|
||||
@ -130,9 +168,23 @@ const getDisasterList = async (keyword = '') => {
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理(调用后端接口)
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
getDisasterList(searchValue.value)
|
||||
getDisasterList(searchValue.value, selectedDisasterType.value)
|
||||
}
|
||||
|
||||
// 点击全部按钮
|
||||
const handleAllClick = () => {
|
||||
if (selectedDisasterType.value !== 'all') {
|
||||
selectedDisasterType.value = 'all'
|
||||
getDisasterList(searchValue.value, 'all')
|
||||
}
|
||||
}
|
||||
|
||||
// 筛选确认
|
||||
const handleFilterConfirm = (type) => {
|
||||
// selectedDisasterType 已经通过 v-model 更新,这里只需重新请求数据
|
||||
getDisasterList(searchValue.value, type)
|
||||
}
|
||||
|
||||
// 点击返回
|
||||
@ -140,7 +192,7 @@ const handleClickBack = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 点击列表项,跳转详情
|
||||
// 点击列表项
|
||||
const handleClickItem = (item) => {
|
||||
router.push({
|
||||
path: '/disaster-detail',
|
||||
@ -157,9 +209,7 @@ const handleClickItem = (item) => {
|
||||
|
||||
// 灾毁填报
|
||||
const handleAdd = () => {
|
||||
// TODO: 跳转到灾毁填报页面
|
||||
console.log('点击灾毁填报')
|
||||
// router.push('/disaster-report')
|
||||
router.push('/disasterReport')
|
||||
}
|
||||
|
||||
// 页面初始化加载数据
|
||||
@ -176,7 +226,6 @@ onMounted(() => {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
/* CardItem 需要设置相对定位 */
|
||||
:deep(.card-item) {
|
||||
position: relative;
|
||||
}
|
||||
@ -185,7 +234,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-right: 24px; /* 为箭头留出空间 */
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.time-box {
|
||||
@ -206,7 +255,6 @@ onMounted(() => {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* 使用绝对定位的箭头样式 */
|
||||
.jump-icon-absolute {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
@ -217,12 +265,11 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 灾毁类型标签样式 */
|
||||
.disaster-type-wrapper {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.fab-btn {
|
||||
.footer-btn {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
@ -249,4 +296,12 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
:deep(.van-button) {
|
||||
&.active {
|
||||
background-color: #1989fa;
|
||||
color: #fff;
|
||||
border-color: #1989fa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
139
packages/mobile/src/views/DisasterManagement/DisasterReport.vue
Normal file
139
packages/mobile/src/views/DisasterManagement/DisasterReport.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<PageContainer title="灾毁填报" @click-back="handleClickBack" class="page-container">
|
||||
<!-- 当前站点信息 -->
|
||||
<CurrentSite />
|
||||
|
||||
<!-- 事件类型 -->
|
||||
<PanelItem title="事件类型">
|
||||
<van-radio-group v-model="eventType" direction="horizontal" class="event-type-group">
|
||||
<van-radio name="water">水毁灾害</van-radio>
|
||||
<van-radio name="ice">冰雪灾害</van-radio>
|
||||
</van-radio-group>
|
||||
</PanelItem>
|
||||
|
||||
<!-- 根据事件类型渲染不同表单 -->
|
||||
<WaterDisaster
|
||||
v-if="eventType === 'water'"
|
||||
ref="waterDisasterRef"
|
||||
v-model="waterDisasterData"
|
||||
/>
|
||||
|
||||
<!-- 冰雪灾害表单(待实现) -->
|
||||
<div v-else class="coming-soon">
|
||||
<van-empty description="冰雪灾害表单开发中..." />
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<van-button type="primary" class="footer-btn" @click="handleSubmit" :loading="submitting">
|
||||
提交
|
||||
</van-button>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showSuccessToast, showFailToast } from 'vant'
|
||||
import PageContainer from '@/components/PageContainer.vue'
|
||||
import CurrentSite from '@/components/CurrentSite.vue'
|
||||
import PanelItem from '@/components/PanelItem.vue'
|
||||
import WaterDisaster from './WaterDisaster.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 事件类型
|
||||
const eventType = ref('water')
|
||||
|
||||
// 表单数据
|
||||
const waterDisasterData = ref({})
|
||||
const waterDisasterRef = ref(null)
|
||||
const submitting = ref(false)
|
||||
|
||||
// 返回上一页
|
||||
const handleClickBack = () => {
|
||||
router.replace('/disasterManagement')
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
// 验证表单
|
||||
if (eventType.value === 'water') {
|
||||
if (!waterDisasterRef.value.validate()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
// 获取表单数据
|
||||
let formData = {}
|
||||
if (eventType.value === 'water') {
|
||||
formData = waterDisasterRef.value.getFormData()
|
||||
}
|
||||
|
||||
// 添加事件类型和站点信息
|
||||
const submitData = {
|
||||
eventType: eventType.value,
|
||||
...formData,
|
||||
// 可以在这里添加站点信息等其他数据
|
||||
}
|
||||
|
||||
console.log('提交数据:', submitData)
|
||||
|
||||
// 模拟提交接口
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
showSuccessToast('提交成功')
|
||||
|
||||
// 提交成功后返回列表页
|
||||
setTimeout(() => {
|
||||
router.replace('/disasterManagement')
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
showFailToast('提交失败,请重试')
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
padding-bottom: 80px;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.event-type-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
:deep(.van-radio) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.coming-soon {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100% - 32px);
|
||||
max-width: 340px;
|
||||
border-radius: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
|
||||
&:active {
|
||||
opacity: 0.9;
|
||||
transform: translateX(-50%) scale(0.98);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
532
packages/mobile/src/views/DisasterManagement/WaterDisaster.vue
Normal file
532
packages/mobile/src/views/DisasterManagement/WaterDisaster.vue
Normal file
@ -0,0 +1,532 @@
|
||||
<template>
|
||||
<div class="water-disaster">
|
||||
<!-- 基本信息 -->
|
||||
<PanelItem title="基本信息">
|
||||
<van-form>
|
||||
<!-- 路况类别 -->
|
||||
<BasePicker v-model="formData.roadCondition" :options="roadConditionOptions" label="路况类别" placeholder="请选择" />
|
||||
|
||||
<!-- 是否阻断 -->
|
||||
<BasePicker v-model="formData.isBlocked" :options="blockedOptions" label="是否阻断" placeholder="请选择" />
|
||||
|
||||
<!-- 抢修进度 -->
|
||||
<BasePicker v-model="formData.repairProgress" :options="repairProgressOptions" label="抢修进度" placeholder="请选择" />
|
||||
|
||||
<!-- 水毁处数 -->
|
||||
<van-field v-model="formData.waterDamageCount" label="水毁处数" placeholder="请填写" type="number" />
|
||||
|
||||
<!-- 阻断里程 -->
|
||||
<van-field v-model="formData.blockedMileage" label="阻断里程" placeholder="请填写" type="digit">
|
||||
<template #button>
|
||||
<span class="field-unit">公里</span>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<!-- 发生时间 -->
|
||||
<BaseDatePicker v-model="formData.occurTime" label="发生时间" placeholder="请选择时间" :columnsType="['year', 'month', 'day', 'hour', 'minute']" />
|
||||
<div class="calibrate-time-btn" @click="calibrateTime">
|
||||
<van-icon name="replay" />
|
||||
<span>校准时间</span>
|
||||
</div>
|
||||
|
||||
<!-- 线路编号 -->
|
||||
<van-field v-model="formData.lineCode" label="线路编号" placeholder="请填写" />
|
||||
|
||||
<!-- 起点桩号 -->
|
||||
<van-field v-model="formData.startPileNo" label="起点桩号(K)" placeholder="请填写" />
|
||||
|
||||
<!-- 起点桩经纬度 -->
|
||||
<div class="coordinate-row">
|
||||
<van-field v-model="formData.startLongitude" label="起点桩经度" placeholder="经度" class="coordinate-field" />
|
||||
<van-field v-model="formData.startLatitude" label="起点桩纬度" placeholder="纬度" class="coordinate-field" />
|
||||
</div>
|
||||
<div class="calibrate-coord-btn" @click="calibrateStartCoord">
|
||||
<van-icon name="location-o" />
|
||||
<span>校准经纬度</span>
|
||||
</div>
|
||||
|
||||
<!-- 止点桩号 -->
|
||||
<van-field v-model="formData.endPileNo" label="止点桩号(K)" placeholder="请填写" />
|
||||
|
||||
<!-- 止点桩经纬度 -->
|
||||
<div class="coordinate-row">
|
||||
<van-field v-model="formData.endLongitude" label="止点桩经度" placeholder="经度" class="coordinate-field" />
|
||||
<van-field v-model="formData.endLatitude" label="止点桩纬度" placeholder="纬度" class="coordinate-field" />
|
||||
</div>
|
||||
<div class="calibrate-coord-btn" @click="calibrateEndCoord">
|
||||
<van-icon name="location-o" />
|
||||
<span>校准经纬度</span>
|
||||
</div>
|
||||
|
||||
<!-- 路况位置 -->
|
||||
<van-field v-model="formData.roadLocation" label="路况位置" placeholder="请填写" />
|
||||
|
||||
<!-- 阻断点小地名 -->
|
||||
<van-field v-model="formData.smallPlaceName" label="阻断点小地名" placeholder="请填写" />
|
||||
</van-form>
|
||||
</PanelItem>
|
||||
|
||||
<!-- 处置情况 -->
|
||||
<PanelItem title="处置情况">
|
||||
<div class="disposal-measures">
|
||||
<span class="measures-label">处置措施</span>
|
||||
<div class="measures-options">
|
||||
<van-checkbox-group v-model="formData.disposalMeasures" direction="horizontal">
|
||||
<van-checkbox name="halfClose">半幅封闭</van-checkbox>
|
||||
<van-checkbox name="fullClose">全副封闭</van-checkbox>
|
||||
<van-checkbox name="bypass">便道通行</van-checkbox>
|
||||
<van-checkbox name="normal">正常通行</van-checkbox>
|
||||
</van-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预计恢复时间 -->
|
||||
<BaseDatePicker v-model="formData.estimatedRecoverTime" label="预计恢复时间" placeholder="请选择时间" :min-date="minDate" :max-date="maxDate" type="datetime" />
|
||||
|
||||
<!-- 实际恢复时间 -->
|
||||
<BaseDatePicker v-model="formData.actualRecoverTime" label="实际恢复时间" placeholder="请选择时间" :min-date="minDate" :max-date="maxDate" type="datetime" />
|
||||
</PanelItem>
|
||||
|
||||
<!-- 人员车辆 -->
|
||||
<PanelItem title="人员车辆">
|
||||
<van-form>
|
||||
<van-field v-model="formData.injuredCount" label="受伤人员" placeholder="请填写" type="number">
|
||||
<template #button>
|
||||
<span class="field-unit">个</span>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model="formData.deathCount" label="死亡人员" placeholder="请填写" type="number">
|
||||
<template #button>
|
||||
<span class="field-unit">个</span>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model="formData.strandedPeople" label="滞留人员" placeholder="请填写" type="number">
|
||||
<template #button>
|
||||
<span class="field-unit">个</span>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model="formData.damagedVehicles" label="损坏车辆" placeholder="请填写" type="number">
|
||||
<template #button>
|
||||
<span class="field-unit">辆</span>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model="formData.strandedVehicles" label="滞留车辆" placeholder="请填写" type="number">
|
||||
<template #button>
|
||||
<span class="field-unit">辆</span>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-form>
|
||||
</PanelItem>
|
||||
|
||||
<!-- 灾毁损失 -->
|
||||
<PanelItem title="灾毁损失">
|
||||
<div class="loss-row">
|
||||
<span class="loss-label">塌方及损失</span>
|
||||
<span class="loss-value">{{ formData.collapseLoss }}万/万元</span>
|
||||
</div>
|
||||
<van-button size="small" block type="primary" plain @click="showLossDialog = true">添加损失</van-button>
|
||||
<van-field v-model="formData.handlingSituation" label="处理情况" placeholder="请填写(选填)" />
|
||||
<van-field v-model="formData.totalLossAmount" label="损失总金额" placeholder="请填写(选填)" type="digit">
|
||||
<template #button>
|
||||
<span class="field-unit">万元</span>
|
||||
</template>
|
||||
</van-field>
|
||||
</PanelItem>
|
||||
|
||||
<PanelItem>
|
||||
<van-field v-model="formData.machineryInput" label="已投机械" placeholder="请填写" type="digit">
|
||||
<template #button>
|
||||
<span class="field-unit">台/班</span>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model="formData.laborInput" label="已投入力" placeholder="请填写" type="number">
|
||||
<template #button>
|
||||
<span class="field-unit">人次</span>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model="formData.fundsInput" label="已投资金" placeholder="请填写" type="digit">
|
||||
<template #button>
|
||||
<span class="field-unit">万元</span>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model="formData.siteDescription" label="现场描述" placeholder="请填写" type="textarea" rows="2" autosize />
|
||||
|
||||
<van-field label="附件">
|
||||
<template #input>
|
||||
</template>
|
||||
</van-field>
|
||||
</PanelItem>
|
||||
|
||||
<!-- 附件 -->
|
||||
<!-- <PanelItem title="附件">
|
||||
<div class="attachment-tip">图片只能上传jpg/png文件,且不超过500kb;视频仅支持20s内的视频</div>
|
||||
<div class="upload-area">
|
||||
<van-uploader
|
||||
v-model="formData.imageFiles"
|
||||
:after-read="afterImageRead"
|
||||
accept="image/jpeg,image/png"
|
||||
:max-size="500 * 1024"
|
||||
@oversize="onOversize"
|
||||
multiple
|
||||
:max-count="9"
|
||||
>
|
||||
<div class="upload-btn">
|
||||
<van-icon name="photo-o" size="24" />
|
||||
<span>上传图片</span>
|
||||
</div>
|
||||
</van-uploader>
|
||||
<van-uploader v-model="formData.videoFile" :after-read="afterVideoRead" accept="video/*" :max-size="20 * 1024 * 1024" @oversize="onVideoOversize">
|
||||
<div class="upload-btn">
|
||||
<van-icon name="video-o" size="24" />
|
||||
<span>上传视频</span>
|
||||
</div>
|
||||
</van-uploader>
|
||||
</div>
|
||||
<div v-if="formData.videoFile.length > 0 && formData.videoFile[0].content" class="video-preview">
|
||||
<video :src="formData.videoFile[0].content" controls style="width: 100%; max-height: 200px"></video>
|
||||
</div>
|
||||
</PanelItem> -->
|
||||
|
||||
<!-- 损失计算弹窗 -->
|
||||
<van-dialog v-model:show="showLossDialog" title="添加塌方损失" show-cancel-button @confirm="confirmLoss" @cancel="showLossDialog = false">
|
||||
<div class="loss-dialog-content">
|
||||
<van-field v-model="lossForm.length" label="长度(m)" type="number" placeholder="请输入长度" />
|
||||
<van-field v-model="lossForm.width" label="宽度(m)" type="number" placeholder="请输入宽度" />
|
||||
<van-field v-model="lossForm.height" label="高度(m)" type="number" placeholder="请输入高度" />
|
||||
<van-field v-model="lossForm.unitPrice" label="单价(元/m³)" type="number" placeholder="请输入单价" />
|
||||
<div class="loss-calc-result">预估塌方量:{{ calculatedVolume }} m³</div>
|
||||
<div class="loss-calc-result">预估损失:{{ calculatedLoss }} 万元</div>
|
||||
</div>
|
||||
</van-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { showToast, showFailToast } from 'vant'
|
||||
import PanelItem from '@/components/PanelItem.vue'
|
||||
import BasePicker from '@/components/BasePicker.vue'
|
||||
import BaseDatePicker from '@/components/BaseDatePicker.vue'
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits(['update:modelValue', 'submit'])
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
roadCondition: '',
|
||||
isBlocked: '',
|
||||
repairProgress: '',
|
||||
waterDamageCount: '',
|
||||
blockedMileage: '',
|
||||
occurTime: '',
|
||||
lineCode: '',
|
||||
startPileNo: '',
|
||||
startLongitude: '',
|
||||
startLatitude: '',
|
||||
endPileNo: '',
|
||||
endLongitude: '',
|
||||
endLatitude: '',
|
||||
roadLocation: '',
|
||||
smallPlaceName: '',
|
||||
disposalMeasures: [],
|
||||
estimatedRecoverTime: '',
|
||||
actualRecoverTime: '',
|
||||
injuredCount: '',
|
||||
deathCount: '',
|
||||
strandedPeople: '',
|
||||
damagedVehicles: '',
|
||||
strandedVehicles: '',
|
||||
collapseLoss: '0',
|
||||
handlingSituation: '',
|
||||
totalLossAmount: '',
|
||||
machineryInput: '',
|
||||
laborInput: '',
|
||||
fundsInput: '',
|
||||
siteDescription: '',
|
||||
imageFiles: [],
|
||||
videoFile: []
|
||||
})
|
||||
|
||||
// 弹窗显示状态
|
||||
const showLossDialog = ref(false)
|
||||
|
||||
// BasePicker 选项数据
|
||||
const roadConditionOptions = [
|
||||
{ label: '高速公路', value: '高速公路' },
|
||||
{ label: '国道', value: '国道' },
|
||||
{ label: '省道', value: '省道' },
|
||||
{ label: '县道', value: '县道' },
|
||||
{ label: '乡道', value: '乡道' },
|
||||
{ label: '村道', value: '村道' }
|
||||
]
|
||||
|
||||
const blockedOptions = [
|
||||
{ label: '是', value: '是' },
|
||||
{ label: '否', value: '否' }
|
||||
]
|
||||
|
||||
const repairProgressOptions = [
|
||||
{ label: '未开始', value: '未开始' },
|
||||
{ label: '进行中', value: '进行中' },
|
||||
{ label: '已抢通', value: '已抢通' },
|
||||
{ label: '已修复', value: '已修复' }
|
||||
]
|
||||
|
||||
// 时间选择器范围
|
||||
const minDate = new Date(2020, 0, 1)
|
||||
const maxDate = new Date(2030, 11, 31)
|
||||
|
||||
// 损失计算表单
|
||||
const lossForm = reactive({
|
||||
length: '',
|
||||
width: '',
|
||||
height: '',
|
||||
unitPrice: ''
|
||||
})
|
||||
|
||||
// 计算塌方量
|
||||
const calculatedVolume = computed(() => {
|
||||
const l = parseFloat(lossForm.length) || 0
|
||||
const w = parseFloat(lossForm.width) || 0
|
||||
const h = parseFloat(lossForm.height) || 0
|
||||
return (l * w * h).toFixed(2)
|
||||
})
|
||||
|
||||
// 计算损失金额(万元)
|
||||
const calculatedLoss = computed(() => {
|
||||
const volume = parseFloat(calculatedVolume.value) || 0
|
||||
const price = parseFloat(lossForm.unitPrice) || 0
|
||||
const lossYuan = volume * price
|
||||
return (lossYuan / 10000).toFixed(2)
|
||||
})
|
||||
|
||||
// 监听父组件传入的数据
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal && Object.keys(newVal).length > 0) {
|
||||
Object.assign(formData, newVal)
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// 监听表单数据变化,同步到父组件
|
||||
watch(
|
||||
formData,
|
||||
() => {
|
||||
emit('update:modelValue', { ...formData })
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 校准时间
|
||||
const calibrateTime = () => {
|
||||
const now = new Date()
|
||||
const formatted = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
formData.occurTime = formatted
|
||||
showToast('时间已校准为当前时间')
|
||||
}
|
||||
|
||||
// 校准起点经纬度
|
||||
const calibrateStartCoord = () => {
|
||||
formData.startLongitude = '108.41763025'
|
||||
formData.startLatitude = '108.41763025'
|
||||
showToast('起点经纬度已校准')
|
||||
}
|
||||
|
||||
// 校准止点经纬度
|
||||
const calibrateEndCoord = () => {
|
||||
formData.endLongitude = '108.41763025'
|
||||
formData.endLatitude = '108.41763025'
|
||||
showToast('止点经纬度已校准')
|
||||
}
|
||||
|
||||
// 确认损失计算
|
||||
const confirmLoss = () => {
|
||||
const lossValue = calculatedLoss.value
|
||||
if (parseFloat(lossValue) > 0) {
|
||||
formData.collapseLoss = lossValue
|
||||
if (formData.totalLossAmount) {
|
||||
const currentTotal = parseFloat(formData.totalLossAmount) || 0
|
||||
formData.totalLossAmount = (currentTotal + parseFloat(lossValue)).toFixed(2)
|
||||
} else {
|
||||
formData.totalLossAmount = lossValue
|
||||
}
|
||||
} else {
|
||||
showToast('请填写有效的长宽高和单价')
|
||||
}
|
||||
lossForm.length = ''
|
||||
lossForm.width = ''
|
||||
lossForm.height = ''
|
||||
lossForm.unitPrice = ''
|
||||
showLossDialog.value = false
|
||||
}
|
||||
|
||||
// 图片上传处理
|
||||
const afterImageRead = (file) => {
|
||||
console.log('图片上传:', file)
|
||||
}
|
||||
|
||||
const onOversize = () => {
|
||||
showFailToast('图片大小不能超过500KB')
|
||||
}
|
||||
|
||||
const afterVideoRead = (file) => {
|
||||
console.log('视频上传:', file)
|
||||
}
|
||||
|
||||
const onVideoOversize = () => {
|
||||
showFailToast('视频大小不能超过20MB')
|
||||
}
|
||||
|
||||
// 暴露验证方法
|
||||
const validate = () => {
|
||||
if (!formData.occurTime) {
|
||||
showToast('请填写发生时间')
|
||||
return false
|
||||
}
|
||||
if (!formData.lineCode) {
|
||||
showToast('请填写线路编号')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取表单数据
|
||||
const getFormData = () => {
|
||||
return { ...formData }
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
validate,
|
||||
getFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.water-disaster {
|
||||
|
||||
.coordinate-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
.coordinate-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.calibrate-time-btn,
|
||||
.calibrate-coord-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #1989fa;
|
||||
margin: 8px 0 8px 12px;
|
||||
padding: 4px 8px;
|
||||
background: #f0f7ff;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.field-unit {
|
||||
color: #969799;
|
||||
font-size: 14px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.disposal-measures {
|
||||
margin-bottom: 16px;
|
||||
.measures-label {
|
||||
font-size: 14px;
|
||||
color: #323233;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.measures-options {
|
||||
:deep(.van-checkbox-group) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
:deep(.van-checkbox) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loss-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f8f9fa;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0;
|
||||
.loss-label {
|
||||
font-size: 14px;
|
||||
color: #323233;
|
||||
}
|
||||
.loss-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ee0a24;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-tip {
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
.upload-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #f8f9fa;
|
||||
border: 1px dashed #dcdee0;
|
||||
border-radius: 8px;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.loss-dialog-content {
|
||||
padding: 8px 16px 16px;
|
||||
.loss-calc-result {
|
||||
font-size: 14px;
|
||||
color: #1989fa;
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.van-field__label) {
|
||||
width: 90px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user