feat: 灾害填报

This commit is contained in:
niedongsheng 2026-04-07 17:01:46 +08:00
parent 63fc4898d1
commit 4ab272934c
5 changed files with 1006 additions and 52 deletions

View 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>

View File

@ -100,6 +100,11 @@ const routes = [
path: '/disasterManagement', path: '/disasterManagement',
name: 'DisasterManagement', name: 'DisasterManagement',
component: () => import('../views/DisasterManagement/DisasterManagement.vue') component: () => import('../views/DisasterManagement/DisasterManagement.vue')
},
{
path: '/disasterReport',
name: 'DisasterReport',
component: () => import('../views/DisasterManagement/DisasterReport.vue')
} }
] ]

View File

@ -1,21 +1,10 @@
<template> <template>
<PageContainer title="灾毁阻断" @click-back="handleClickBack"> <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>
<CurrentSite /> <CurrentSite />
<div class="list-panel"> <div class="list-panel">
<CardItem v-for="(item, index) in list" :key="index" :title="item.title" @click="handleClickItem(item)"> <CardItem v-for="(item, index) in list" :key="index" :title="item.title" @click="handleClickItem(item)">
<template #headerExtra> <template #headerExtra>
<!-- 使用 Vant Tag 组件显示状态 -->
<van-tag :type="item.status === '未解除' ? 'danger' : 'success'" plain size="medium"> <van-tag :type="item.status === '未解除' ? 'danger' : 'success'" plain size="medium">
{{ item.status }} {{ item.status }}
</van-tag> </van-tag>
@ -30,27 +19,30 @@
<span class="info-label">预计恢复时间</span> <span class="info-label">预计恢复时间</span>
<span class="info-value">{{ item.estimateRecoverTime }}</span> <span class="info-value">{{ item.estimateRecoverTime }}</span>
</div> </div>
<!-- 使用 Vant Tag 组件显示灾毁类型 -->
<div class="disaster-type-wrapper"> <div class="disaster-type-wrapper">
<van-tag type="primary" size="medium" plain>{{ item.disasterType }}</van-tag> <van-tag type="primary" size="medium" plain>{{ item.disasterType }}</van-tag>
</div> </div>
</div> </div>
<!-- 使用绝对定位的箭头 -->
<van-icon class="jump-icon-absolute" name="arrow" /> <van-icon class="jump-icon-absolute" name="arrow" />
</CardItem> </CardItem>
<!-- 加载状态 -->
<div v-if="loading" class="loading-wrapper"> <div v-if="loading" class="loading-wrapper">
<van-loading size="24px" vertical>加载中...</van-loading> <van-loading size="24px" vertical>加载中...</van-loading>
</div> </div>
<!-- 空状态提示 -->
<EmptyBox v-if="!loading && list.length === 0" :placeholder="emptyText" /> <EmptyBox v-if="!loading && list.length === 0" :placeholder="emptyText" />
</div> </div>
<!-- 灾毁填报按钮 --> <van-button type="primary" class="footer-btn" icon="plus" @click="handleAdd"> 冰雪填报 </van-button>
<van-button type="primary" class="fab-btn" icon="plus" @click="handleAdd"> 冰雪填报 </van-button>
<!-- 筛选组件v-model 绑定选中的值visible 控制显示隐藏 -->
<TagFilter
v-model="selectedDisasterType"
:visible="showFilter"
@update:visible="showFilter = $event"
@confirm="handleFilterConfirm"
/>
</PageContainer> </PageContainer>
</template> </template>
@ -63,6 +55,7 @@ import SearchInput from '@/components/SearchInput.vue'
import CardItem from '@/components/CardItem.vue' import CardItem from '@/components/CardItem.vue'
import EmptyBox from '@/components/EmptyBox.vue' import EmptyBox from '@/components/EmptyBox.vue'
import CurrentSite from '@/components/CurrentSite.vue' import CurrentSite from '@/components/CurrentSite.vue'
import TagFilter from '@/components/TagFilter.vue'
import mockDataJSON from './mockData.json' import mockDataJSON from './mockData.json'
const router = useRouter() const router = useRouter()
@ -79,45 +72,90 @@ const loading = ref(false)
// //
const emptyText = ref('暂无相关灾毁信息') 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 loading.value = true
try { try {
// TODO: // TODO:
const response = await fetch('/api/disaster/list', { // const response = await fetch('/api/disaster/list', {
method: 'POST', // method: 'POST',
headers: { // headers: {
'Content-Type': 'application/json' // 'Content-Type': 'application/json'
}, // },
body: JSON.stringify({ // body: JSON.stringify({
keyword: keyword.trim() // keyword: keyword.trim(),
}) // disasterType: disasterType === 'all' ? '' : disasterType
}) // })
// })
// 使
// const result = await response.json() // const result = await response.json()
//
// if (result.code === 200) { // if (result.code === 200) {
// list.value = result.data // list.value = result.data
// emptyText.value = keyword ? '' : ''
// } else { // } else {
// showToast(result.message || '') // showToast(result.message || '')
// list.value = [] // list.value = []
// } // }
// ========== ========== // ========== ==========
await new Promise((resolve) => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 500))
const mockData = mockDataJSON const mockData = mockDataJSON
// let filteredData = [...mockData]
if (keyword) { if (keyword) {
const filtered = mockData.filter((item) => item.title.toLowerCase().includes(keyword.toLowerCase())) filteredData = filteredData.filter((item) =>
list.value = filtered item.title.toLowerCase().includes(keyword.toLowerCase())
emptyText.value = filtered.length === 0 ? '未搜索到相关灾毁信息' : '暂无相关灾毁信息' )
}
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 { } else {
list.value = mockData
emptyText.value = '暂无相关灾毁信息' emptyText.value = '暂无相关灾毁信息'
} }
// ========== ========== // ========== ==========
@ -130,9 +168,23 @@ const getDisasterList = async (keyword = '') => {
} }
} }
// //
const handleSearch = () => { 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('/') router.push('/')
} }
// //
const handleClickItem = (item) => { const handleClickItem = (item) => {
router.push({ router.push({
path: '/disaster-detail', path: '/disaster-detail',
@ -157,9 +209,7 @@ const handleClickItem = (item) => {
// //
const handleAdd = () => { const handleAdd = () => {
// TODO: router.push('/disasterReport')
console.log('点击灾毁填报')
// router.push('/disaster-report')
} }
// //
@ -176,7 +226,6 @@ onMounted(() => {
padding-bottom: 80px; padding-bottom: 80px;
} }
/* CardItem 需要设置相对定位 */
:deep(.card-item) { :deep(.card-item) {
position: relative; position: relative;
} }
@ -185,7 +234,7 @@ onMounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
padding-right: 24px; /* 为箭头留出空间 */ padding-right: 24px;
} }
.time-box { .time-box {
@ -206,7 +255,6 @@ onMounted(() => {
color: #333333; color: #333333;
} }
/* 使用绝对定位的箭头样式 */
.jump-icon-absolute { .jump-icon-absolute {
position: absolute; position: absolute;
right: 16px; right: 16px;
@ -217,12 +265,11 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
} }
/* 灾毁类型标签样式 */
.disaster-type-wrapper { .disaster-type-wrapper {
margin-top: 4px; margin-top: 4px;
} }
.fab-btn { .footer-btn {
position: fixed; position: fixed;
bottom: 30px; bottom: 30px;
left: 50%; left: 50%;
@ -249,4 +296,12 @@ onMounted(() => {
justify-content: center; justify-content: center;
padding: 40px 0; padding: 40px 0;
} }
</style>
:deep(.van-button) {
&.active {
background-color: #1989fa;
color: #fff;
border-color: #1989fa;
}
}
</style>

View 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>

View 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 }} </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>