546 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="water-disaster">
<!-- 基本信息 -->
<PanelItem title="基本信息" v-if="!isContinue">
<van-form>
<!-- 路况类别 -->
<BasePicker v-model="formData.roadConditionType" :options="roadConditionOptions" label="路况类别" placeholder="请选择" />
<!-- 是否阻断 (event.isBlocked) -->
<BasePicker v-model="formData.event.isBlocked" :options="blockedOptions" label="是否阻断" placeholder="请选择" />
<!-- 抢险进度 (event.repairProgress) -->
<BasePicker v-model="formData.event.repairProgress" :options="repairProgressOptions" label="抢险进度" placeholder="请选择" />
<!-- 水毁处数 (event.damageCount) -->
<van-field v-model="formData.event.damageCount" label="水毁处数" placeholder="请填写" type="number" />
<!-- 阻断里程 (event.blockedMileage) -->
<van-field v-model="formData.event.blockedMileage" label="阻断里程" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">公里</span>
</template>
</van-field>
<!-- 发生时间 (顶层 occurTime) -->
<BaseDatePicker v-model="formData.occurTime" label="发生时间" placeholder="请选择时间" :columnsType="['year', 'month', 'day', 'hour', 'minute']" />
<div class="calibrate-time-btn" @click="calibrateTime">
<van-icon name="replay" />
<span>校准时间</span>
</div>
<!-- 线路编号 (顶层 routeNo) -->
<van-field v-model="formData.routeNo" label="线路编号" placeholder="请填写" />
<!-- 起点桩号 (event.startStakeNo) -->
<van-field v-model="formData.event.startStakeNo" label="起点桩号(K)" placeholder="请填写" />
<!-- 止点桩号 (event.endStakeNo) -->
<van-field v-model="formData.event.endStakeNo" label="止点桩号(K)" placeholder="请填写" />
<!-- 路况位置 (occurLocation) -->
<van-field v-model="formData.occurLocation" label="路况位置" placeholder="请填写" />
<!-- 阻断点小地名 (event.blockedPointName) -->
<van-field v-model="formData.event.blockedPointName" label="阻断点小地名" placeholder="请填写" />
</van-form>
</PanelItem>
<!-- 处置情况 (report) -->
<PanelItem title="处置情况">
<div class="disposal-measures">
<span class="measures-label">处置措施</span>
<div class="measures-options">
<!-- 改为单选使用 van-radio-group -->
<van-radio-group v-model="formData.report.disposalMeasures" direction="horizontal">
<van-radio name="半幅封闭">半幅封闭</van-radio>
<van-radio name="全副封闭">全副封闭</van-radio>
<van-radio name="便道通行">便道通行</van-radio>
<van-radio name="正常通行">正常通行</van-radio>
</van-radio-group>
</div>
</div>
<!-- 预计恢复时间 (report.expectRecoverTime) -->
<BaseDatePicker v-model="formData.report.expectRecoverTime" label="预计恢复时间" placeholder="请选择时间" :min-date="minDate" :max-date="maxDate" type="datetime" />
<!-- 实际恢复时间 (report.actualRecoverTime) -->
<BaseDatePicker v-model="formData.report.actualRecoverTime" label="实际恢复时间" placeholder="请选择时间" :min-date="minDate" :max-date="maxDate" type="datetime" />
</PanelItem>
<!-- 人员车辆 (report) -->
<PanelItem title="人员车辆">
<van-form>
<van-field v-model="formData.report.injuredCount" label="受伤人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.report.deadCount" label="死亡人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.report.strandedPersonCount" label="滞留人员" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.report.damagedVehicleCount" label="损坏车辆" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
<van-field v-model="formData.report.strandedVehicleCount" label="滞留车辆" placeholder="请填写" type="number">
<template #button>
<span class="field-unit"></span>
</template>
</van-field>
</van-form>
</PanelItem>
<!-- 灾毁损失 (lossList) -->
<PanelItem title="灾毁损失">
<LossList v-model="formData.lossList" />
<van-field v-model="formData.report.remark" label="处理情况" placeholder="请填写(选填)" />
<van-field v-model="formData.report.totalLossAmount" label="损失总金额" placeholder="请填写(选填)" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
</van-field>
</PanelItem>
<!-- 投入资源 (report) -->
<PanelItem>
<van-field v-model="formData.report.investedMachinery" label="已投机械" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">/</span>
</template>
</van-field>
<van-field v-model="formData.report.investedManpower" label="已投入力" placeholder="请填写" type="number">
<template #button>
<span class="field-unit">人次</span>
</template>
</van-field>
<van-field v-model="formData.report.investedFunds" label="已投资金" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
</van-field>
<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/*" :modelValue="getFileList()" :after-read="afterRead" multiple :max-count="6" @delete="removeFile" />
</template>
</van-field>
</PanelItem>
<PanelItem>
<!-- 是否需要恢复重建 (event.needsRecovery) -->
<BasePicker v-model="formData.event.needsRecovery" :options="needsRecoveryOptions" label="是否需要恢复重建" placeholder="请选择" />
<!-- 恢复重建预估费用 (event.estimatedRecoveryCost) -->
<van-field v-model="formData.event.estimatedRecoveryCost" v-if="!isContinue" label="恢复重建预估费用" placeholder="请填写" type="digit">
<template #button>
<span class="field-unit">万元</span>
</template>
</van-field>
</PanelItem>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { showToast, showFailToast, showLoadingToast } from 'vant'
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'
import { request } from '@shared/utils/request'
const route = useRoute()
// 是否为续报
const isContinue = computed(() => route.query.isContinue)
// 表单数据 - 按 Request 接口结构定义,使用 ref 包装
const formData = ref({
// 顶层字段
occurLocation: '', // 发生地点
occurTime: '', // 发生时间
roadConditionType: '', // 路况类别
routeNo: '', // 线路编号
// event 对象
event: {
blockedMileage: '', // 阻断里程
blockedPointName: '', // 阻断点小地名
contactPerson: '', // 联系人
contactPhone: '', // 联系电话
damageCount: '', // 水毁处数
district: '', // 上报区县
endStakeNo: '', // 止点桩号
estimatedRecoveryCost: '', // 恢复重建预估费用
inspectionMileage: '', // 巡查里程
isBlocked: '', // 是否阻断
needsRecovery: '', // 是否需要恢复重建
repairProgress: '', // 抢险进度
reporterUnit: '', // 填报单位
startStakeNo: '' // 起点桩号
},
// report 对象
report: {
actualRecoverTime: '', // 实际恢复时间
damagedVehicleCount: '', // 损坏车辆
deadCount: '', // 死亡人员
disposalMeasures: '', // 处置措施(单个值,不再用逗号分隔)
expectRecoverTime: '', // 预计恢复时间
injuredCount: '', // 受伤人员
investedFunds: '', // 已投资金
investedMachinery: '', // 已投机械
investedManpower: '', // 已投人力
remark: '', // 处理情况/备注
siteDescription: '', // 现场描述
strandedPersonCount: '', // 滞留人员
strandedVehicleCount: '', // 滞留车辆
totalLossAmount: '' // 损失总金额
},
// lossList 数组
lossList: [],
// fileList 数组
fileList: []
})
const getFileList = () => {
const fileList = formData.value.fileList?.map((item) => {
return {
url: item.fileUrl,
name: item.fileName
}
})
return fileList
}
// BasePicker 选项数据
const roadConditionOptions = [
{ label: '高速公路', value: '高速公路' },
{ label: '国道', value: '国道' },
{ label: '省道', value: '省道' },
{ label: '县道', value: '县道' },
{ label: '乡道', value: '乡道' },
{ label: '村道', value: '村道' }
]
const blockedOptions = [
{ label: '是', value: true },
{ label: '否', value: false }
]
const repairProgressOptions = [
{ label: '未抢险', value: '未抢险' },
{ label: '抢险中', value: '抢险中' },
{ label: '已完成', value: '已完成' }
]
const needsRecoveryOptions = [
{ label: '是', value: true },
{ label: '否', value: false }
]
// 时间选择器范围
const minDate = new Date(2020, 0, 1)
const maxDate = new Date(2030, 11, 31)
const initFormData = (newVal) => {
if (newVal && Object.keys(newVal).length > 0) {
// 深度合并数据 - 直接替换整个对象
formData.value = {
occurLocation: newVal.occurLocation || '',
occurTime: newVal.occurTime || '',
roadConditionType: newVal.roadConditionType || '',
routeNo: newVal.routeNo || '',
event: { ...formData.value.event, ...(newVal.event || {}) },
report: { ...formData.value.report, ...(newVal.report || {}) },
lossList: newVal.lossList || [],
fileList: newVal.fileList || []
}
}
}
// 校准时间
const calibrateTime = () => {
const now = new Date()
const formatted = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
formData.value.occurTime = formatted
showToast('时间已校准为当前时间')
}
// 图片上传处理
const afterImageRead = (file) => {
console.log('图片上传:', file)
}
const onOversize = () => {
showFailToast('图片大小不能超过500KB')
}
const afterVideoRead = (file) => {
console.log('视频上传:', file)
}
const onVideoOversize = () => {
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
}
/**
* 统一的上传前校验
* @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 uploadFormData = new FormData()
uploadFormData.append('file', file)
const res = await request({
url: '/snow-ops-platform/file/upload',
method: 'post',
data: uploadFormData
})
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.value.fileList) formData.value.fileList = []
formData.value.fileList.push(fileData)
} else {
throw new Error(res.message)
}
} catch (error) {
toast.close()
showToast({
type: 'fail',
message: error.message
})
}
}
const removeFile = (file, index) => {
// 删除文件
formData.value.fileList.splice(index, 1)
}
// 暴露验证方法
const validate = () => {
if (!formData.value.occurTime) {
showToast('请填写发生时间')
return false
}
if (!formData.value.routeNo) {
showToast('请填写线路编号')
return false
}
return true
}
// 获取表单数据 - 返回 formData.value 的副本
const getFormData = () => {
return { ...formData.value }
}
// 暴露方法给父组件
defineExpose({
validate,
initFormData,
getFormData
})
</script>
<style lang="scss" scoped>
.water-disaster {
.coordinate-row {
display: flex;
gap: 8px;
.coordinate-field {
flex: 1;
}
}
.calibrate-time-btn,
.calibrate-coord-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #1989fa;
margin: 8px 0 8px 12px;
padding: 4px 8px;
background: #f0f7ff;
border-radius: 20px;
cursor: pointer;
width: fit-content;
}
.field-unit {
color: #969799;
font-size: 14px;
margin-left: 4px;
}
.disposal-measures {
margin-bottom: 16px;
.measures-label {
font-size: 14px;
color: #323233;
display: block;
margin-bottom: 8px;
}
.measures-options {
:deep(.van-radio-group) {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
:deep(.van-radio) {
margin-right: 0;
}
}
}
.attachment-tip {
font-size: 12px;
color: #969799;
margin-bottom: 12px;
}
.upload-area {
display: flex;
gap: 16px;
flex-wrap: wrap;
.upload-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
background: #f8f9fa;
border: 1px dashed #dcdee0;
border-radius: 8px;
gap: 4px;
font-size: 12px;
color: #969799;
cursor: pointer;
}
}
.video-preview {
margin-top: 12px;
}
:deep(.van-field__label) {
width: 110px;
}
}
</style>