feat: 灾毁管理

This commit is contained in:
niedongsheng 2026-04-08 16:01:04 +08:00
parent 6203cf9ab9
commit 210128041e
14 changed files with 2241 additions and 70 deletions

View File

@ -17,7 +17,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['back'])
const emit = defineEmits(['click-back'])
const onClickLeft = () => {
emit('click-back')

View File

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

View File

@ -0,0 +1,565 @@
<template>
<PageContainer title="灾毁详情" @click-back="handleClickBack" class="page-container">
<!-- 当前站点信息 -->
<CurrentSite />
<!-- 基本信息 -->
<PanelItem title="基本信息">
<template #headerExtra>
<!-- 状态标签需要从续报中判断是否已解除或接口增加 eventStatus 字段 -->
<div class="status-wrapper">
<van-tag :type="getEventStatusType()" size="medium" plain>
{{ getEventStatusText() }}
</van-tag>
</div>
</template>
<!-- 事件类型 -->
<div class="info-row">
<span class="info-label">事件类型</span>
<span class="info-value">水毁事件</span>
</div>
<!-- 路况类别 -->
<div class="info-row">
<span class="info-label">路况类别</span>
<span class="info-value">{{ detailData.roadConditionType || '-' }}</span>
</div>
<!-- 是否阻断 -->
<div class="info-row">
<span class="info-label">是否阻断</span>
<span class="info-value">{{ detailData.event?.isBlocked ? '是' : '否' }}</span>
</div>
<!-- 抢修进度 -->
<div class="info-row">
<span class="info-label">抢修进度</span>
<span class="info-value">{{ detailData.event?.repairProgress || '-' }}</span>
</div>
<!-- 处理措施从首报中获取 -->
<div class="info-row">
<span class="info-label">处理措施</span>
<span class="info-value">{{ formatDisposalMeasures(firstReport?.disposalMeasures) || '-' }}</span>
</div>
<!-- 水毁处数 -->
<div class="info-row">
<span class="info-label">水毁处数</span>
<span class="info-value">{{ detailData.event?.damageCount || 0 }}</span>
</div>
<!-- 阻断里程 -->
<div class="info-row">
<span class="info-label">阻断里程</span>
<span class="info-value">{{ detailData.event?.blockedMileage ? detailData.event.blockedMileage + '公里' : '-' }}</span>
</div>
<!-- 发生时间 -->
<div class="info-row">
<span class="info-label">发生时间</span>
<span class="info-value">{{ detailData.occurTime || '-' }}</span>
</div>
<!-- 线路编号 -->
<div class="info-row">
<span class="info-label">线路编号</span>
<span class="info-value">{{ detailData.routeNo || '-' }}</span>
</div>
<!-- 地点路线 -->
<div class="info-row">
<span class="info-label">地点路线</span>
<span class="info-value">{{ detailData.occurLocation || '-' }}</span>
</div>
<!-- 起点桩号及经纬度 -->
<div class="info-row">
<span class="info-label">起点桩号</span>
<span class="info-value">{{ detailData.event?.startStakeNo || '-' }}</span>
</div>
<div class="info-row sub-row">
<span class="info-label">起点桩经度</span>
<span class="info-value">{{ detailData.event?.startStakeLng || '-' }}</span>
</div>
<div class="info-row sub-row">
<span class="info-label">起点桩纬度</span>
<span class="info-value">{{ detailData.event?.startStakeLat || '-' }}</span>
</div>
<!-- 止点桩号及经纬度 -->
<div class="info-row">
<span class="info-label">止点桩号</span>
<span class="info-value">{{ detailData.event?.endStakeNo || '-' }}</span>
</div>
<div class="info-row sub-row">
<span class="info-label">止点桩经度</span>
<span class="info-value">{{ detailData.event?.endStakeLng || '-' }}</span>
</div>
<div class="info-row sub-row">
<span class="info-label">止点桩纬度</span>
<span class="info-value">{{ detailData.event?.endStakeLat || '-' }}</span>
</div>
<!-- 路况位置使用阻断点小地名或发生地点 -->
<div class="info-row">
<span class="info-label">路况位置</span>
<span class="info-value">{{ detailData.event?.blockedPointName || detailData.occurLocation || '-' }}</span>
</div>
<!-- 阻断点小地名 -->
<div class="info-row">
<span class="info-label">阻断点小地名</span>
<span class="info-value">{{ detailData.event?.blockedPointName || '-' }}</span>
</div>
<!-- 上报区县 -->
<div class="info-row">
<span class="info-label">上报区县</span>
<span class="info-value">{{ detailData.event?.district || '-' }}</span>
</div>
<!-- 巡查里程 -->
<div class="info-row">
<span class="info-label">巡查里程</span>
<span class="info-value">{{ detailData.event?.inspectionMileage ? detailData.event.inspectionMileage + '公里' : '-' }}</span>
</div>
<!-- 是否恢复重建 -->
<div class="info-row">
<span class="info-label">是否恢复重建</span>
<span class="info-value">{{ detailData.event?.needsRecovery ? '是' : '否' }}</span>
</div>
<!-- 恢复重建预估费用 -->
<div class="info-row">
<span class="info-label">恢复重建预估费用</span>
<span class="info-value">{{ detailData.event?.estimatedRecoveryCost ? detailData.event.estimatedRecoveryCost + '万元' : '-' }}</span>
</div>
<!-- 联系人 -->
<div class="info-row">
<span class="info-label">联系人</span>
<span class="info-value">{{ detailData.event?.contactPerson || '-' }}</span>
</div>
<!-- 联系电话 -->
<div class="info-row">
<span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.event?.contactPhone || '-' }}</span>
</div>
<!-- 填报单位 -->
<div class="info-row">
<span class="info-label">填报单位</span>
<span class="info-value">{{ detailData.event?.reporterUnit || '-' }}</span>
</div>
</PanelItem>
<!-- 填报信息 -->
<PanelItem title="填报信息">
<!-- 首报信息 -->
<div class="report-section" v-if="firstReport">
<div class="report-header">
<span class="report-title">首报</span>
<span class="report-meta">{{ firstReport.reporterName || '-' }} {{ firstReport.reportTime || '-' }}</span>
</div>
<div class="report-content">
<div class="info-row">
<span class="info-label">处置情况</span>
<span class="info-value">{{ formatDisposalMeasures(firstReport.disposalMeasures) || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">塌方及损失</span>
<span class="info-value">{{ getLossDescription(firstReport) }}</span>
</div>
<div class="info-row">
<span class="info-label">路产损失</span>
<span class="info-value">{{ firstReport.totalLossAmount ? firstReport.totalLossAmount + '万元' : '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">有无车辆滞留</span>
<span class="info-value">{{ getVehicleStrandedText(firstReport) }}</span>
</div>
<div class="info-row">
<span class="info-label">滞留车辆</span>
<span class="info-value">{{ firstReport.strandedVehicleCount || 0 }}</span>
</div>
<div class="info-row">
<span class="info-label">预计恢复时间</span>
<span class="info-value">{{ firstReport.expectRecoverTime || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">实际恢复时间</span>
<span class="info-value">{{ firstReport.actualRecoverTime || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">现场描述</span>
<span class="info-value">{{ firstReport.siteDescription || '-' }}</span>
</div>
<!-- 附件 -->
<div class="info-row" v-if="firstReport.fileList && firstReport.fileList.length > 0">
<span class="info-label">附件</span>
<div class="attachment-list">
<div v-for="(file, index) in firstReport.fileList" :key="index" class="attachment-item">
<van-icon :name="file.fileType === 1 ? 'photo-o' : 'video-o'" />
<span class="file-name">{{ file.fileName }}</span>
<van-button size="mini" type="primary" plain @click="previewFile(file)">预览</van-button>
</div>
</div>
</div>
</div>
</div>
<!-- 续报列表 -->
<div v-for="(report, index) in continueReports" :key="index" class="report-section">
<div class="report-header">
<span class="report-title">续报{{ index + 1 }}</span>
<span class="report-meta">{{ report.reporterName || '-' }} {{ report.reportTime || '-' }}</span>
</div>
<div class="report-content">
<div class="info-row">
<span class="info-label">处置情况</span>
<span class="info-value">{{ formatDisposalMeasures(report.disposalMeasures) || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">塌方及损失</span>
<span class="info-value">{{ getLossDescription(report) }}</span>
</div>
<div class="info-row">
<span class="info-label">路产损失</span>
<span class="info-value">{{ report.totalLossAmount ? report.totalLossAmount + '万元' : '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">有无车辆滞留</span>
<span class="info-value">{{ getVehicleStrandedText(report) }}</span>
</div>
<div class="info-row">
<span class="info-label">滞留车辆</span>
<span class="info-value">{{ report.strandedVehicleCount || 0 }}</span>
</div>
<div class="info-row">
<span class="info-label">预计恢复时间</span>
<span class="info-value">{{ report.expectRecoverTime || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">实际恢复时间</span>
<span class="info-value">{{ report.actualRecoverTime || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">现场描述</span>
<span class="info-value">{{ report.siteDescription || '-' }}</span>
</div>
<!-- 附件 -->
<div class="info-row" v-if="report.fileList && report.fileList.length > 0">
<span class="info-label">附件</span>
<div class="attachment-list">
<div v-for="(file, fileIndex) in report.fileList" :key="fileIndex" class="attachment-item">
<van-icon :name="file.fileType === 1 ? 'photo-o' : 'video-o'" />
<span class="file-name">{{ file.fileName }}</span>
<van-button size="mini" type="primary" plain @click="previewFile(file)">预览</van-button>
</div>
</div>
</div>
</div>
</div>
<!-- 无填报信息时显示 -->
<EmptyBox v-if="!hasReportData" placeholder="暂无填报信息" />
</PanelItem>
<!-- 底部按钮未解除状态显示续报按钮 -->
<div class="footer-buttons" v-if="!isEventResolved">
<van-button type="primary" class="footer-btn" @click="handleContinueReport">续报</van-button>
</div>
</PageContainer>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast } from 'vant'
import PageContainer from '@/components/PageContainer.vue'
import PanelItem from '@/components/PanelItem.vue'
import CurrentSite from '@/components/CurrentSite.vue'
import EmptyBox from '@/components/EmptyBox.vue'
import { request } from '@shared/utils/request'
const router = useRouter()
const route = useRoute()
// Data
const detailData = ref({
event: null, // Event
report: null, // Report
reportList: [], //
fileList: [], //
lossList: [], //
occurLocation: '',
occurTime: '',
roadConditionType: '',
routeNo: ''
})
// 使 report reportList reportType === 1
const firstReport = computed(() => {
if (detailData.value.report) {
return detailData.value.report
}
return detailData.value.reportList?.find(r => r.reportType === 1)
})
// reportType === 2
const continueReports = computed(() => {
return detailData.value.reportList?.filter(r => r.reportType === 2) || []
})
//
const hasReportData = computed(() => {
return firstReport.value || continueReports.value.length > 0
})
//
const isEventResolved = computed(() => {
//
// if (firstReport.value?.actualRecoverTime) {
// return true
// }
// //
// return continueReports.value.some(r => r.actualRecoverTime)
})
//
const getEventStatusText = () => {
return isEventResolved.value ? '已解除' : '未解除'
}
//
const getEventStatusType = () => {
return isEventResolved.value ? 'success' : 'danger'
}
//
const formatDisposalMeasures = (measures) => {
if (!measures) return ''
const measureMap = {
halfClose: '半幅封闭',
fullClose: '全副封闭',
bypass: '便道通行',
normal: '正常通行'
}
return measures
.split(',')
.map((m) => measureMap[m.trim()] || m.trim())
.join('、')
}
// lossList
const getLossDescription = (report) => {
const lossList = report?.lossList
if (!lossList || lossList.length === 0) return '-'
const totalVolume = lossList.reduce((sum, loss) => {
const volume = (loss.length || 0) * (loss.width || 0) * (loss.height || 0)
return sum + volume
}, 0)
const totalAmount = lossList.reduce((sum, loss) => sum + (loss.totalAmount || 0), 0)
return `${totalVolume}方,共损失${totalAmount}万元`
}
//
const getVehicleStrandedText = (report) => {
const count = report?.strandedVehicleCount || 0
return count > 0 ? `有车滞留,共${count}` : '无车滞留'
}
//
const getDisasterDetail = async () => {
const id = route.query.id
if (!id) {
showToast('缺少灾毁ID')
return
}
try {
const result = await request({
url: `/snow-ops-platform/water-damage/getById`,
method: 'get',
params: { id }
})
if (result?.data) {
// Data
const data = result.data
detailData.value = {
event: data.event || null,
report: data.report || null,
reportList: data.reportList || [],
fileList: data.fileList || [],
lossList: data.lossList || [],
occurLocation: data.occurLocation || '',
occurTime: data.occurTime || '',
roadConditionType: data.roadConditionType || '',
routeNo: data.routeNo || ''
}
} else {
showToast(result.message || '获取详情失败')
}
} catch (error) {
console.error('获取灾毁详情失败:', error)
showToast('获取详情失败,请稍后重试')
}
}
//
const handleClickBack = () => {
router.push('/disasterManagement')
}
//
const handleContinueReport = () => {
router.push({
path: '/disasterReport',
query: {
id: route.query.id,
eventId: detailData.value.event?.id,
isContinue: 'true'
}
})
}
//
const previewFile = (file) => {
if (file.fileUrl) {
window.open(file.fileUrl, '_blank')
}
}
onMounted(() => {
getDisasterDetail()
})
</script>
<style scoped lang="scss">
.page-container {
padding-bottom: 80px;
}
.status-wrapper {
border-bottom: 1px solid #ebedf0;
}
.info-row {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
line-height: 1.4;
&.sub-row {
margin-left: 20px;
margin-top: -8px;
}
}
.info-label {
width: 110px;
flex-shrink: 0;
color: #969799;
font-size: 14px;
}
.info-value {
flex: 1;
color: #323233;
font-size: 14px;
word-break: break-all;
}
.report-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #ebedf0;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
}
.report-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px dashed #ebedf0;
.report-title {
font-size: 16px;
font-weight: 500;
color: #1989fa;
}
.report-meta {
font-size: 12px;
color: #969799;
}
}
.report-content {
.info-row {
margin-bottom: 10px;
}
}
.attachment-list {
flex: 1;
.attachment-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding: 8px;
background: #f7f8fa;
border-radius: 4px;
.van-icon {
font-size: 20px;
color: #1989fa;
}
.file-name {
flex: 1;
font-size: 13px;
color: #323233;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.footer-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 12px;
padding: 12px 16px;
background: #fff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
z-index: 10;
.footer-btn {
flex: 1;
height: 44px;
border-radius: 22px;
font-size: 16px;
}
}
</style>

View File

@ -3,24 +3,24 @@
<CurrentSite />
<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="getEventTitle(item)" @click="handleClickItem(item)">
<template #headerExtra>
<van-tag :type="item.status === '未解除' ? 'danger' : 'success'" plain size="medium">
{{ item.status }}
<van-tag :type="getEventStatusType(item.eventStatus)" plain size="medium">
{{ getEventStatusText(item.eventStatus) }}
</van-tag>
</template>
<div class="info-block">
<div class="time-box">
<span class="info-label">发生时间</span>
<span class="info-value">{{ item.occurTime }}</span>
<span class="info-value">{{ item.occurTime || '-' }}</span>
</div>
<div class="time-box">
<span class="info-label">预计恢复时间</span>
<span class="info-value">{{ item.estimateRecoverTime }}</span>
<span class="info-value">{{ item.expectRecoverTime || '-' }}</span>
</div>
<div class="disaster-type-wrapper">
<van-tag type="primary" size="medium" plain>{{ item.disasterType }}</van-tag>
<van-tag class="tag" type="primary" size="medium" plain>{{ getDisasterTypeText(item) }}</van-tag>
</div>
</div>
@ -34,7 +34,7 @@
<EmptyBox v-if="!loading && list.length === 0" :placeholder="emptyText" />
</div>
<van-button type="primary" class="footer-btn" @click="handleAdd"> 灾害填报 </van-button>
<van-button type="primary" class="footer-btn" @click="handleAdd">灾害填报</van-button>
<!-- 筛选组件v-model 绑定选中的值visible 控制显示隐藏 -->
<TagFilter
@ -78,7 +78,7 @@ const showFilter = ref(false)
// v-model TagFilter
const selectedDisasterType = ref('all')
//
//
const disasterTypes = [
{ label: '全部', value: 'all' },
{ label: '边坡坍塌', value: '边坡坍塌' },
@ -91,6 +91,53 @@ const disasterTypes = [
{ label: '其他', value: '其他' }
]
//
const getEventTitle = (item) => {
// 使 eventName使+线
if (item.eventName) {
return item.eventName
}
const parts = []
if (item.routeNo) parts.push(item.routeNo)
if (item.occurLocation) parts.push(item.occurLocation)
if (item.startStakeNo) parts.push(item.startStakeNo)
if (parts.length > 0) {
return parts.join(' ')
}
return '未命名事件'
}
//
const getEventStatusText = (status) => {
// eventStatus: 0- 1-
return status === 1 ? '已解除' : '未解除'
}
//
const getEventStatusType = (status) => {
return status === 1 ? 'success' : 'danger'
}
//
const getDisasterTypeText = (item) => {
// roadConditionTyperepairProgress
//
// 1
if (item.repairProgress) {
return item.repairProgress
}
// 2
if (item.roadConditionType) {
return item.roadConditionType
}
// 3""
return '水毁灾害'
}
//
const getShortTypeName = (type) => {
const typeMap = {
@ -101,9 +148,15 @@ const getShortTypeName = (type) => {
'行道树倒塌': '树倒',
'积水': '积水',
'积雪': '积雪',
'其他': '其他'
'其他': '其他',
'未抢修': '待抢修',
'抢修中': '抢修中',
'已完成': '已完成',
'高速公路': '高速',
'国道': '国道',
'省道': '省道'
}
return typeMap[type] || type.substring(0, 2)
return typeMap[type] || (type ? type.substring(0, 2) : '灾害')
}
//
@ -115,12 +168,20 @@ const getDisasterList = async (keyword = '', disasterType = 'all') => {
url: '/snow-ops-platform/water-damage/list',
method: 'get',
params: {
keyword: keyword.trim(),
disasterType: disasterType === 'all' ? '' : disasterType
keyword: keyword.trim(),
disasterType: disasterType === 'all' ? '' : disasterType
}
})
if (result?.data?.records) {
list.value = result.data.records
list.value = result.data.records.map(item => ({
...item,
// 使
title: getEventTitle(item),
status: getEventStatusText(item.eventStatus),
occurTime: item.occurTime,
estimateRecoverTime: item.expectRecoverTime,
disasterType: getDisasterTypeText(item)
}))
} else {
showToast(result.message || '获取数据失败')
list.value = []
@ -162,14 +223,9 @@ const handleClickBack = () => {
//
const handleClickItem = (item) => {
router.push({
path: '/disaster-detail',
path: '/disasterDetail',
query: {
id: item.id,
title: item.title,
status: item.status,
occurTime: item.occurTime,
estimateRecoverTime: item.estimateRecoverTime,
disasterType: item.disasterType
id: item.id
}
})
}
@ -271,4 +327,7 @@ onMounted(() => {
border-color: #1989fa;
}
}
:deep(.van-tag) {
white-space: nowrap;
}
</style>

View File

@ -1,10 +1,10 @@
<template>
<PageContainer title="灾毁填报" @click-back="handleClickBack" class="page-container">
<PageContainer :title="title" @click-back="handleClickBack" class="page-container">
<!-- 当前站点信息 -->
<CurrentSite />
<!-- 事件类型 -->
<PanelItem title="事件类型">
<PanelItem title="事件类型" style="margin-bottom: 10px;" v-if="!isContinue">
<van-radio-group v-model="eventType" direction="horizontal" class="event-type-group">
<van-radio name="water">水毁灾害</van-radio>
<van-radio name="ice">冰雪灾害</van-radio>
@ -30,8 +30,8 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast, showSuccessToast, showFailToast } from 'vant'
import PageContainer from '@/components/PageContainer.vue'
import CurrentSite from '@/components/CurrentSite.vue'
@ -41,6 +41,12 @@ import { request } from "@shared/utils/request";
import mockFormData from './waterDisasterFormData.json'
const router = useRouter()
const route = useRoute()
//
const isContinue = computed(() => route.query.isContinue)
const title = ref(!isContinue ? '灾毁填报' : '灾毁续报')
//
const eventType = ref('water')
@ -83,16 +89,17 @@ const handleSubmit = async () => {
method: 'post',
data: submitData
})
//
await new Promise(resolve => setTimeout(resolve, 1000))
showSuccessToast('提交成功')
if(res?.code === '00000') {
showSuccessToast('提交成功')
} else {
showFailToast(res.message)
}
//
setTimeout(() => {
router.replace('/disasterManagement')
}, 1500)
}, 1000)
} catch (error) {
showFailToast('提交失败,请重试')
console.error('提交失败:', error)
@ -101,8 +108,45 @@ const handleSubmit = async () => {
}
}
//
const getDisasterDetail = async () => {
const id = route.query.id
if (!id) {
return
}
try {
const result = await request({
url: `/snow-ops-platform/water-damage/getById`,
method: 'get',
params: { id }
})
if (result?.data) {
// Data
const data = result.data
const newFormData = {
...data,
lossList: null,
report: formData.value.report,
fileList: null
}
waterDisasterRef.value.initFormData(newFormData)
} else {
showToast(result.message || '获取详情失败')
}
} catch (error) {
console.error('获取灾毁详情失败:', error)
showToast('获取详情失败,请稍后重试')
}
}
onMounted(() => {
waterDisasterRef.value.initFormData(formData.value)
if(route.query?.id) {
getDisasterDetail()
} else {
waterDisasterRef.value.initFormData(formData.value)
}
})
</script>

View File

@ -2,7 +2,7 @@
<!-- 损失计算弹窗 -->
<van-dialog
v-model:show="visible"
title="塌方损失信息"
:title="itemName + '损失信息'"
show-cancel-button
@confirm="confirm"
@cancel="cancelLoss"
@ -11,21 +11,21 @@
>
<div class="loss-dialog-content">
<!-- 塌方长 -->
<van-field v-model="formData.length" label="塌方长" placeholder="请填写长度" type="digit" clearable>
<van-field v-model="formData.length" :label="itemName + '长'" placeholder="请填写长度" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<!-- 塌方宽 -->
<van-field v-model="formData.width" label="塌方宽" placeholder="请填写宽度" type="digit" clearable>
<van-field v-model="formData.width" :label="itemName + '宽'" placeholder="请填写宽度" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<!-- 塌方高 -->
<van-field v-model="formData.height" label="塌方高" placeholder="请填写高度" type="digit" clearable>
<van-field v-model="formData.height" :label="itemName + '高'" placeholder="请填写高度" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
@ -38,7 +38,7 @@
</template>
</van-field>
<van-field v-model="formData.totalPrice" label="塌方损失金额" placeholder="请填写金额" type="digit" clearable>
<van-field v-model="formData.totalAmount" :label="itemName + '损失金额'" placeholder="请填写金额" type="digit" clearable>
<template #button>
<span class="field-unit"></span>
</template>
@ -61,7 +61,9 @@ const formData = ref({
unitPrice: ''
})
const totalPrice = computed(() => {
const itemName = ref('')
const totalAmount = computed(() => {
const l = parseFloat(formData.value.length)
const w = parseFloat(formData.value.width)
const h = parseFloat(formData.value.height)
@ -84,14 +86,25 @@ const totalPrice = computed(() => {
return (l * w * h * price)
})
watch(totalPrice, ()=>{
formData.value.totalPrice = totalPrice.value
watch(totalAmount, ()=>{
formData.value.totalAmount = totalAmount.value
})
//
const show = () => {
const show = (item) => {
console.log("🚀 ~ show ~ item:", item)
formData.value.length = item.length
formData.value.width = item.width
formData.value.height = item.height
formData.value.unitPrice = item.unitPrice
formData.value.totalAmount = item.totalAmount
itemName.value = getItemName(item)
visible.value = true
}
const getItemName = (item) => {
return item.lossTypeName
}
//
const validateForm = () => {
const l = parseFloat(formData.length)
@ -125,11 +138,11 @@ const validateForm = () => {
//
const confirm = () => {
if(!formData.value.totalPrice) {
if(!formData.value.totalAmount) {
showToast('请填写损失金额')
return
}
emit('confirm', formData.value.totalPrice)
emit('confirm', formData.value)
//
resetForm()
visible.value = false
@ -147,7 +160,7 @@ const resetForm = () => {
formData.value.width = ''
formData.value.height = ''
formData.value.unitPrice = ''
formData.value.totalPrice
formData.value.totalAmount
}
//
const emit = defineEmits(['confirm'])

View File

@ -1,14 +1,15 @@
<template>
<div class="loss-list">
<template v-for="item in modelValue">
<van-field v-model="item.totalAmount" :label="getItemLabel(item)" placeholder="请填写" type="digit" @click="cubeCalculateDialog.show()">
<template v-for="(item, index) in modelValue">
<van-field v-model="item.totalAmount" :label="getItemLabel(item)" placeholder="请填写" type="digit" @click="showCalculateDialog(item, index)">
<template #button>
<span class="field-unit">/万元</span>
<span class="field-unit">{{ item.unit }}</span>
<van-icon @click.stop="removeItem(index)" class="remove-icon" name="delete-o" />
</template>
</van-field>
</template>
<van-button size="small" block type="primary" plain @click="addLoss">添加损失</van-button>
<CubeCalculateDialog ref="cubeCalculateDialog" />
<CubeCalculateDialog ref="cubeCalculateDialog" @confirm="confirmCalculate" />
<LossPicker ref="lossPicker" :options="options" @confirm="confirmAddLoss" />
</div>
</template>
@ -31,6 +32,9 @@ const props = defineProps({
const lossPicker = ref(null)
const options = ref({})
const dialogItemIndex = ref(null)
//
const addLoss = () => {
lossPicker.value?.show()
@ -40,6 +44,10 @@ const confirmAddLoss = (item) => {
emit('update:modelValue', [...props.modelValue, item])
}
const removeItem = (index) => {
emit('update:modelValue', [...props.modelValue.slice(0, index), ...props.modelValue.slice(index + 1)])
}
const cubeCalculateDialog = ref(null)
const getItemLabel = (item) => {
@ -62,6 +70,18 @@ const getLossDict = async (params) => {
})
}
const showCalculateDialog = (item, index) => {
if(item.unit?.indexOf('方') > -1) {
dialogItemIndex.value = index
cubeCalculateDialog.value?.show(item)
}
}
const confirmCalculate = (newValue) => {
const oldItem = props.modelValue[dialogItemIndex.value]
props.modelValue[dialogItemIndex.value] = { ...oldItem, ...newValue }
}
onMounted(async () => {
await getLossDict()
})
@ -109,4 +129,10 @@ onMounted(async () => {
}
}
}
.remove-icon {
margin-left: 5px;
font-size: 16px;
color: #ee0a24;
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="water-disaster">
<!-- 基本信息 -->
<PanelItem title="基本信息">
<PanelItem title="基本信息" v-if="!isContinue">
<van-form>
<!-- 路况类别 -->
<BasePicker v-model="formData.roadConditionType" :options="roadConditionOptions" label="路况类别" placeholder="请选择" />
@ -175,7 +175,7 @@
<!-- 是否需要恢复重建 (event.needsRecovery) -->
<BasePicker v-model="formData.event.needsRecovery" :options="needsRecoveryOptions" label="是否需要恢复重建" placeholder="请选择" />
<!-- 恢复重建预估费用 (event.estimatedRecoveryCost) -->
<van-field v-model="formData.event.estimatedRecoveryCost" label="恢复重建预估费用" placeholder="请填写" type="digit">
<van-field v-model="formData.event.estimatedRecoveryCost" v-if="!isContinue" label="恢复重建预估费用" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
@ -191,7 +191,12 @@ import PanelItem from '@/components/PanelItem.vue'
import BasePicker from '@/components/BasePicker.vue'
import BaseDatePicker from '@/components/BaseDatePicker.vue'
import LossList from './LossList.vue'
import { useRouter, useRoute } from 'vue-router'
const route = useRoute()
//
const isContinue = computed(() => route.query.isContinue)
//
const disposalMeasuresArray = ref([])

View File

@ -40,24 +40,6 @@
"totalLossAmount": 85.6
},
"lossList": [
{
"length": 50,
"width": 8.5,
"height": 2.5,
"unitPrice": 380,
"totalAmount": 38.9,
"lossCategory": "路面损毁",
"remark": "沥青路面严重损坏"
},
{
"length": 30,
"width": 2.5,
"height": 6,
"unitPrice": 520,
"totalAmount": 23.4,
"lossCategory": "挡墙损毁",
"remark": "浆砌片石挡墙垮塌"
}
],
"fileList": []
}

View File

@ -66,7 +66,6 @@ const routes = [
breadcrumb: true
},
component: () => import('../views/airSkyLand/AirSkyLand.vue')
},
{
path: '/3DSituationalAwareness',
@ -74,7 +73,7 @@ const routes = [
meta: {
title: '三维态势感知',
screen: true,
skipInitialCameraView: true, // 跳过MapViewport的自动初始视图由页面自己控制相机
skipInitialCameraView: true, // 跳过MapViewport的自动初始视图由页面自己控制相机
breadcrumb: true
},
component: () => import('../views/3DSituationalAwarenessRefactor/index.vue')
@ -149,6 +148,24 @@ const routes = [
title: '项目管理',
breadcrumb: true
}
},
{
path: '/disasterManagement',
name: 'disasterManagement',
component: () => import('../views/DisasterManagement/DisasterManagement.vue'),
meta: {
title: '灾害巡检事件',
breadcrumb: true
}
},
{
path: '/disasterReport',
name: 'DisasterReport',
component: () => import('../views/DisasterManagement/DisasterReport/DisasterReport.vue'),
meta: {
title: '水毁事件填报',
breadcrumb: true
}
}
]

View File

@ -0,0 +1,403 @@
<template>
<div class="page-container">
<!-- 筛选栏 -->
<el-card class="filter-card" shadow="never">
<el-form :model="filterForm" label-width="100px" @submit.prevent>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="所属单位">
<el-input v-model="filterForm.unit" placeholder="请输入" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="发生地点">
<el-input v-model="filterForm.occurLocation" placeholder="请输入" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="路线编号">
<el-input v-model="filterForm.routeNo" placeholder="请输入" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="事件类别">
<el-select v-model="filterForm.disasterType" placeholder="请选择" clearable>
<el-option label="水毁事件" value="水毁事件" />
<el-option label="塌方事件" value="塌方事件" />
<el-option label="泥石流" value="泥石流" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="事件状态">
<el-select v-model="filterForm.eventStatus" placeholder="请选择" clearable>
<el-option label="未解除" :value="0" />
<el-option label="已解除" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="是否阻断">
<el-select v-model="filterForm.isBlocked" placeholder="请选择" clearable>
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="路线类型">
<el-select v-model="filterForm.roadConditionType" placeholder="请选择" clearable>
<el-option label="国省道" value="国省道" />
<el-option label="县乡道" value="县乡道" />
<el-option label="高速" value="高速" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="日期">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="toReport">新增</el-button>
<el-button type="success" @click="handleExport">导出Excel</el-button>
<el-button @click="resetFilters">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 数据表格卡片 -->
<el-card class="table-card" shadow="never">
<div class="table-container">
<el-table :data="tableData" border stripe v-loading="loading" style="width: 100%" height="100%">
<el-table-column label="序号" width="60">
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="routeNo" label="所属区县" min-width="120" />
<el-table-column prop="routeNo" label="路线编号" min-width="120" />
<el-table-column prop="routeName" label="路线名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="startPile" label="起点桩号" min-width="110" />
<el-table-column prop="endPile" label="终点桩号" min-width="110" />
<el-table-column prop="occurLocation" label="发生地点" min-width="180" show-overflow-tooltip />
<el-table-column prop="isBlocked" label="是否阻断" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isBlocked === '是' ? 'danger' : 'success'" size="small">
{{ row.isBlocked || '—' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="disasterType" label="灾害类型" min-width="120" />
<el-table-column prop="roadConditionType" label="路线类别" min-width="110" />
<el-table-column prop="eventStatus" label="事件状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.eventStatus === 1 ? 'success' : 'danger'" size="small">
{{ row.eventStatus === 1 ? '已解除' : '未解除' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="measure" label="处理措施" min-width="120" show-overflow-tooltip />
<el-table-column prop="occurTime" label="发生时间" width="170" />
<el-table-column prop="expectRecoverTime" label="预计恢复时间" width="170" />
<el-table-column prop="contactPerson" label="联系人" width="110" />
<el-table-column prop="contactPhone" label="联系电话" width="120" />
<el-table-column label="图片" width="80" align="center">
<template #default="{ row }">
<el-button v-if="row.hasImage" link type="primary" size="small" @click="viewImages(row)">查看</el-button>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="视频" width="80" align="center">
<template #default="{ row }">
<el-button v-if="row.hasVideo" link type="primary" size="small" @click="viewVideos(row)">播放</el-button>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="handleDetail(row)">详情</el-button>
<el-button link type="warning" size="small" @click="handleEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { request } from '@/utils/request'
const router = useRouter()
//
const filterForm = reactive({
unit: '', //
occurLocation: '', //
routeNo: '', // 线
disasterType: '', // /
eventStatus: '', // (0-, 1-)
isBlocked: '', //
roadConditionType: '', // 线
pushStatus: '' //
})
//
const dateRange = ref(null)
//
const tableData = ref([])
const loading = ref(false)
//
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
//
const buildQueryParams = () => {
const params = {
pageNum: pagination.currentPage,
pageSize: pagination.pageSize,
unit: filterForm.unit,
occurLocation: filterForm.occurLocation,
routeNo: filterForm.routeNo,
disasterType: filterForm.disasterType,
eventStatus: filterForm.eventStatus,
isBlocked: filterForm.isBlocked,
roadConditionType: filterForm.roadConditionType
}
if (dateRange.value && dateRange.value.length === 2) {
params.startDate = dateRange.value[0]
params.endDate = dateRange.value[1]
}
// null
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === null || params[key] === undefined) {
delete params[key]
}
})
return params
}
//
const fetchData = async () => {
loading.value = true
try {
const params = buildQueryParams()
//
const response = await request({
url: '/snow-ops-platform/unified-disaster/list',
method: 'get',
params
})
if (response.code === '00000') {
const records = response.data.records || response.data || []
tableData.value = records.map((item) => ({
...item,
//
eventStatusText: item.eventStatus === 1 ? '已解除' : '未解除',
//
...item
}))
pagination.total = response.data.total || records.length
} else {
ElMessage.error(response.message || '查询失败')
}
} catch (error) {
ElMessage.error('数据加载失败')
console.error(error)
} finally {
loading.value = false
}
}
//
const handleQuery = () => {
pagination.currentPage = 1
fetchData()
}
// Excel
const handleExport = async () => {
try {
const params = buildQueryParams()
const response = await request({
url: '/api/disaster/record/export',
method: 'post',
data: params,
responseType: 'blob'
})
//
const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.href = url
link.download = `水毁事件列表_${new Date().getTime()}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error) {
ElMessage.error('导出失败')
console.error(error)
}
}
//
const resetFilters = () => {
filterForm.unit = ''
filterForm.occurLocation = ''
filterForm.routeNo = ''
filterForm.disasterType = ''
filterForm.eventStatus = ''
filterForm.isBlocked = ''
filterForm.roadConditionType = ''
filterForm.pushStatus = ''
dateRange.value = null
handleQuery()
}
//
const handleDetail = (row) => {
router.push({ path: '/disasterDetail', query: { id: row.id } })
}
//
const handleEdit = (row) => {
router.push({ path: '/disasterReport', query: { id: row.id, mode: 'edit' } })
}
//
const toReport = () => {
router.push('/disasterReport')
}
//
const viewImages = (row) => {
if (row.images && row.images.length) {
//
ElMessage.info('图片预览功能开发中')
} else {
ElMessage.info('暂无图片')
}
}
//
const viewVideos = (row) => {
if (row.videos && row.videos.length) {
ElMessage.info('视频播放功能开发中')
} else {
ElMessage.info('暂无视频')
}
}
//
const handleSizeChange = (val) => {
pagination.pageSize = val
fetchData()
}
//
const handleCurrentChange = (val) => {
pagination.currentPage = val
fetchData()
}
//
onMounted(() => {
fetchData()
})
</script>
<style scoped lang="scss">
.page-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 20px;
background-color: #f5f7fa;
box-sizing: border-box;
overflow: hidden;
.filter-card {
flex-shrink: 0;
margin-bottom: 20px;
:deep(.el-card__body) {
padding: 20px;
}
}
.table-card {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.el-card__body) {
flex: 1;
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
}
}
.table-container {
flex: 1;
overflow: hidden;
padding: 0 20px;
:deep(.el-table) {
--el-table-border-color: #ebeef5;
}
}
.pagination-container {
flex-shrink: 0;
padding: 16px 20px;
display: flex;
justify-content: flex-end;
border-top: 1px solid #ebeef5;
}
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<div class="disaster-report">
<el-page-header @back="handleBack" class="page-header">
<template #content>
<span class="title">{{ isEdit ? '灾毁填报' : '灾毁填报' }}</span>
</template>
</el-page-header>
<WaterDisasterReport />
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Upload } from '@element-plus/icons-vue'
import { request } from '@/utils/request'
import WaterDisasterReport from './WaterDisasterReport.vue'
const router = useRouter()
const route = useRoute()
const isEdit = computed(() => !!route.query.id)
const handleBack = () => {
router.back()
}
</script>
<style scoped lang="scss">
.disaster-report {
padding: 20px;
background-color: #f5f7fa;
height: 100%;
overflow: auto;
.page-header {
margin-bottom: 20px;
padding: 0 0 16px 0;
border-bottom: 1px solid #e4e7ed;
.title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
}
.disaster-form {
.form-section {
margin-bottom: 20px;
:deep(.el-card__header) {
padding: 12px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
:deep(.el-card__body) {
padding: 20px;
}
}
.section-header {
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
position: relative;
padding-left: 10px;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
}
}
.sub-section-title {
font-size: 14px;
font-weight: 500;
color: #606266;
margin: 8px 0 16px 0;
padding-left: 8px;
border-left: 3px solid #409eff;
}
.unit-suffix {
margin-left: 8px;
color: #909399;
font-size: 12px;
}
.upload-tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
.form-actions {
display: flex;
justify-content: center;
gap: 16px;
padding: 20px 0 40px;
}
}
}
</style>

View File

@ -0,0 +1,735 @@
<template>
<div class="disaster-form-page">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px" class="disaster-form" @submit.prevent>
<!-- 基础信息区块 -->
<el-card class="form-section" shadow="never">
<template #header>
<div class="section-header">
<span class="section-title">基础信息</span>
</div>
</template>
<el-row :gutter="24">
<!-- 填报人员信息 -->
<el-col :span="24">
<div class="sub-section-title">填报人员信息</div>
</el-col>
<el-col :span="8">
<el-form-item label="填报单位" prop="reportUnit">
<el-input v-model="formData.reportUnit" placeholder="请输入填报单位" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系人" prop="contactPerson">
<el-input v-model="formData.contactPerson" placeholder="请输入联系人" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
<!-- 路况事件信息 -->
<el-col :span="24">
<div class="sub-section-title">路况事件信息</div>
</el-col>
<el-col :span="8">
<el-form-item label="事件类型" prop="eventType">
<el-select v-model="formData.eventType" placeholder="请选择" style="width: 100%">
<el-option label="水毁灾害" value="水毁灾害" />
<el-option label="冰雪灾害" value="冰雪灾害" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="路况类别" prop="blockedMileage">
<el-input-number v-model="formData.blockedMileage" :min="0" :precision="3" style="width: 100%" placeholder="请填写" />
<span class="unit-suffix">公里</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="是否阻断" prop="isBlocked">
<el-select v-model="formData.isBlocked" placeholder="请选择" style="width: 100%">
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="抢险进度" prop="isBlocked">
<el-select v-model="formData.isBlocked" placeholder="请选择" style="width: 100%">
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="处理措施" prop="measure">
<el-select v-model="formData.measure" placeholder="请选择" style="width: 100%">
<el-option label="正常通行" value="正常通行" />
<el-option label="交通管制" value="交通管制" />
<el-option label="封闭施工" value="封闭施工" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="水毁处数" prop="waterDamageCount">
<el-input-number v-model="formData.waterDamageCount" :min="0" :step="1" style="width: 100%" placeholder="请填写" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-row>
<el-col :span="8">
<el-form-item label="阻断里程" prop="blockedMileage">
<el-input-number v-model="formData.blockedMileage" :min="0" :precision="3" style="width: 100%" placeholder="请填写" />
<span class="unit-suffix">公里</span>
</el-form-item>
</el-col>
</el-row>
</el-col>
<el-col :span="8">
<el-form-item label="发现时间" prop="discoverTime">
<el-date-picker v-model="formData.discoverTime" type="datetime" placeholder="请选择日期时间" style="width: 100%" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="预计恢复" prop="predictRecoverTime">
<el-date-picker v-model="formData.predictRecoverTime" type="datetime" placeholder="请选择日期时间" style="width: 100%" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="现场描述" prop="siteDescription">
<el-input v-model="formData.siteDescription" type="textarea" :rows="3" placeholder="请输入现场描述及续行方案" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 位置信息区块 -->
<el-card class="form-section" shadow="never">
<template #header>
<div class="section-header">
<span class="section-title">位置信息</span>
</div>
</template>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="路线类型" prop="routeType">
<el-select v-model="formData.routeType" placeholder="请选择" style="width: 100%">
<el-option label="国省道" value="国省道" />
<el-option label="县乡道" value="县乡道" />
<el-option label="高速" value="高速" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="所属区县" prop="district">
<el-select v-model="formData.district" placeholder="请选择" style="width: 100%">
<el-option label="万州区" value="万州区" />
<el-option label="开州区" value="开州区" />
<el-option label="黔江区" value="黔江区" />
<el-option label="涪陵区" value="涪陵区" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="地点路线" prop="routeCode">
<el-input v-model="formData.routeCode" placeholder="请输入地点路线" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="起点桩号" prop="startPile">
<el-input v-model="formData.startPile" placeholder="K_请填写" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="止点桩号" prop="endPile">
<el-input v-model="formData.endPile" placeholder="K_请填写" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="路况位置" prop="locationPath">
<el-cascader v-model="formData.locationPath" :options="locationOptions" placeholder="请选择" style="width: 100%" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="阻断点小地名" prop="smallPlaceName">
<el-input v-model="formData.smallPlaceName" placeholder="请填写" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="经度" prop="longitude">
<el-input v-model="formData.longitude" placeholder="请输入经度" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="纬度" prop="latitude">
<el-input v-model="formData.latitude" placeholder="请输入纬度" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 附件上传区块 -->
<el-card class="form-section" shadow="never">
<template #header>
<div class="section-header">
<span class="section-title">附件上传</span>
</div>
</template>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="图片上传" prop="images">
<el-upload
v-model:file-list="formData.images"
action="#"
list-type="picture-card"
:auto-upload="false"
:limit="9"
:on-preview="handlePicturePreview"
:on-remove="handlePictureRemove"
:before-upload="beforeImageUpload"
accept="image/jpeg,image/png"
>
<el-icon><Plus /></el-icon>
</el-upload>
<div class="upload-tip">只能上传jpg/png格式且不超过500kb</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="视频上传" prop="videos">
<el-upload v-model:file-list="formData.videos" action="#" :auto-upload="false" :limit="3" :before-upload="beforeVideoUpload" accept="video/*">
<el-button type="primary"
><el-icon><Upload /></el-icon> 选择文件</el-button
>
</el-upload>
<div class="upload-tip">仅支持3M以内的视频</div>
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 灾毁损失区块 -->
<el-card class="form-section" shadow="never">
<template #header>
<div class="section-header">
<span class="section-title">灾毁损失</span>
</div>
</template>
<el-row :gutter="24">
<el-col :span="24">
<div class="sub-section-title">路况事件信息</div>
</el-col>
<el-col :span="8">
<el-form-item label="受伤人员" prop="injuredCount">
<el-input-number v-model="formData.injuredCount" :min="0" :step="1" style="width: 100%" />
<span class="unit-suffix"></span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="死亡人员" prop="deathCount">
<el-input-number v-model="formData.deathCount" :min="0" :step="1" style="width: 100%" />
<span class="unit-suffix"></span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="滞留人员" prop="deathCount">
<el-input-number v-model="formData.deathCount" :min="0" :step="1" style="width: 100%" />
<span class="unit-suffix"></span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="滞留车辆" prop="strandedVehicles">
<el-input-number v-model="formData.strandedVehicles" :min="0" :step="1" style="width: 100%" />
<span class="unit-suffix"></span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="损坏车辆" prop="damagedVehicles">
<el-input-number v-model="formData.damagedVehicles" :min="0" :step="1" style="width: 100%" />
<span class="unit-suffix"></span>
</el-form-item>
</el-col>
<el-col :span="24">
<div class="sub-section-title">道路损失及其他</div>
</el-col>
<el-col :span="8">
<el-form-item label="清理方量" prop="clearVolume">
<el-input-number v-model="formData.clearVolume" :min="0" :precision="2" style="width: 100%" />
<span class="unit-suffix">立方米</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="填方及损失" prop="fillLoss">
<el-input v-model="formData.fillLoss" placeholder="填方及损失" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="路面及损失" prop="roadLoss">
<el-input v-model="formData.roadLoss" placeholder="路面及损失" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="护坡及损失" prop="slopeLoss">
<el-input v-model="formData.slopeLoss" placeholder="护坡及损失" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="桥梁及损失" prop="bridgeLoss">
<el-input v-model="formData.bridgeLoss" placeholder="桥梁及损失" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="隧道及损失" prop="tunnelLoss">
<el-input v-model="formData.tunnelLoss" placeholder="隧道及损失" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="涵洞及损失" prop="culvertLoss">
<el-input v-model="formData.culvertLoss" placeholder="涵洞及损失" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="护栏" prop="guardrailLength">
<el-input-number v-model="formData.guardrailLength" :min="0" :precision="3" style="width: 100%" />
<span class="unit-suffix">公里</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="行道树" prop="roadsideTrees">
<el-input-number v-model="formData.roadsideTrees" :min="0" :step="1" style="width: 100%" />
<span class="unit-suffix"></span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="其他损失" prop="otherLossAmount">
<el-input-number v-model="formData.otherLossAmount" :min="0" :precision="2" style="width: 100%" />
<span class="unit-suffix">万元</span>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="其他损失描述" prop="otherLossDesc">
<el-input v-model="formData.otherLossDesc" type="textarea" :rows="2" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="24">
<div class="sub-section-title">已投入机械</div>
</el-col>
<el-col :span="8">
<el-form-item label="投入机械" prop="inputMachinery">
<el-input-number v-model="formData.inputMachinery" :min="0" :precision="1" style="width: 100%" />
<span class="unit-suffix">/</span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="投入人力" prop="inputLabor">
<el-input-number v-model="formData.inputLabor" :min="0" :step="1" style="width: 100%" />
<span class="unit-suffix"></span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="投入资金" prop="inputFunds">
<el-input-number v-model="formData.inputFunds" :min="0" :precision="2" style="width: 100%" />
<span class="unit-suffix">万元</span>
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 恢复重建预估费用区块 -->
<el-card class="form-section" shadow="never">
<template #header>
<div class="section-header">
<span class="section-title">恢复重建预估费用</span>
</div>
</template>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="是否需要恢复重建" prop="needReconstruction">
<el-radio-group v-model="formData.needReconstruction">
<el-radio label="是"></el-radio>
<el-radio label="否"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="恢复重建预估费用" prop="reconstructionCost">
<el-input-number v-model="formData.reconstructionCost" :min="0" :precision="2" style="width: 100%" />
<span class="unit-suffix">万元</span>
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 提交按钮 -->
<div class="form-actions">
<el-button @click="handleBack">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">提交</el-button>
</div>
</el-form>
<!-- 图片预览对话框 -->
<el-dialog v-model="previewDialogVisible" title="图片预览" width="600px">
<img :src="previewImageUrl" style="width: 100%" alt="预览图片" />
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Upload } from '@element-plus/icons-vue'
import { request } from '@/utils/request'
const router = useRouter()
const route = useRoute()
const formRef = ref(null)
const submitting = ref(false)
//
const isEdit = computed(() => !!route.query.id)
//
const previewDialogVisible = ref(false)
const previewImageUrl = ref('')
//
const locationOptions = [
{
value: 'chongqing',
label: '重庆市',
children: [
{
value: 'wanzhou',
label: '万州区',
children: [
{ value: 'shuanghekou', label: '双河口街道' },
{ value: 'zhoujiaba', label: '周家坝街道' }
]
},
{
value: 'kaizhou',
label: '开州区',
children: [
{ value: 'hanfeng', label: '汉丰街道' },
{ value: 'wenfeng', label: '文峰街道' }
]
}
]
}
]
//
const formData = reactive({
//
reportUnit: '',
contactPerson: '',
contactPhone: '',
//
eventType: '',
dangerDesc: '',
measure: '',
isBlocked: '',
waterDamageCount: 0,
blockedMileage: 0,
discoverTime: '',
predictRecoverTime: '',
siteDescription: '',
//
routeType: '国省道',
district: '',
routeCode: '',
routeName: '',
startPile: '',
endPile: '',
locationPath: [],
smallPlaceName: '',
longitude: '',
latitude: '',
//
images: [],
videos: [],
//
injuredCount: 0,
deathCount: 0,
strandedVehicles: 0,
damagedVehicles: 0,
clearVolume: 0,
fillLoss: '',
roadLoss: '',
slopeLoss: '',
bridgeLoss: '',
tunnelLoss: '',
culvertLoss: '',
guardrailLength: 0,
roadsideTrees: 0,
otherLossAmount: 0,
otherLossDesc: '',
inputMachinery: 0,
inputLabor: 0,
inputFunds: 0,
//
needReconstruction: '是',
reconstructionCost: 0
})
//
const formRules = {
reportUnit: [{ required: true, message: '请输入填报单位', trigger: 'blur' }],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
eventType: [{ required: true, message: '请选择事件类型', trigger: 'change' }],
isBlocked: [{ required: true, message: '请选择是否阻断', trigger: 'change' }],
discoverTime: [{ required: true, message: '请选择发现时间', trigger: 'change' }],
routeType: [{ required: true, message: '请选择路线类型', trigger: 'change' }],
district: [{ required: true, message: '请选择所属区县', trigger: 'change' }],
routeCode: [{ required: true, message: '请输入路线编号', trigger: 'blur' }]
}
//
const beforeImageUpload = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
const isLt500k = file.size / 1024 < 500
if (!isJpgOrPng) {
ElMessage.error('只能上传 JPG/PNG 格式的图片!')
return false
}
if (!isLt500k) {
ElMessage.error('图片大小不能超过 500KB!')
return false
}
return false // false
}
//
const beforeVideoUpload = (file) => {
const isLt3M = file.size / 1024 / 1024 < 3
if (!isLt3M) {
ElMessage.error('视频大小不能超过 3MB!')
return false
}
return false // false
}
//
const handlePicturePreview = (file) => {
previewImageUrl.value = file.url
previewDialogVisible.value = true
}
//
const handlePictureRemove = (file, fileList) => {
formData.images = fileList
}
//
const handleBack = () => {
router.back()
}
//
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) {
ElMessage.warning('请完善表单信息')
return
}
submitting.value = true
try {
//
const submitData = {
...formData,
//
locationPath: formData.locationPath.join('/'),
//
imageList: formData.images.map((item) => item.raw),
videoList: formData.videos.map((item) => item.raw)
}
// 使
// if (isEdit.value) {
// await request({ url: `/api/water-damage/events/${route.query.id}`, method: 'put', data: submitData })
// ElMessage.success('')
// } else {
// await request({ url: '/api/water-damage/events', method: 'post', data: submitData })
// ElMessage.success('')
// }
await new Promise((resolve) => setTimeout(resolve, 500))
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
router.back()
} catch (error) {
ElMessage.error('提交失败,请重试')
console.error(error)
} finally {
submitting.value = false
}
})
}
//
const loadEditData = async () => {
if (!isEdit.value) return
try {
// 使
// const response = await request({ url: `/api/water-damage/events/${route.query.id}`, method: 'get' })
// const data = response.data
// Object.assign(formData, data)
//
await new Promise((resolve) => setTimeout(resolve, 300))
Object.assign(formData, {
reportUnit: '万州区公路中心',
contactPerson: '王强',
contactPhone: '15978906789',
eventType: '水毁事件',
dangerDesc: '抢险中',
measure: '交通管制',
isBlocked: '是',
waterDamageCount: 3,
blockedMileage: 1.25,
discoverTime: '2024-07-11 16:00:00',
predictRecoverTime: '2024-08-12 16:30:00',
siteDescription: '边坡塌方导致道路中断,正在组织抢通',
routeType: '国省道',
district: '万州区',
routeCode: 'S202',
routeName: '开云路',
startPile: 'K17.200',
endPile: 'K17.350',
locationPath: ['chongqing', 'wanzhou', 'shuanghekou'],
smallPlaceName: '三汇口',
longitude: '108.408',
latitude: '30.808',
injuredCount: 0,
deathCount: 0,
strandedVehicles: 5,
damagedVehicles: 1,
clearVolume: 1200,
guardrailLength: 0.15,
roadsideTrees: 8,
otherLossAmount: 10,
otherLossDesc: '部分边沟损毁',
inputMachinery: 6,
inputLabor: 25,
inputFunds: 8,
needReconstruction: '是',
reconstructionCost: 50
})
} catch (error) {
ElMessage.error('加载数据失败')
console.error(error)
}
}
onMounted(() => {
loadEditData()
})
</script>
<style scoped lang="scss">
.disaster-form-page {
padding: 20px;
background-color: #f5f7fa;
.disaster-form {
.form-section {
margin-bottom: 20px;
:deep(.el-card__header) {
padding: 12px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
:deep(.el-card__body) {
padding: 20px;
}
}
.section-header {
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
position: relative;
padding-left: 10px;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
}
}
.sub-section-title {
font-size: 14px;
font-weight: 500;
color: #606266;
margin: 8px 0 16px 0;
padding-left: 8px;
border-left: 3px solid #409eff;
}
.unit-suffix {
margin-left: 8px;
color: #909399;
font-size: 12px;
}
.upload-tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
.form-actions {
display: flex;
justify-content: center;
gap: 16px;
padding: 20px 0 40px;
}
}
}
</style>

View File

@ -0,0 +1,202 @@
[
{
"id": 1,
"reportUnit": "开州区交通建设发展中心",
"routeCode": "S202",
"routeName": "开云路",
"startPile": "17.200",
"endPile": "17.350",
"locationDesc": "三汇口",
"isBlocked": "是",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2024-07-11",
"predictRecoverTime": "2024-08-12",
"contactPerson": "郭玲雅",
"contactPhone": "18223732276",
"hasImage": true,
"hasVideo": false
},
{
"id": 2,
"reportUnit": "彭水苗族土家族自治县...",
"routeCode": "G319",
"routeName": "高雄-成都",
"startPile": "2272",
"endPile": "2273",
"locationDesc": "高谷",
"isBlocked": "否",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2025-06-04",
"predictRecoverTime": "2025-06-04",
"contactPerson": "朱小明",
"contactPhone": "18996180931",
"hasImage": false,
"hasVideo": false
},
{
"id": 3,
"reportUnit": "巫溪县交通局",
"routeCode": "S301",
"routeName": "溪城路",
"startPile": "40.970",
"endPile": "40.990",
"locationDesc": "重庆市巫溪县通城镇通江街",
"isBlocked": "否",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2025-07-04",
"predictRecoverTime": "2025-08-20",
"contactPerson": "宁阳",
"contactPhone": "18182310829",
"hasImage": true,
"hasVideo": false
},
{
"id": 4,
"reportUnit": "綦江区公路事务中心",
"routeCode": "S534",
"routeName": "一赶路",
"startPile": "132.550",
"endPile": "132.600",
"locationDesc": "重庆市綦江区赶水镇280县道..",
"isBlocked": "否",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2025-07-22",
"predictRecoverTime": "2025-07-22",
"contactPerson": "沈伟",
"contactPhone": "13637759650",
"hasImage": false,
"hasVideo": false
},
{
"id": 5,
"reportUnit": "酉阳土家族苗族自治县...",
"routeCode": "G211",
"routeName": "银川一榕江",
"startPile": "1828.8",
"endPile": "1828.81",
"locationDesc": "重庆市酉阳土家族苗族自治县.",
"isBlocked": "否",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2025-10-20",
"predictRecoverTime": "2025-10-21",
"contactPerson": "陈建琼",
"contactPhone": "",
"hasImage": false,
"hasVideo": false
},
{
"id": 6,
"reportUnit": "黔江区公路事务中心",
"routeCode": "S203",
"routeName": "黔彭路",
"startPile": "48.720",
"endPile": "48.750",
"locationDesc": "重庆市黔江区小南海镇209省道",
"isBlocked": "是",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2023-09-25",
"predictRecoverTime": "2023-09-25",
"contactPerson": "刘昭君",
"contactPhone": "13594943712",
"hasImage": true,
"hasVideo": false
},
{
"id": 7,
"reportUnit": "武隆区公路养护公司",
"routeCode": "S521",
"routeName": "后沧路",
"startPile": "7.050",
"endPile": "7.070",
"locationDesc": "代家窑脚",
"isBlocked": "是",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2020-06-21",
"predictRecoverTime": "2020-07-11",
"contactPerson": "向显凤",
"contactPhone": "18696935340",
"hasImage": false,
"hasVideo": false
},
{
"id": 8,
"reportUnit": "石柱县公路局",
"routeCode": "G350",
"routeName": "利川一炉霍",
"startPile": "87.850",
"endPile": "87.900",
"locationDesc": "三块石",
"isBlocked": "是",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2020-07-12",
"predictRecoverTime": "2020-07-12",
"contactPerson": "陈世良",
"contactPhone": "13609484536",
"hasImage": true,
"hasVideo": false
},
{
"id": 9,
"reportUnit": "梁平区公路局",
"routeCode": "G243",
"routeName": "开县一凭祥",
"startPile": "67.88",
"endPile": "67.885",
"locationDesc": "合兴街道老拱桥",
"isBlocked": "否",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2021-05-15",
"predictRecoverTime": "2021-05-15",
"contactPerson": "江勇",
"contactPhone": "13628409515",
"hasImage": false,
"hasVideo": false
},
{
"id": 10,
"reportUnit": "奉节公路事务中心",
"routeCode": "G348",
"routeName": "武汉一大理",
"startPile": "806.8",
"endPile": "806.86",
"locationDesc": "麻牛坨",
"isBlocked": "是",
"eventType": "水毁事件",
"routeCategory": "国省道",
"eventStatus": "已解除",
"measure": "正常通行",
"discoverTime": "2021-07-08",
"predictRecoverTime": "2021-07-10",
"contactPerson": "王大平",
"contactPhone": "13983502063",
"hasImage": true,
"hasVideo": false
}
]