feat: H5端冰雪灾害填报改造

This commit is contained in:
niedongsheng 2026-04-20 11:51:52 +08:00
parent 9979660218
commit d831a04968
12 changed files with 1075 additions and 646 deletions

View File

@ -212,7 +212,7 @@ const handleClickItem = (item) => {
}
if (item.disasterType === 'ICE_SNOW') {
router.push({
name: 'IceEventDetail',
path: '/iceDisasterDetail',
query: {
id: item.relationId
}

View File

@ -26,7 +26,7 @@ import PageContainer from '@/components/PageContainer.vue'
import CurrentSite from '@/components/CurrentSite.vue'
import PanelItem from '@/components/PanelItem.vue'
import WaterDisaster from './WaterDisaster/WaterDisaster.vue'
import IceDisaster from './IceDisaster.vue'
import IceDisaster from './IceDisaster/IceDisaster.vue'
import { request } from '@shared/utils/request'
const router = useRouter()
@ -38,7 +38,7 @@ const isContinue = computed(() => route.query.isContinue)
const title = ref(!isContinue ? '灾毁填报' : '灾毁续报')
//
const eventType = ref('water')
const eventType = ref(route.query.eventType ? route.query.eventType : 'water')
const eventTypeOptions = [
{
label: '水毁灾害',

View File

@ -1,601 +0,0 @@
<template>
<div class="home">
<div class="content">
<PanelItem title="基本信息">
<van-form class="IceEventAddForm" label-align="left" colon>
<van-field v-model="form.occurTime" label="发生时间" center>
<template #button>
<van-button plain round type="primary" size="mini" @click="getCurrentTime">校准时间</van-button>
</template>
</van-field>
<!-- 线路编号 (顶层 routeNo) -->
<RoadRoutesPicker v-model="form.routeNo" label="线路编号" placeholder="请线路" @change="handleRouteNoChange" />
<van-field v-model="form.event.occurLocation" label="发生地点" center placeholder="请填写" />
<van-field v-model="form.occurLocation" label="路况位置" center placeholder="请填写" />
<van-field v-model="form.event.startStakeNo" label="起点桩号" center placeholder="请填写" />
<van-field v-model="form.event.endStakeNo" label="止点桩号" center placeholder="请填写" />
<van-field v-model="form.event.disasterMileage" label="受灾里程" center type="number" placeholder="请填写" />
</van-form>
</PanelItem>
<PanelItem title="处置情况">
<van-form class="IceEventAddForm" label-align="left" colon>
<van-field label="处置措施" center>
<template #input>
<div class="disposal-buttons">
<van-button
v-for="item in options['iceDisposalMeasures'] || []"
:key="item.value"
plain
:type="form.report.disposalMeasures === item.value ? 'primary' : 'default'"
size="small"
@click="toggleDisposal(item.value)"
>
{{ item.label }}
</van-button>
</div>
</template>
</van-field>
<van-field v-model="form.report.expectRecoverTime" label="预计恢复时间" center placeholder="请选择" readonly clickable @click="showExpectPicker = true" />
<van-popup :show="showExpectPicker" round position="bottom" close-on-click-overlay @close="showExpectPicker = false">
<van-picker-group title="选择日期时间" :tabs="['选择日期', '选择时间']" @confirm="handleConfirmExpectTime" @cancel="showExpectPicker = false">
<van-date-picker v-model="expectDate" :min-date="minDate" :max-date="maxDate" />
<van-time-picker v-model="expectTime" />
</van-picker-group>
</van-popup>
</van-form>
</PanelItem>
<PanelItem title="实施情况">
<van-form class="IceEventAddForm" label-align="left" colon>
<van-field v-model="form.report.inputManpower" type="number" label="投入人力" center placeholder="请填写">
<template #extra> 人次 </template>
</van-field>
<van-field v-model="form.report.inputFunds" type="number" label="投入资金" center placeholder="请填写">
<template #extra> 万元 </template>
</van-field>
<van-field v-model="form.report.inputEquipment" type="number" label="投入设备" center placeholder="请填写">
<template #extra> 台班 </template>
</van-field>
<!-- 选择物资列表 -->
<van-field
v-for="(material, index) in form.yhzMaterialList"
:key="material.rid"
v-model="material.usageAmount"
type="number"
@input="checkMaterialAmount(material, index)"
:label="material.wzmc"
center
:placeholder="`余额: ${material.ye} `"
>
<template #extra>
<span style="margin-right: 10px">{{ material.dw }}</span>
<van-button size="small" type="danger" @click.stop="form.yhzMaterialList.splice(index, 1)"> 删除 </van-button>
</template>
</van-field>
<van-button class="add-wzbtn" type="primary" icon="plus" plain @click="handleOpenAddMaterial">添加物资 </van-button>
<van-popup :show="showAddMaterialPopup" position="bottom" close-on-click-overlay @close="showAddMaterialPopup = false">
<div style="padding: 16px">
<h3 style="text-align: center; margin-bottom: 16px">添加物资</h3>
<!-- 搜索框 -->
<van-field v-model="searchText" placeholder="输入物资名称搜索" clearable @update:model-value="handleSearch"> </van-field>
<van-checkbox-group v-model="checked">
<van-cell-group inset style="margin: 16px 0">
<div style="display: flex; justify-content: space-between; padding: 8px 16px">
<span> {{ materialList.length }} </span>
<van-button size="mini" @click="toggleSelectAll" :type="isAllSelected ? 'primary' : 'default'">
{{ isAllSelected ? '取消全选' : '全选' }}
</van-button>
</div>
<van-cell v-for="(item, index) in materialList" clickable :key="item.rid" :title="item.wzmc" @click="toggle(index)">
<template #right-icon>
<van-checkbox :name="item.rid" :ref="(el) => (checkboxRefs[index] = el)" @click.stop />
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<van-button type="primary" block @click="addSelectedMaterials" style="margin-top: 10px"> 确认添加 </van-button>
</div>
</van-popup>
<van-field label="有无车辆滞留" center>
<template #input>
<div class="disposal-buttons">
<van-button plain :type="form.report.hasStrandedVehicles === 1 ? 'primary' : 'default'" size="small" @click="form.report.hasStrandedVehicles = 1">
有滞留
</van-button>
<van-button
plain
:type="form.report.hasStrandedVehicles === 0 ? 'primary' : 'default'"
size="small"
@click="
() => {
form.report.hasStrandedVehicles = 0
form.report.strandedVehicleCount = null
}
"
class="last-button"
>
无滞留
</van-button>
</div>
</template>
</van-field>
<van-field v-if="form.report.hasStrandedVehicles === 1" v-model="form.report.strandedVehicleCount" type="number" label="滞留车辆数" center placeholder="请填写" />
<van-field v-model="form.report.actualRecoverTime" label="实际恢复时间" center placeholder="请选择" readonly clickable @click="showActualPicker = true" />
<van-popup :show="showActualPicker" round position="bottom" close-on-click-overlay @close="showActualPicker = false">
<van-picker-group title="选择日期时间" :tabs="['选择日期', '选择时间']" @confirm="handleConfirmActualTime" @cancel="showActualPicker = false">
<van-date-picker v-model="actualDate" :min-date="minDate" :max-date="maxDate" />
<van-time-picker v-model="actualTime" />
</van-picker-group>
</van-popup>
<van-field v-model="form.report.siteDescription" label="现场情况描述" type="textarea" rows="2" autosize center placeholder="请填写" />
<van-field label="附件" center>
<template #input>
<van-uploader
:modelValue="getFileList()"
name="photos"
:file-type="['image/jpeg', 'image/png']"
:after-read="afterRead"
multiple
:max-count="6"
@delete="removeFile"
/>
</template>
</van-field>
</van-form>
</PanelItem>
</div>
<van-button type="primary" class="add-btn" @click="handleAdd"> 提交 </van-button>
</div>
</template>
<script setup>
import { ref, onMounted, reactive, watch, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showLoadingToast } from 'vant'
import PanelItem from '@/components/PanelItem.vue'
import { request } from '../../../../shared/utils/request'
import { useYHZStore } from '@/stores/yhzStore'
import RoadRoutesPicker from './RoadRoutesPicker.vue'
import { useOptions } from '@shared/composables/useOptions'
const router = useRouter()
const yhzStore = useYHZStore()
const { options } = useOptions()
//
const INIT_FORM = reactive({
occurLocation: '',
occurTime: '',
routeNo: '',
event: {
occurLocation: '', //
startStakeNo: '', //
endStakeNo: '', //
startStakeLng: null,
startStakeLat: null,
endStakeLng: null,
endStakeLat: null,
disasterMileage: '', //
serviceStationId: '', // ID
district: '', //
reporterName: '',
reportTime: '', //
reporterPhone: '', //
reportUnit: '',
routeNo: '',
occurTime: '',
roadConditionLocation: '',
routeType: '',
createTime: '', //
updateTime: '', //
isDeleted: '' // 0- 1-
},
report: {
inputManpower: null, //
inputFunds: null, //
inputEquipment: null, //
disposalMeasures: '',
expectRecoverTime: '',
actualRecoverTime: '',
hasStrandedVehicles: 0,
strandedVehicleCount: null,
siteDescription: '',
reporterName: '',
reporterPhone: '',
reportTime: '',
industrialSalt: null,
antiSlipSand: null,
sandbags: null,
createTime: '', //
updateTime: '' //
},
yhzMaterialList: [], //
fileList: []
})
const form = reactive({ ...INIT_FORM })
const getFileList = () => {
return (
form.fileList?.map((item) => ({
url: item.fileUrl,
name: item.fileName
})) || []
)
}
//
const formatTime = (date = new Date()) => {
const pad = (n) => n.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
const getCurrentTime = () => {
form.occurTime = formatTime()
}
const toggleDisposal = (type) => {
form.report.disposalMeasures = form.report.disposalMeasures === type ? '' : type
}
//
onMounted(() => {
form.occurTime = formatTime() //
})
//
const showAddMaterialPopup = ref(false)
const materialList = ref([])
const checkboxRefs = ref([])
const checked = ref([])
const toggle = (index) => {
checkboxRefs.value[index].toggle()
}
const searchText = ref('')
const handleSearch = () => {
getMaterialList(searchText.value)
}
//
const toggleSelectAll = () => {
if (isAllSelected.value) {
checked.value = []
} else {
checked.value = materialList.value.map((item) => item.rid)
}
}
//
const isAllSelected = computed(() => {
return materialList.value.length > 0 && materialList.value.every((item) => checked.value.includes(item.rid))
})
//
const addSelectedMaterials = () => {
checked.value.forEach((rid) => {
const material = materialList.value.find((m) => m.rid === rid)
if (material && !form.yhzMaterialList.some((m) => m.rid === rid)) {
form.yhzMaterialList.push({
rid: rid,
wzmc: material.wzmc,
usageAmount: null,
dw: material.dw,
ye: material.ye
})
}
})
showAddMaterialPopup.value = false
checked.value = []
}
//
const checkMaterialAmount = (material, index) => {
if (material.usageAmount > material.ye) {
showToast({
type: 'fail',
message: '输入数量不能超过物资余额'
})
form.yhzMaterialList[index].usageAmount = material.ye
}
}
//
const getMaterialList = async (wzmc) => {
try {
const data = {
yhzid: yhzStore.getYHZInfo.id,
wzmc,
pageNum: 1,
pageSize: 9999
}
const res = await request({
url: '/snow-ops-platform/yjwz/list',
method: 'GET',
params: data
})
if (res.code === '00000') {
materialList.value = res.data.records
} else {
throw new Error(res.message)
}
} catch (error) {
showToast({
type: 'fail',
message: error.message
})
}
}
//
const handleOpenAddMaterial = async () => {
await getMaterialList()
showAddMaterialPopup.value = true
}
const handleAdd = async () => {
try {
const toast = showLoadingToast({
message: '上报中...',
forbidClick: true,
duration: 0 // 0
})
form.event.serviceStationId = yhzStore.getYHZInfo?.id || ''
form.event.district = yhzStore.getYHZInfo?.qxmc || ''
const reportTime = form.report.reportTime || form.event.reportTime || formatTime()
const submitData = {
...form,
event: {
...form.event,
routeNo: form.routeNo,
occurTime: form.occurTime,
reportTime,
reportUnit: form.event.reportUnit,
disposalMeasures: form.report.disposalMeasures,
expectRecoverTime: form.report.expectRecoverTime,
actualRecoverTime: form.report.actualRecoverTime || null,
roadConditionLocation: form.occurLocation,
eventType: '冰雪事件'
},
report: {
...form.report,
reporterName: form.report.reporterName || form.event.reporterName,
reporterPhone: form.report.reporterPhone || form.event.reporterPhone,
reportTime
}
}
if (submitData.report.hasStrandedVehicles !== 1) {
submitData.report.strandedVehicleCount = null
}
const res = await request({
url: '/snow-ops-platform/event/addOrUpdate',
method: 'POST',
data: submitData
})
if (res.code === '00000') {
toast.close()
showToast({
type: 'success',
message: '上报成功'
})
setTimeout(() => {
router.replace('/disasterManagement')
}, 500)
} else {
toast.close()
throw new Error(res.message)
}
} catch (error) {
showToast({
type: 'fail',
message: error.message
})
}
}
const expectDate = ref([])
const expectTime = ref([])
const actualDate = ref([])
const actualTime = ref([])
//
const showExpectPicker = ref(false)
const minDate = new Date()
const maxDate = new Date(2050, 11, 31)
const handleConfirmExpectTime = () => {
const [year, month, day] = expectDate.value
const [hour, minute] = expectTime.value
form.report.expectRecoverTime =
`${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ` + `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:00`
showExpectPicker.value = false
}
//
const showActualPicker = ref(false)
const handleConfirmActualTime = () => {
const [year, month, day] = actualDate.value
const [hour, minute] = actualTime.value
form.report.actualRecoverTime =
`${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ` + `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:00`
showActualPicker.value = false
}
//
watch(showExpectPicker, (val) => {
if (val) {
const current = form.report.expectRecoverTime ? new Date(form.report.expectRecoverTime) : new Date()
expectDate.value = [current.getFullYear(), current.getMonth() + 1, current.getDate()]
expectTime.value = [current.getHours(), current.getMinutes()]
}
})
watch(showActualPicker, (val) => {
if (val) {
const current = form.report.actualRecoverTime ? new Date(form.report.actualRecoverTime) : new Date()
actualDate.value = [current.getFullYear(), current.getMonth() + 1, current.getDate()]
actualTime.value = [current.getHours(), current.getMinutes()]
}
})
const parsePointValue = (point) => {
if (!point) {
return { longitude: null, latitude: null }
}
if (Array.isArray(point) && point.length >= 2) {
return {
longitude: point[0] ?? null,
latitude: point[1] ?? null
}
}
if (typeof point === 'string') {
try {
const parsed = JSON.parse(point)
if (Array.isArray(parsed) && parsed.length >= 2) {
return {
longitude: parsed[0] ?? null,
latitude: parsed[1] ?? null
}
}
} catch (_error) {
return { longitude: null, latitude: null }
}
}
return { longitude: null, latitude: null }
}
const handleRouteNoChange = (item) => {
form.routeNo = item.routeCode
form.event.startStakeNo = item.startStakeNo
form.event.endStakeNo = item.endStakeNo
const startPoint = parsePointValue(item.startPoint ?? item.startpoint)
const endPoint = parsePointValue(item.endPoint ?? item.endpoint)
form.event.startStakeLng = startPoint.longitude
form.event.startStakeLat = startPoint.latitude
form.event.endStakeLng = endPoint.longitude
form.event.endStakeLat = endPoint.latitude
}
//
const afterRead = async (file) => {
try {
const toast = showLoadingToast({
message: '上传中...',
forbidClick: true,
duration: 0 // 0
})
const formData = new FormData()
formData.append('file', file.file)
const res = await request({
url: '/snow-ops-platform/file/upload',
method: 'post',
data: formData
})
toast.close()
if (res.code === '00000') {
form.fileList.push({
fileName: file.file.name,
fileUrl: res.data,
fileType: 1,
fileSize: file.file.size
})
} else {
throw new Error(res.message)
}
} catch (error) {
toast.close()
showToast({
type: 'fail',
message: error.message
})
}
}
//
const removeFile = (file, index) => {
form.fileList.splice(index, 1)
}
</script>
<style scoped>
.home {
/* 自动匹配导航栏高度 */
}
.content {
}
.content .van-cell-group .van-cell {
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.add-wzbtn {
width: calc(100% - 32px);
margin: 10px 16px;
}
.add-btn {
position: fixed;
bottom: 20px;
left: 16px;
right: 16px;
width: calc(100% - 32px);
margin: 0 auto;
border-radius: 24px;
font-size: 16px;
height: 44px;
z-index: 999;
}
.grid {
margin-top: 16px;
}
.btn {
margin-top: 24px;
}
.status-tag {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
color: white;
font-size: 12px;
}
.status-good {
background-color: #07c160;
}
.status-warning {
background-color: #ff976a;
}
.status-danger {
background-color: #ee0a24;
}
.disposal-buttons {
display: flex;
gap: 10px;
padding: 8px 0;
}
.disposal-buttons .van-button {
flex: 1;
}
.last-button {
margin-right: 16px;
}
</style>

View File

@ -0,0 +1,535 @@
<template>
<PageContainer title="冰毁详情" @click-back="handleClickBack" class="page-container">
<!-- 当前站点信息 -->
<CurrentSite />
<!-- 基本信息 -->
<PanelItem title="基本信息" v-if="!loading">
<template #headerExtra>
<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(detailData.event?.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">
<span class="info-label">止点桩号</span>
<span class="info-value">{{ detailData.event?.endStakeNo || '-' }}</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?.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="填报信息" v-if="!loading">
<!-- 遍历所有填报记录首报 + 续报 -->
<div v-for="(report, index) in allReports" :key="index" class="report-section">
<div class="report-header">
<span class="report-title">{{ report?.title }}</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">{{ report.disposalMeasures || '-' }}</span>
</div>
<template v-for="(lossItem, idx) of report.lossList" :key="idx">
<div class="info-row">
<span class="info-label">{{ lossItem.lossCategory }}</span>
<span class="info-value">{{ lossItem.totalAmount }}{{ lossItem.unit }}</span>
</div>
<div class="info-row" v-if="lossItem.lossCategory == '其他损失'">
<span class="info-label">其它损失描述</span>
<span class="info-value">{{ lossItem.remark }}</span>
</div>
</template>
<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 column" 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">
<div class="preview-image-block" v-if="file.fileType === 1" @click="previewFile(report, file)">
<img :src="file.fileUrl" alt="" />
</div>
<div class="preview-video-block" v-else>
<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(report, file)">预览</van-button> -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 无填报信息时显示 -->
<EmptyBox v-if="!hasReportData" placeholder="暂无填报信息" />
</PanelItem>
<!-- 底部按钮未解除状态显示续报按钮 -->
<div class="footer-buttons" v-if="!loading">
<van-button type="primary" class="footer-btn" @click="handleContinueReport">续报</van-button>
</div>
<van-loading class="loading-icon" v-if="loading">加载中...</van-loading>
<van-image-preview :startPosition="startPosition" v-model:show="previewImagesVisible" :images="imagesForPreview">
<template v-slot:index="{ index }">{{ index + 1 }}</template>
</van-image-preview>
</PageContainer>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast, showImagePreview } 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: [], //
fileList: [], //
lossList: [], //
occurLocation: '',
occurTime: '',
roadConditionType: '',
routeNo: ''
})
const loading = ref(true)
const allReports = computed(() => {
const reports =
detailData.value.report?.map((item, index) => {
if (index === detailData.value.report.length - 1) {
item.title = '首报'
} else {
item.title = '续报' + (detailData.value.report.length - 1 - index)
}
return item
}) || []
return reports
// return reports.reverse()
})
//
const hasReportData = computed(() => {
return allReports.value.length > 0
})
//
const getEventStatusText = () => {
return detailData.eventStatus === 1 ? '已解除' : '未解除'
}
//
const getEventStatusType = () => {
return detailData.eventStatus === 1 ? 'success' : 'danger'
}
//
const formatDisposalMeasures = (measures) => {
if (!measures) return ''
const measureMap = {
半幅封闭: '半幅封闭',
全副封闭: '全副封闭',
便道通行: '便道通行',
正常通行: '正常通行'
}
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 ? `有车滞留` : '无车滞留'
}
//
const getDisasterDetail = async () => {
const id = route.query.id
if (!id) {
showToast('缺少灾毁ID')
return
}
loading.value = true
try {
const result = await request({
url: `/snow-ops-platform/event/getById`,
method: 'get',
params: { id }
})
if (result?.data) {
// Data
const data = result.data
detailData.value = {
event: data.event || null,
report: data.reportList || [], // 使 report
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('获取详情失败,请稍后重试')
}
loading.value = false
}
//
const handleClickBack = () => {
router.push('/disasterManagement')
}
//
const handleContinueReport = () => {
router.push({
path: '/disasterReport',
query: {
id: route.query.id,
eventType: 'ice',
isContinue: 'true'
}
})
}
const isImageFile = (file) => {
// url
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i
if (file.fileUrl && imageExtensions.test(file.fileUrl)) return true
//
if (file.type && file.type.startsWith('image/')) return true
return false
}
const imagesForPreview = ref([])
const previewImagesVisible = ref(false)
const startPosition = ref(0)
//
const previewFile = (report, file) => {
const images = report.fileList.filter((file) => isImageFile(file))
imagesForPreview.value = images.map((item) => item.fileUrl)
startPosition.value = imagesForPreview.value.indexOf(file.fileUrl)
previewImagesVisible.value = true
}
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;
}
&.column {
flex-direction: column;
.info-label {
margin-bottom: 10px;
}
}
}
.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 {
display: flex;
flex-wrap: wrap;
flex: 1;
gap: 10px;
overflow: hidden;
.attachment-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 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;
}
}
}
.preview-image-block {
width: 60px;
height: 60px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.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;
}
}
.loading-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View File

@ -0,0 +1,535 @@
<template>
<PageContainer title="冰毁详情" @click-back="handleClickBack" class="page-container">
<!-- 当前站点信息 -->
<CurrentSite />
<!-- 基本信息 -->
<PanelItem title="基本信息" v-if="!loading">
<template #headerExtra>
<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(detailData.event?.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">
<span class="info-label">止点桩号</span>
<span class="info-value">{{ detailData.event?.endStakeNo || '-' }}</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?.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="填报信息" v-if="!loading">
<!-- 遍历所有填报记录首报 + 续报 -->
<div v-for="(report, index) in allReports" :key="index" class="report-section">
<div class="report-header">
<span class="report-title">{{ report?.title }}</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">{{ report.disposalMeasures || '-' }}</span>
</div>
<template v-for="(lossItem, idx) of report.lossList" :key="idx">
<div class="info-row">
<span class="info-label">{{ lossItem.lossCategory }}</span>
<span class="info-value">{{ lossItem.totalAmount }}{{ lossItem.unit }}</span>
</div>
<div class="info-row" v-if="lossItem.lossCategory == '其他损失'">
<span class="info-label">其它损失描述</span>
<span class="info-value">{{ lossItem.remark }}</span>
</div>
</template>
<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 column" 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">
<div class="preview-image-block" v-if="file.fileType === 1" @click="previewFile(report, file)">
<img :src="file.fileUrl" alt="" />
</div>
<div class="preview-video-block" v-else>
<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(report, file)">预览</van-button> -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 无填报信息时显示 -->
<EmptyBox v-if="!hasReportData" placeholder="暂无填报信息" />
</PanelItem>
<!-- 底部按钮未解除状态显示续报按钮 -->
<div class="footer-buttons" v-if="!loading">
<van-button type="primary" class="footer-btn" @click="handleContinueReport">续报</van-button>
</div>
<van-loading class="loading-icon" v-if="loading">加载中...</van-loading>
<van-image-preview :startPosition="startPosition" v-model:show="previewImagesVisible" :images="imagesForPreview">
<template v-slot:index="{ index }">{{ index + 1 }}</template>
</van-image-preview>
</PageContainer>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast, showImagePreview } 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: [], //
fileList: [], //
lossList: [], //
occurLocation: '',
occurTime: '',
roadConditionType: '',
routeNo: ''
})
const loading = ref(true)
const allReports = computed(() => {
const reports =
detailData.value.report?.map((item, index) => {
if (index === detailData.value.report.length - 1) {
item.title = '首报'
} else {
item.title = '续报' + (detailData.value.report.length - 1 - index)
}
return item
}) || []
return reports
// return reports.reverse()
})
//
const hasReportData = computed(() => {
return allReports.value.length > 0
})
//
const getEventStatusText = () => {
return detailData.eventStatus === 1 ? '已解除' : '未解除'
}
//
const getEventStatusType = () => {
return detailData.eventStatus === 1 ? 'success' : 'danger'
}
//
const formatDisposalMeasures = (measures) => {
if (!measures) return ''
const measureMap = {
半幅封闭: '半幅封闭',
全副封闭: '全副封闭',
便道通行: '便道通行',
正常通行: '正常通行'
}
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 ? `有车滞留` : '无车滞留'
}
//
const getDisasterDetail = async () => {
const id = route.query.id
if (!id) {
showToast('缺少灾毁ID')
return
}
loading.value = true
try {
const result = await request({
url: `/snow-ops-platform/event/getById`,
method: 'get',
params: { id }
})
if (result?.data) {
// Data
const data = result.data
detailData.value = {
event: data.event || null,
report: data.reportList || [], // 使 report
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('获取详情失败,请稍后重试')
}
loading.value = false
}
//
const handleClickBack = () => {
router.push('/disasterManagement')
}
//
const handleContinueReport = () => {
router.push({
path: '/disasterReport',
query: {
id: route.query.id,
eventType: 'ice',
isContinue: 'true'
}
})
}
const isImageFile = (file) => {
// url
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i
if (file.fileUrl && imageExtensions.test(file.fileUrl)) return true
//
if (file.type && file.type.startsWith('image/')) return true
return false
}
const imagesForPreview = ref([])
const previewImagesVisible = ref(false)
const startPosition = ref(0)
//
const previewFile = (report, file) => {
const images = report.fileList.filter((file) => isImageFile(file))
imagesForPreview.value = images.map((item) => item.fileUrl)
startPosition.value = imagesForPreview.value.indexOf(file.fileUrl)
previewImagesVisible.value = true
}
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;
}
&.column {
flex-direction: column;
.info-label {
margin-bottom: 10px;
}
}
}
.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 {
display: flex;
flex-wrap: wrap;
flex: 1;
gap: 10px;
overflow: hidden;
.attachment-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 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;
}
}
}
.preview-image-block {
width: 60px;
height: 60px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.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;
}
}
.loading-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View File

@ -161,7 +161,7 @@ import PanelItem from '@/components/PanelItem.vue'
import BasePicker from '@/components/BasePicker.vue'
import BaseDatePicker from '@/components/BaseDatePicker.vue'
import RoadRoutesPicker from '../RoadRoutesPicker.vue'
import LossList from './LossList.vue'
import LossList from './components/LossList.vue'
import { useRouter, useRoute } from 'vue-router'
import { request } from '@shared/utils/request'
import { useOptions } from '@shared/composables/useOptions'

View File

@ -1,5 +1,5 @@
<template>
<PageContainer title="毁详情" @click-back="handleClickBack" class="page-container">
<PageContainer title="毁详情" @click-back="handleClickBack" class="page-container">
<!-- 当前站点信息 -->
<CurrentSite />

View File

@ -1,40 +0,0 @@
{
"occurLocation": "G108国道 K2250+300处",
"occurTime": null,
"roadConditionType": "国道",
"routeNo": "G108",
"event": {
"blockedMileage": 1.5,
"blockedPointName": "磨盘山隧道口",
"contactPerson": "张明",
"contactPhone": "13812345678",
"damageCount": 3,
"district": "武侯区",
"endStakeNo": "K2251+200",
"estimatedRecoveryCost": 120.5,
"isBlocked": true,
"needsRecovery": true,
"repairProgress": "抢险中",
"reporterUnit": "武侯区交通运输局",
"startStakeNo": "K2250+300"
},
"report": {
"damagedVehicleCount": 2,
"strandedPersonCount": 12,
"deadCount": 0,
"strandedVehicleCount": 12,
"disposalMeasures": null,
"actualRecoverTime": null,
"expectRecoverTime": null,
"injuredCount": 1,
"investedFunds": 35.8,
"investedMachinery": 6,
"investedManpower": 45,
"remark": "已组织抢险队伍进行抢通,便道已修建完成",
"siteDescription": "因持续强降雨导致山体滑坡掩埋路面约50米边坡垮塌严重",
"totalLossAmount": 85.6
},
"lossList": [
],
"fileList": []
}