feat: 水毁灾害

This commit is contained in:
niedongsheng 2026-04-10 11:38:24 +08:00
parent af4eb34c4c
commit 9ec3f14585
13 changed files with 663 additions and 234 deletions

View File

@ -31,9 +31,9 @@
<span class="info-value">{{ detailData.event?.isBlocked ? '是' : '否' }}</span> <span class="info-value">{{ detailData.event?.isBlocked ? '是' : '否' }}</span>
</div> </div>
<!-- 进度 --> <!-- 进度 -->
<div class="info-row"> <div class="info-row">
<span class="info-label">进度</span> <span class="info-label">进度</span>
<span class="info-value">{{ detailData.event?.repairProgress || '-' }}</span> <span class="info-value">{{ detailData.event?.repairProgress || '-' }}</span>
</div> </div>
@ -165,7 +165,7 @@
</div> </div>
<div class="report-content"> <div class="report-content">
<div class="info-row"> <div class="info-row">
<span class="info-label">处置情况</span> <span class="info-label">处置措施</span>
<span class="info-value">{{ formatDisposalMeasures(report.disposalMeasures) || '-' }}</span> <span class="info-value">{{ formatDisposalMeasures(report.disposalMeasures) || '-' }}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
@ -274,10 +274,10 @@ const getEventStatusType = () => {
const formatDisposalMeasures = (measures) => { const formatDisposalMeasures = (measures) => {
if (!measures) return '' if (!measures) return ''
const measureMap = { const measureMap = {
halfClose: '半幅封闭', '半幅封闭': '半幅封闭',
fullClose: '全副封闭', '全副封闭': '全副封闭',
bypass: '便道通行', '便道通行': '便道通行',
normal: '正常通行' '正常通行': '正常通行'
} }
return measures return measures
.split(',') .split(',')

View File

@ -124,7 +124,7 @@ const getDisasterTypeText = (item) => {
// roadConditionTyperepairProgress // roadConditionTyperepairProgress
// //
// 1 // 1
if (item.repairProgress) { if (item.repairProgress) {
return item.repairProgress return item.repairProgress
} }
@ -149,8 +149,8 @@ const getShortTypeName = (type) => {
'积水': '积水', '积水': '积水',
'积雪': '积雪', '积雪': '积雪',
'其他': '其他', '其他': '其他',
'未抢修': '待抢修', '未抢险': '待抢险',
'抢修中': '抢修中', '抢险中': '抢险中',
'已完成': '已完成', '已完成': '已完成',
'高速公路': '高速', '高速公路': '高速',
'国道': '国道', '国道': '国道',

View File

@ -9,8 +9,8 @@
<!-- 是否阻断 (event.isBlocked) --> <!-- 是否阻断 (event.isBlocked) -->
<BasePicker v-model="formData.event.isBlocked" :options="blockedOptions" label="是否阻断" placeholder="请选择" /> <BasePicker v-model="formData.event.isBlocked" :options="blockedOptions" label="是否阻断" placeholder="请选择" />
<!-- 进度 (event.repairProgress) --> <!-- 进度 (event.repairProgress) -->
<BasePicker v-model="formData.event.repairProgress" :options="repairProgressOptions" label="抢进度" placeholder="请选择" /> <BasePicker v-model="formData.event.repairProgress" :options="repairProgressOptions" label="抢进度" placeholder="请选择" />
<!-- 水毁处数 (event.damageCount) --> <!-- 水毁处数 (event.damageCount) -->
<van-field v-model="formData.event.damageCount" label="水毁处数" placeholder="请填写" type="number" /> <van-field v-model="formData.event.damageCount" label="水毁处数" placeholder="请填写" type="number" />
@ -73,10 +73,10 @@
<div class="measures-options"> <div class="measures-options">
<!-- 改为单选使用 van-radio-group --> <!-- 改为单选使用 van-radio-group -->
<van-radio-group v-model="disposalMeasureValue" direction="horizontal"> <van-radio-group v-model="disposalMeasureValue" direction="horizontal">
<van-radio name="halfClose">半幅封闭</van-radio> <van-radio name="半幅封闭">半幅封闭</van-radio>
<van-radio name="fullClose">全副封闭</van-radio> <van-radio name="全副封闭">全副封闭</van-radio>
<van-radio name="bypass">便道通行</van-radio> <van-radio name="便道通行">便道通行</van-radio>
<van-radio name="normal">正常通行</van-radio> <van-radio name="正常通行">正常通行</van-radio>
</van-radio-group> </van-radio-group>
</div> </div>
</div> </div>
@ -148,30 +148,12 @@
</template> </template>
</van-field> </van-field>
<van-field v-model="formData.report.siteDescription" label="现场描述" placeholder="请填写" type="textarea" rows="2" autosize /> <van-field v-model="formData.report.siteDescription" label="现场描述" placeholder="请填写" type="textarea" rows="2" autosize />
<van-field label="附件" center>
<template #input>
<van-uploader accept="video/*,image/*" v-model="formData.fileList" :file-list="formData.fileList" :after-read="afterRead" multiple :max-count="6" />
</template>
</van-field>
</PanelItem> </PanelItem>
<!-- 附件 (fileList) -->
<!-- <PanelItem title="附件">
<div class="attachment-tip">图片只能上传jpg/png文件且不超过500kb视频仅支持20s内的视频</div>
<div class="upload-area">
<van-uploader v-model="imageFileList" :after-read="afterImageRead" accept="image/jpeg,image/png" :max-size="500 * 1024" @oversize="onOversize" multiple :max-count="9">
<div class="upload-btn">
<van-icon name="photo-o" size="24" />
<span>上传图片</span>
</div>
</van-uploader>
<van-uploader v-model="videoFileList" :after-read="afterVideoRead" accept="video/*" :max-size="20 * 1024 * 1024" @oversize="onVideoOversize">
<div class="upload-btn">
<van-icon name="video-o" size="24" />
<span>上传视频</span>
</div>
</van-uploader>
</div>
<div v-if="videoFileList.length > 0 && videoFileList[0].content" class="video-preview">
<video :src="videoFileList[0].content" controls style="width: 100%; max-height: 200px"></video>
</div>
</PanelItem> -->
<PanelItem> <PanelItem>
<!-- 是否需要恢复重建 (event.needsRecovery) --> <!-- 是否需要恢复重建 (event.needsRecovery) -->
<BasePicker v-model="formData.event.needsRecovery" :options="needsRecoveryOptions" label="是否需要恢复重建" placeholder="请选择" /> <BasePicker v-model="formData.event.needsRecovery" :options="needsRecoveryOptions" label="是否需要恢复重建" placeholder="请选择" />
@ -187,12 +169,13 @@
<script setup> <script setup>
import { ref, reactive, computed, watch } from 'vue' import { ref, reactive, computed, watch } from 'vue'
import { showToast, showFailToast } from 'vant' import { showToast, showFailToast, showLoadingToast } from 'vant'
import PanelItem from '@/components/PanelItem.vue' import PanelItem from '@/components/PanelItem.vue'
import BasePicker from '@/components/BasePicker.vue' import BasePicker from '@/components/BasePicker.vue'
import BaseDatePicker from '@/components/BaseDatePicker.vue' import BaseDatePicker from '@/components/BaseDatePicker.vue'
import LossList from './LossList.vue' import LossList from './LossList.vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { request } from '@shared/utils/request'
const route = useRoute() const route = useRoute()
@ -229,7 +212,7 @@ const formData = reactive({
inspectionMileage: '', // inspectionMileage: '', //
isBlocked: '', // isBlocked: '', //
needsRecovery: '', // needsRecovery: '', //
repairProgress: '', // repairProgress: '', //
reporterUnit: '', // reporterUnit: '', //
startStakeLat: '', // startStakeLat: '', //
startStakeLng: '', // startStakeLng: '', //
@ -337,9 +320,9 @@ const blockedOptions = [
] ]
const repairProgressOptions = [ const repairProgressOptions = [
{ label: '未抢修', value: '未抢修' }, { label: '未抢险', value: '未抢险' },
{ label: '抢修中', value: '抢修中' }, { label: '抢险中', value: '抢险中' },
{ label: '已完成', value: '已完成' }, { label: '已完成', value: '已完成' }
] ]
const needsRecoveryOptions = [ const needsRecoveryOptions = [
@ -411,6 +394,137 @@ const onVideoOversize = () => {
showFailToast('视频大小不能超过20MB') showFailToast('视频大小不能超过20MB')
} }
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 isVideoFile = (file) => {
// url
const videoExtensions = /\.(mp4|avi|mov|wmv|flv|mkv)$/i
if (file.fileUrl && videoExtensions.test(file.fileUrl)) return true
}
/**
* 判断图片是否可以上传
* @param {File} file - 图片文件
* @returns {boolean} 是否允许上传
*/
const isValidImage = (file) => {
//
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
showFailToast('只能上传 JPG/PNG 格式的图片!')
return false
}
// 500KB = 500 * 1024 bytes
const isLt500k = file.size / 1024 < 500
if (!isLt500k) {
showFailToast(`图片大小不能超过 500KB当前大小${(file.size / 1024).toFixed(2)}KB`)
return false
}
return true
}
/**
* 判断视频是否可以上传
* @param {File} file - 视频文件
* @returns {boolean} 是否允许上传
*/
const isValidVideo = (file) => {
//
const allowedTypes = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm']
const isValidType = allowedTypes.includes(file.type)
if (!isValidType) {
showFailToast('请上传有效的视频文件MP4、MOV、AVI、WEBM格式')
return false
}
// 50MB = 50 * 1024 * 1024 bytes
const maxSize = 50 * 1024 * 1024
const isValidSize = file.size <= maxSize
if (!isValidSize) {
showFailToast(`视频大小不能超过 50MB当前大小${(file.size / 1024 / 1024).toFixed(2)}MB`)
return false
}
return true
}
/**
* 统一的上传前校验用于 el-upload before-upload
* @param {File} file - 上传的文件
* @returns {boolean} 是否允许上传
*/
const checkFile = (file) => {
//
if (file.type.startsWith('image/')) {
return isValidImage(file)
}
//
if (file.type.startsWith('video/')) {
return isValidVideo(file)
}
showFailToast('只支持图片和视频文件!')
return false
}
const afterRead = async (options) => {
const file = options.file
if(!checkFile(file)) return
const toast = showLoadingToast({
message: '上传中...',
forbidClick: true,
duration: 0 // 0
})
try {
const formData = new FormData()
formData.append('file', file)
const res = await request({
url: '/snow-ops-platform/file/upload',
method: 'post',
data: formData
})
toast.close()
if (res.code === '00000') {
const name = file.name
let type = isImageFile(file) ? 1 : isVideoFile(file) ? 2 : 3
const url = res.data
const fileData = {
fileName: name,
fileUrl: url,
fileType: type,
fileSize: file.size
}
if (!formData.fileList) formData.fileList = []
formData.fileList.push(fileData)
console.log('fileList.value', formData.fileList)
} else {
throw new Error(res.message)
}
} catch (error) {
toast.close()
showToast({
type: 'fail',
message: error.message
})
}
}
// //
const validate = () => { const validate = () => {
if (!formData.occurTime) { if (!formData.occurTime) {
@ -523,4 +637,4 @@ defineExpose({
width: 110px; width: 110px;
} }
} }
</style> </style>

View File

@ -17,7 +17,7 @@
"inspectionMileage": 25.6, "inspectionMileage": 25.6,
"isBlocked": true, "isBlocked": true,
"needsRecovery": true, "needsRecovery": true,
"repairProgress": "抢中", "repairProgress": "抢中",
"reporterUnit": "武侯区交通运输局", "reporterUnit": "武侯区交通运输局",
"startStakeLat": "30.652145", "startStakeLat": "30.652145",
"startStakeLng": "104.075632", "startStakeLng": "104.075632",
@ -28,7 +28,7 @@
"strandedPersonCount": 12, "strandedPersonCount": 12,
"deadCount": 0, "deadCount": 0,
"strandedVehicleCount": 12, "strandedVehicleCount": 12,
"disposalMeasures": "halfClose,bypass", "disposalMeasures": "正常通行",
"actualRecoverTime": "2024-07-17 12:00:00", "actualRecoverTime": "2024-07-17 12:00:00",
"expectRecoverTime": "2024-07-18 18:00:00", "expectRecoverTime": "2024-07-18 18:00:00",
"injuredCount": 1, "injuredCount": 1,

View File

@ -1,8 +1,8 @@
<!-- FileUpload.vue --> <!-- FileUpload.vue -->
<template> <template>
<div class="file-upload"> <div class="file-upload">
<UploadBlock v-model="modelValue" :type="type" :multiple="multiple" :limit="limit" :placeholder="placeholder" :customFileType="fileType" /> <UploadBlock v-if="!readonly" v-model="modelValue" :type="type" :multiple="multiple" :limit="limit" :placeholder="placeholder" :customFileType="fileType" />
<PreviewBlock v-model:file-list="modelValue" :type="type" /> <PreviewBlock :readonly="readonly" v-model:file-list="modelValue" :type="type" />
</div> </div>
</template> </template>

View File

@ -29,10 +29,10 @@
</el-icon> </el-icon>
</div> </div>
<div class="preview-info"> <!-- <div class="preview-info">
<span class="file-name" :title="file.fileName">{{ file.fileName }}</span> <span class="file-name" :title="file.fileName">{{ file.fileName }}</span>
<span class="file-size">{{ formatFileSize(file.fileSize) }}</span> <span class="file-size">{{ formatFileSize(file.fileSize) }}</span>
</div> </div> -->
<div class="preview-actions" v-if="!readonly"> <div class="preview-actions" v-if="!readonly">
<el-button <el-button

View File

@ -99,7 +99,6 @@ const getAccept = () => {
const uploadFiles = async (event) => { const uploadFiles = async (event) => {
const file = event.target.files[0] const file = event.target.files[0]
const name = file.name
try { try {
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
@ -110,8 +109,9 @@ const uploadFiles = async (event) => {
}) })
if (res.code === '00000') { if (res.code === '00000') {
let fileType = 3 let fileType = 3
if (props.type == 'image') fileType = 1 const name = file.name
if (props.type == 'video') fileType = 2 if(props.type == 'image') fileType = 1
if(props.type == 'video') fileType = 2
const url = res.data const url = res.data
const fileData = { const fileData = {

View File

@ -0,0 +1,142 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="80%"
:show-close="true"
:close-on-click-modal="false"
:close-on-press-escape="true"
class="video-preview-dialog"
@close="handleClose"
>
<div class="video-container">
<video
ref="videoRef"
:src="currentVideoUrl"
class="video-player"
controls
autoplay
controlslist="nodownload"
></video>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose" type="danger" plain>关闭预览</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch, onUnmounted } from 'vue'
// URL
const currentVideoUrl = ref('')
//
const dialogTitle = ref('视频预览')
//
const dialogVisible = ref(false)
// DOM
const videoRef = ref(null)
//
const show = (options) => {
if (!options || !options.url) {
console.error('视频URL不能为空')
return
}
currentVideoUrl.value = options.url
dialogTitle.value = options.title || '视频预览'
dialogVisible.value = true
}
//
const close = () => {
dialogVisible.value = false
// URL
setTimeout(() => {
if (!dialogVisible.value) {
currentVideoUrl.value = ''
dialogTitle.value = '视频预览'
}
}, 100)
}
//
const handleKeydown = (event) => {
if (event.key === 'Escape' && dialogVisible.value) {
close()
}
}
//
const handleClose = () => {
close()
}
// /
watch(dialogVisible, (visible) => {
if (visible) {
document.addEventListener('keydown', handleKeydown)
} else {
document.removeEventListener('keydown', handleKeydown)
}
})
//
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
//
defineExpose({
show,
close
})
</script>
<style scoped>
.video-preview-dialog :deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.video-preview-dialog :deep(.el-dialog__body) {
padding: 0;
max-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
}
.video-container {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
}
.video-player {
width: 100%;
max-height: 70vh;
object-fit: contain;
outline: none;
display: block;
}
.video-preview-dialog :deep(.el-dialog__footer) {
padding: 16px 20px;
border-top: 1px solid #e4e7ed;
}
.dialog-footer {
display: flex;
justify-content: center;
}
</style>

View File

@ -3,19 +3,19 @@
<!-- 合并后的单个卡片 --> <!-- 合并后的单个卡片 -->
<el-card class="form-card" shadow="never"> <el-card class="form-card" shadow="never">
<div slot="header" class="card-header"> <div slot="header" class="card-header">
<span>信息</span> <span>信息</span>
</div> </div>
<!-- 所有表单项合并到一个区域每行一个 --> <!-- 所有表单项合并到一个区域每行一个 -->
<el-form :model="formData" label-width="120px" size="small"> <el-form :model="formData" label-width="120px" size="small">
<!-- 处置措施 --> <!-- 处置措施 -->
<el-form-item label="处置措施"> <el-form-item label="处置措施">
<el-radio-group v-model="disposalMeasureValue"> <el-select v-model="formData.report.disposalMeasures">
<el-radio label="halfClose">半幅封闭</el-radio> <el-option label="半幅封闭" value="半幅封闭" />
<el-radio label="fullClose">全副封闭</el-radio> <el-option label="全副封闭" value="全副封闭" />
<el-radio label="bypass">便道通行</el-radio> <el-option label="便道通行" value="便道通行" />
<el-radio label="normal">正常通行</el-radio> <el-option label="正常通行" value="正常通行" />
</el-radio-group> </el-select>
</el-form-item> </el-form-item>
<!-- 预计恢复时间 --> <!-- 预计恢复时间 -->
@ -216,8 +216,8 @@ const blockedOptions = [
] ]
const repairProgressOptions = [ const repairProgressOptions = [
{ label: '未抢修', value: '未抢修' }, { label: '未抢险', value: '未抢险' },
{ label: '抢修中', value: '抢修中' }, { label: '抢险中', value: '抢险中' },
{ label: '已完成', value: '已完成' } { label: '已完成', value: '已完成' }
] ]

View File

@ -0,0 +1,165 @@
<template>
<el-row class="loss-list-detail" :gutter="24">
<el-col :span="colSpan" v-for="(item, index) in configs" :key="index">
<div class="info-item">
<span class="info-label">{{ item.lossTypeName }}</span>
<span class="info-value">{{ getValue(item) }}{{ item.unit }}</span>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Delete, Plus } from '@element-plus/icons-vue'
import { request } from '@shared/utils/request'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
colSpan: {
type: Number,
default: 8
}
})
const getValue = (config) => {
const value = props.modelValue?.find((v) => v.lossTypeId === config.lossTypeId)
if (value == null) props.modelValue?.push({ ...config })
return value?.totalAmount || 0
}
const configs = ref([])
//
const getLossDict = async () => {
try {
const res = await request({
url: '/snow-ops-platform/water-damage/loss/typeAndInfo',
method: 'get'
})
configs.value = res.data?.records
} catch (error) {
console.error('获取损失类型失败:', error)
ElMessage.error('获取损失类型失败')
}
}
onMounted(async () => {
await getLossDict()
})
</script>
<style scoped lang="scss">
.loss-list-detail {
width: 100%;
.loss-table {
margin-bottom: 16px;
:deep(.el-table) {
.amount-cell {
display: flex;
align-items: center;
gap: 8px;
.unit-text {
color: #909399;
font-size: 14px;
white-space: nowrap;
}
}
}
}
.add-button-wrapper {
display: flex;
justify-content: flex-start;
}
.calculate-form {
padding: 8px 0;
:deep(.el-form-item) {
margin-bottom: 20px;
}
}
.calculation-preview {
background-color: #f5f7fa;
border-radius: 8px;
padding: 16px;
margin-top: 16px;
.preview-item {
display: flex;
justify-content: space-between;
align-items: center;
.preview-label {
color: #606266;
font-size: 14px;
}
.preview-value {
color: #f56c6c;
font-size: 16px;
font-weight: 500;
}
}
}
.loss-picker-content {
max-height: 400px;
overflow-y: auto;
padding: 8px 0;
.loss-radio-group {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
.loss-radio-item {
margin: 0;
padding: 10px 12px;
border-radius: 6px;
width: 100%;
box-sizing: border-box;
:deep(.el-radio__label) {
width: calc(100% - 22px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
}
.info-item {
display: flex;
align-items: flex-start;
line-height: 1.5;
margin-top: 10px;
.info-label {
white-space: nowrap;
flex-shrink: 0;
color: #909399;
font-size: 14px;
}
.info-value {
flex: 1;
color: #606266;
font-size: 14px;
word-break: break-all;
}
}
</style>

View File

@ -47,16 +47,25 @@
<el-row :gutter="20" class="info-row"> <el-row :gutter="20" class="info-row">
<el-col :span="8"> <el-col :span="8">
<div class="info-item"> <div class="info-item">
<span class="info-label">进度</span> <span class="info-label">进度</span>
<span class="info-value">{{ detailData.event?.repairProgress || '-' }}</span> <span class="info-value">{{ detailData.event?.repairProgress || '-' }}</span>
</div> </div>
</el-col> </el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">处理措施</span>
<span class="info-value">{{ getBaseDisposalMeasures() }}</span>
</div>
</el-col>
<el-col :span="8"> <el-col :span="8">
<div class="info-item"> <div class="info-item">
<span class="info-label">水毁处数</span> <span class="info-label">水毁处数</span>
<span class="info-value">{{ detailData.event?.damageCount || 0 }}</span> <span class="info-value">{{ detailData.event?.damageCount || 0 }}</span>
</div> </div>
</el-col> </el-col>
</el-row>
<el-row :gutter="20" class="info-row">
<el-col :span="8"> <el-col :span="8">
<div class="info-item"> <div class="info-item">
<span class="info-label">阻断里程</span> <span class="info-label">阻断里程</span>
@ -66,66 +75,24 @@
</el-row> </el-row>
<el-row :gutter="20" class="info-row"> <el-row :gutter="20" class="info-row">
<el-col :span="8">
<div class="info-item">
<span class="info-label">发生时间</span>
<span class="info-value">{{ detailData.occurTime || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">线路编号</span>
<span class="info-value">{{ detailData.routeNo || '-' }}</span>
</div>
</el-col>
<el-col :span="8"> <el-col :span="8">
<div class="info-item"> <div class="info-item">
<span class="info-label">地点路线</span> <span class="info-label">地点路线</span>
<span class="info-value">{{ detailData.occurLocation || '-' }}</span> <span class="info-value">{{ detailData.occurLocation || '-' }}</span>
</div> </div>
</el-col> </el-col>
</el-row>
<el-row :gutter="20" class="info-row">
<el-col :span="8"> <el-col :span="8">
<div class="info-item"> <div class="info-item">
<span class="info-label">起点桩号</span> <span class="info-label">起点桩号</span>
<span class="info-value">{{ detailData.event?.startStakeNo || '-' }}</span> <span class="info-value">{{ detailData.event?.startStakeNo || '-' }}</span>
</div> </div>
</el-col> </el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">起点桩经度</span>
<span class="info-value">{{ detailData.event?.startStakeLng || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">起点桩纬度</span>
<span class="info-value">{{ detailData.event?.startStakeLat || '-' }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="info-row">
<el-col :span="8"> <el-col :span="8">
<div class="info-item"> <div class="info-item">
<span class="info-label">止点桩号</span> <span class="info-label">止点桩号</span>
<span class="info-value">{{ detailData.event?.endStakeNo || '-' }}</span> <span class="info-value">{{ detailData.event?.endStakeNo || '-' }}</span>
</div> </div>
</el-col> </el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">止点桩经度</span>
<span class="info-value">{{ detailData.event?.endStakeLng || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">止点桩纬度</span>
<span class="info-value">{{ detailData.event?.endStakeLat || '-' }}</span>
</div>
</el-col>
</el-row> </el-row>
<el-row :gutter="20" class="info-row"> <el-row :gutter="20" class="info-row">
@ -141,55 +108,37 @@
<span class="info-value">{{ detailData.event?.blockedPointName || '-' }}</span> <span class="info-value">{{ detailData.event?.blockedPointName || '-' }}</span>
</div> </div>
</el-col> </el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">上报区县</span>
<span class="info-value">{{ detailData.event?.district || '-' }}</span>
</div>
</el-col>
</el-row> </el-row>
<el-row :gutter="20" class="info-row"> <el-row :gutter="20" class="info-row">
<el-col :span="8"> <el-col :span="8">
<div class="info-item"> <div class="info-item">
<span class="info-label">巡查里程</span> <span class="info-label">所属区县</span>
<span class="info-value">{{ detailData.event?.inspectionMileage ? detailData.event.inspectionMileage + '公里' : '-' }}</span> <span class="info-value">{{ detailData.event?.district || '-' }}</span>
</div> </div>
</el-col> </el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">发生时间</span>
<span class="info-value">{{ detailData.occurTime || '-' }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="info-row">
<el-col :span="8"> <el-col :span="8">
<div class="info-item"> <div class="info-item">
<span class="info-label">是否恢复重建</span> <span class="info-label">是否恢复重建</span>
<span class="info-value">{{ detailData.event?.needsRecovery ? '是' : '否' }}</span> <span class="info-value">{{ detailData.event?.needsRecovery ? '是' : '否' }}</span>
</div> </div>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="16">
<div class="info-item"> <div class="info-item">
<span class="info-label">恢复重建预估费用</span> <span class="info-label">恢复重建预估费用</span>
<span class="info-value">{{ detailData.event?.estimatedRecoveryCost ? detailData.event.estimatedRecoveryCost + '万元' : '-' }}</span> <span class="info-value">{{ detailData.event?.estimatedRecoveryCost ? detailData.event.estimatedRecoveryCost + '万元' : '-' }}</span>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" class="info-row">
<el-col :span="8">
<div class="info-item">
<span class="info-label">联系人</span>
<span class="info-value">{{ detailData.event?.contactPerson || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">联系电话</span>
<span class="info-value">{{ detailData.event?.contactPhone || '-' }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">填报单位</span>
<span class="info-value">{{ detailData.event?.reporterUnit || '-' }}</span>
</div>
</el-col>
</el-row>
</el-card> </el-card>
<!-- 填报信息卡片 --> <!-- 填报信息卡片 -->
@ -204,52 +153,83 @@
<div v-for="(report, index) in allReports" :key="index" class="report-section"> <div v-for="(report, index) in allReports" :key="index" class="report-section">
<div class="report-header"> <div class="report-header">
<span class="report-title">{{ report?.title }}</span> <span class="report-title">{{ report?.title }}</span>
<span class="report-meta">{{ report.reporterName || '-' }} {{ report.reportTime || '-' }}</span> <span class="report-meta">时间{{ report.reportTime || '-' }}</span>
</div> </div>
<div class="content-wrapper">
<div class="basic-info-wrapper">
<div class="info-list">
<div class="info-item">
<span class="info-label">现场描述</span>
<span class="info-value">{{ report.siteDescription || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">处置措施</span>
<span class="info-value">{{ report.disposalMeasures || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">路产损失</span>
<span class="info-value">{{ report.totalLossAmount ? report.totalLossAmount + '万元' : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">实际恢复时间</span>
<span class="info-value">{{ report.actualRecoverTime || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">预计恢复时间</span>
<span class="info-value">{{ report.expectRecoverTime || '-' }}</span>
</div>
<div class="info-list"> <div class="info-item">
<div class="info-item"> <span class="info-label">填报人</span>
<span class="info-label">处置情况</span> <span class="info-value">{{ report.reporterName ? report.reporterName : '-' }}</span>
<span class="info-value">{{ formatDisposalMeasures(report.disposalMeasures) || '-' }}</span> </div>
</div>
<div class="info-item">
<span class="info-label">塌方及损失</span>
<span class="info-value">{{ getLossDescription(report) }}</span>
</div>
<div class="info-item">
<span class="info-label">路产损失</span>
<span class="info-value">{{ report.totalLossAmount ? report.totalLossAmount + '万元' : '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">有无车辆滞留</span>
<span class="info-value">{{ getVehicleStrandedText(report) }}</span>
</div>
<div class="info-item">
<span class="info-label">滞留车辆</span>
<span class="info-value">{{ report.strandedVehicleCount || 0 }}</span>
</div>
<div class="info-item">
<span class="info-label">预计恢复时间</span>
<span class="info-value">{{ report.expectRecoverTime || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">实际恢复时间</span>
<span class="info-value">{{ report.actualRecoverTime || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">现场描述</span>
<span class="info-value">{{ report.siteDescription || '-' }}</span>
</div>
<!-- 附件 --> <div class="info-item">
<div v-if="report.fileList && report.fileList.length > 0" class="info-item attachment-item"> <span class="info-label">联系电话</span>
<span class="info-label">附件</span> <span class="info-value">{{ report.phone ? report.phone : '-' }}</span>
<div class="attachment-list"> </div>
<el-link v-for="(file, fileIndex) in report.fileList" :key="fileIndex" :underline="false" @click="previewFile(file)" class="attachment-link">
<el-icon><Picture v-if="file.fileType === 1" /><VideoCamera v-else /></el-icon>
<span class="file-name">{{ file.fileName }}</span>
</el-link>
</div> </div>
<div class="file-list">
<FileUpload v-model="report.fileList" :readonly="!isEdit" />
</div>
</div>
<div class="detal-info-wrapper">
<template v-if="report.showDetail">
<LossListDetail :modelValue="report.lossList" :col-span="8" />
<el-row :gutter="24">
<el-col :span="24">
<div class="info-item">
<span class="info-label">其它损失描述</span>
<span class="info-value">{{ '未对接' }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<div class="info-item">
<span class="info-label">已投入机械</span>
<span class="info-value">{{ report.investedMachinery }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">已投入人力</span>
<span class="info-value">{{ report.investedManpower }}</span>
</div>
</el-col>
<el-col :span="8">
<div class="info-item">
<span class="info-label">已投入资金</span>
<span class="info-value">{{ report.investedFunds }}</span>
</div>
</el-col>
</el-row>
</template>
<el-button style="margin-top: 30px" type="primary" link @click="report.showDetail = !report.showDetail">
{{ report.showDetail ? '点击关闭详情' : '点击查看详情' }}
</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -276,6 +256,8 @@ import { ElMessage } from 'element-plus'
import { ArrowLeft, Picture, VideoCamera } from '@element-plus/icons-vue' import { ArrowLeft, Picture, VideoCamera } from '@element-plus/icons-vue'
import ContinueReport from './ContinueReport.vue' import ContinueReport from './ContinueReport.vue'
import { request } from '@shared/utils/request' import { request } from '@shared/utils/request'
import LossListDetail from './LossListDetail.vue'
import FileUpload from '@/component/FileUpload/FileUpload.vue'
import mockData from '../DisasterReport/waterMockJson.json' import mockData from '../DisasterReport/waterMockJson.json'
const router = useRouter() const router = useRouter()
@ -307,10 +289,16 @@ const continueReport = ref(null)
const allReports = computed(() => { const allReports = computed(() => {
const reports = const reports =
detailData.value.report?.map((item, index) => { detailData.value.report?.map((item, index) => {
item.title = index === 0 ? '首报' : '续报' + index if (index === detailData.value.report.length - 1) {
item.title = '首报'
} else {
item.title = '续报' + (detailData.value.report.length - 1 - index)
}
console.log(detailData.value.report.length - 1, ' ', index, ' -', item.title)
return item return item
}) || [] }) || []
return reports.reverse() return reports
// return reports.reverse()
}) })
// //
@ -328,14 +316,20 @@ const getEventStatusType = () => {
return eventStatus.value === 1 ? 'success' : 'danger' return eventStatus.value === 1 ? 'success' : 'danger'
} }
const getBaseDisposalMeasures = () => {
const firstItem = allReports.value[0]
if (!firstItem) return '-'
return formatDisposalMeasures(firstItem.disposalMeasures || '') || '-'
}
// //
const formatDisposalMeasures = (measures) => { const formatDisposalMeasures = (measures) => {
if (!measures) return '' if (!measures) return ''
const measureMap = { const measureMap = {
halfClose: '半幅封闭', 半幅封闭: '半幅封闭',
fullClose: '全副封闭', 全副封闭: '全副封闭',
bypass: '便道通行', 便道通行: '便道通行',
normal: '正常通行' 正常通行: '正常通行'
} }
return measures return measures
.split(',') .split(',')
@ -381,6 +375,7 @@ const getDisasterDetail = async () => {
if (result?.data) { if (result?.data) {
const data = result.data const data = result.data
console.log('🚀 ~ getDisasterDetail ~ data:', data)
detailData.value = { detailData.value = {
event: data.event || null, event: data.event || null,
report: data.report || [], report: data.report || [],
@ -397,7 +392,7 @@ const getDisasterDetail = async () => {
const newFormData = { const newFormData = {
...data, ...data,
lossList: null, lossList: null,
report: mockData.report, report: route.query.mock ? mockData.report : {},
fileList: null fileList: null
} }
continueReport.value?.initFormData(newFormData) continueReport.value?.initFormData(newFormData)
@ -416,25 +411,6 @@ const handleClickBack = () => {
router.push('/disasterManagement') 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(() => { onMounted(() => {
getDisasterDetail() getDisasterDetail()
}) })
@ -502,8 +478,12 @@ onMounted(() => {
align-items: flex-start; align-items: flex-start;
line-height: 1.5; line-height: 1.5;
& + .info-item {
margin-top: 10px;
}
.info-label { .info-label {
width: 120px; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
color: #909399; color: #909399;
font-size: 14px; font-size: 14px;
@ -612,7 +592,26 @@ onMounted(() => {
margin-right: 10px; margin-right: 10px;
} }
.right-panel { .right-panel {
width: 400px; width: 300px;
} }
}
.content-wrapper {
display: flex;
flex-direction: column;
}
.basic-info-wrapper {
display: flex;
}
.detal-info-wrapper {
margin-top: 10px;
border-top: 1px solid #efefef;
padding-top: 10px;
}
.info-list {
flex: 1;
overflow: hidden;
}
.file-list {
} }
</style> </style>

View File

@ -22,9 +22,8 @@
<el-col :xs="24" :sm="12" :md="8" :lg="6"> <el-col :xs="24" :sm="12" :md="8" :lg="6">
<el-form-item label="事件类别"> <el-form-item label="事件类别">
<el-select v-model="filterForm.disasterType" placeholder="请选择" clearable> <el-select v-model="filterForm.disasterType" placeholder="请选择" clearable>
<el-option label="水毁事件" value="水毁事件" /> <el-option label="水毁事件" value="WATER_DAMAGE" />
<el-option label="塌方事件" value="塌方事件" /> <el-option label="冰雪灾害" value="ICE_SNOW" />
<el-option label="泥石流" value="泥石流" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -87,20 +86,25 @@
{{ scope.$index + 1 }} {{ scope.$index + 1 }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="routeNo" label="所属区县" min-width="120" /> <el-table-column prop="district" label="所属区县" min-width="120" />
<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="routeName" label="路线名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="startPile" label="起点桩号" min-width="110" show-overflow-tooltip /> <el-table-column prop="startStakeNo" label="起点桩号" min-width="110" show-overflow-tooltip />
<el-table-column prop="endPile" label="终点桩号" min-width="110" show-overflow-tooltip /> <el-table-column prop="endStakeNo" label="终点桩号" min-width="110" show-overflow-tooltip />
<el-table-column prop="roadConditionLocation" label="路况位置" min-width="150" show-overflow-tooltip /> <el-table-column prop="roadConditionLocation" label="路况位置" min-width="150" show-overflow-tooltip />
<el-table-column prop="isBlocked" label="是否阻断" width="90" align="center"> <el-table-column prop="isBlocked" label="是否阻断" width="90" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.isBlocked === '是' ? 'danger' : 'success'" size="small"> <el-tag :type="row.blocked === true ? 'danger' : 'success'" size="small">
{{ row.isBlocked || '—' }} {{ row.blocked === true ? '是' : row.blocked === false ? '否' : '—' }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="disasterType" label="事件类型" min-width="120" show-overflow-tooltip /> <el-table-column prop="disasterType" label="事件类型" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.disasterType == 'ICE_SNOW'">冰雪灾害</span>
<span v-if="row.disasterType == 'WATER_DAMAGE'">水毁灾害</span>
</template>
</el-table-column>
<el-table-column prop="roadConditionType" label="路况类别" min-width="110" show-overflow-tooltip /> <el-table-column prop="roadConditionType" label="路况类别" min-width="110" show-overflow-tooltip />
<el-table-column prop="eventStatus" label="事件状态" width="100" align="center"> <el-table-column prop="eventStatus" label="事件状态" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
@ -109,20 +113,20 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="measure" label="处理措施" min-width="150" show-overflow-tooltip /> <el-table-column prop="disposalMeasures" label="处理措施" min-width="150" show-overflow-tooltip />
<el-table-column prop="occurTime" label="发现时间" width="170" /> <el-table-column prop="occurTime" label="发现时间" width="170" />
<el-table-column prop="expectRecoverTime" label="预计恢复时间" width="170" /> <el-table-column prop="expectRecoverTime" label="预计恢复时间" width="170" />
<el-table-column prop="contactPerson" label="联系人" width="110" /> <el-table-column prop="contactPerson" label="联系人" width="110" />
<el-table-column prop="contactPhone" label="联系电话" width="120" /> <el-table-column prop="contactPhone" label="联系电话" width="120" />
<el-table-column label="图片" width="80" align="center"> <el-table-column label="图片" width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button v-if="row.hasImage" link type="primary" size="small" @click="viewImages(row)">查看</el-button> <el-icon style="font-size: 18px; cursor: pointer;" v-if="row.images && row.images.length > 0" @click="viewImages(row)"><Picture /></el-icon>
<span v-else></span> <span v-else></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="视频" width="80" align="center"> <el-table-column label="视频" width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button v-if="row.hasVideo" link type="primary" size="small" @click="viewVideos(row)">播放</el-button> <el-icon v-if="row.videos && row.videos.length > 0" style="font-size: 18px; cursor: pointer;" @click="viewVideos(row)"><VideoPlay /></el-icon>
<span v-else></span> <span v-else></span>
</template> </template>
</el-table-column> </el-table-column>
@ -148,6 +152,9 @@
/> />
</div> </div>
</el-card> </el-card>
<el-image-viewer v-if="showPreviewImage && selectedRow" :url-list="selectedRow.images" show-progress @close="showPreviewImage = false" />
<VideoPreviewDialog ref="videoPreviewDialog" />
</div> </div>
</template> </template>
@ -156,6 +163,8 @@ import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { request } from '@/utils/request' import { request } from '@/utils/request'
import { Picture, VideoPlay } from '@element-plus/icons-vue'
import VideoPreviewDialog from '@/component/VideoPreviewDialog.vue'
const router = useRouter() const router = useRouter()
@ -171,6 +180,11 @@ const filterForm = reactive({
pushStatus: '' // pushStatus: '' //
}) })
const showPreviewImage = ref(false)
const selectedRow = ref(null)
const videoPreviewDialog = ref(null)
// //
const dateRange = ref(null) const dateRange = ref(null)
@ -221,7 +235,7 @@ const fetchData = async () => {
// //
const response = await request({ const response = await request({
url: '/snow-ops-platform/unified-disaster/list', url: '/snow-ops-platform/unified-disaster/pc/list',
method: 'get', method: 'get',
params params
}) })
@ -313,21 +327,16 @@ const toReport = () => {
// //
const viewImages = (row) => { const viewImages = (row) => {
if (row.images && row.images.length) { selectedRow.value = row
// showPreviewImage.value = true
ElMessage.info('图片预览功能开发中')
} else {
ElMessage.info('暂无图片')
}
} }
// //
const viewVideos = (row) => { const viewVideos = (row) => {
if (row.videos && row.videos.length) { videoPreviewDialog.value.show({
ElMessage.info('视频播放功能开发中') url: row.videos[0],
} else { title: row.eventName
ElMessage.info('暂无视频') })
}
} }
// //
@ -400,4 +409,4 @@ onMounted(() => {
border-top: 1px solid #ebeef5; border-top: 1px solid #ebeef5;
} }
} }
</style> </style>

View File

@ -77,8 +77,8 @@
</el-col> </el-col>
<!-- 处理措施--> <!-- 处理措施-->
<el-col :span="8"> <el-col :span="8">
<el-form-item label="处理措施" prop="event.repairProgress"> <el-form-item label="处理措施" prop="event.disposalMeasures">
<el-select v-model="formData.event.repairProgress" placeholder="请选择" style="width: 100%"> <el-select v-model="formData.event.disposalMeasures" placeholder="请选择" style="width: 100%">
<el-option label="全幅封闭" value="全幅封闭" /> <el-option label="全幅封闭" value="全幅封闭" />
<el-option label="半幅封闭" value="半幅封闭" /> <el-option label="半幅封闭" value="半幅封闭" />
<el-option label="正常通行" value="正常通行" /> <el-option label="正常通行" value="正常通行" />
@ -414,7 +414,7 @@ const formData = reactive({
inspectionMileage: null, // inspectionMileage: null, //
isBlocked: null, // isBlocked: null, //
needsRecovery: null, // needsRecovery: null, //
repairProgress: null, // repairProgress: null, //
reporterUnit: null, // reporterUnit: null, //
startStakeLat: null, // startStakeLat: null, //
startStakeLng: null, // startStakeLng: null, //