feat: 抽取灾毁文件上传组件

This commit is contained in:
niedongsheng 2026-04-20 14:37:51 +08:00
parent fd1dc1c993
commit 037cdda407
3 changed files with 284 additions and 180 deletions

View File

@ -9,7 +9,8 @@
placeholder="请选择时间"
:columnsType="['year', 'month', 'day', 'hour', 'minute']"
/>
<div class="calibrate-time-btn" @click="calibrateTime">
<!-- 校准时间 -->
<div class="calibrate-time-btn" @click="calibrateTime(true)">
<van-icon name="replay" />
<span>校准时间</span>
</div>
@ -132,7 +133,14 @@
:max-date="maxDate"
type="datetime"
/>
<!-- 文件上传 -->
<DisasterFileUpload label="附件上传" v-model="formData.fileList" />
</PanelItem>
<!-- 提交按钮 -->
<van-button type="primary" class="footer-btn" @click="handleSubmit" :loading="submitting">
提交
</van-button>
</div>
</template>
<script setup>
@ -145,6 +153,8 @@ import MaterialPicker from '../components/MaterialPicker.vue';
import { useRoute } from 'vue-router';
import { request } from '@shared/utils/request';
import { useOptions } from '@shared/composables/useOptions';
import DisasterFileUpload from '../components/DisasterFileUpload.vue';
import { showToast, showFailToast, showLoadingToast } from 'vant';
const route = useRoute();
const { options } = useOptions();
@ -205,6 +215,63 @@ const handleRouteNoChange = (item = {}) => {
formData.event.endStakeLat = endPoint.latitude;
};
const calibrateTime = isShowToast => {
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;
if (isShowToast) showToast('时间已校准为当前时间');
};
const handleSubmit = async () => {
//
if (!validate()) return;
submitting.value = true;
try {
//
const submitData = {
...formData.value,
//
};
const res = await request({
url: '/snow-ops-platform/event/addOrUpdate',
method: 'post',
data: submitData,
});
if (res?.code === '00000') {
showSuccessToast('提交成功');
let isRebuilded = false;
if (isContinue && detail.value.event.needsRecovery) {
//
isRebuilded = true;
}
if (!isRebuilded && submitData.event.needsRecovery) {
router.replace({
name: 'RebuildAdd',
params: {
data: res.data.id,
},
});
} else {
//
setTimeout(() => {
router.replace('/disasterManagement');
}, 500);
}
} else {
showFailToast(res.message);
}
} catch (error) {
showFailToast('提交失败,请重试');
console.error('提交失败:', error);
} finally {
submitting.value = false;
}
};
const getDisasterDetail = async () => {
const id = route.query.id;
if (!id) {
@ -234,9 +301,47 @@ const getDisasterDetail = async () => {
};
onMounted(() => {
calibrateTime();
if (isContinue.value) {
getDisasterDetail();
}
});
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.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;
}
.footer-btn {
position: fixed;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 32px);
max-width: 340px;
border-radius: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
z-index: 10;
&:active {
opacity: 0.9;
transform: translateX(-50%) scale(0.98);
}
}
</style>

View File

@ -132,11 +132,8 @@
</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="7" @delete="removeFile" />
</template>
</van-field>
<!-- 文件上传 -->
<DisasterFileUpload label="附件上传" v-model="formData.fileList" />
</PanelItem>
<PanelItem v-if="!isContinue || (isContinue && !detail?.event.needsRecovery)">
<!-- 是否需要恢复重建 (event.needsRecovery) -->
@ -160,6 +157,7 @@ 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 DisasterFileUpload from '../components/DisasterFileUpload.vue'
import RoadRoutesPicker from '../components/RoadRoutesPicker.vue'
import LossList from '../components/LossList.vue'
import { useRouter, useRoute } from 'vue-router'
@ -221,17 +219,6 @@ const formData = ref({
fileList: []
})
const getFileList = () => {
const fileList = formData.value.fileList?.map((item) => {
return {
url: item.fileUrl,
name: item.fileName
}
})
return fileList
}
const submitting = ref(false)
//
@ -262,38 +249,6 @@ const calibrateTime = () => {
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
}
const parsePointValue = (point) => {
if (!point) {
return { longitude: null, latitude: null }
@ -337,136 +292,6 @@ const handleRouteNoChange = (item) => {
formData.event.endStakeLatitude = endPoint.latitude
}
/**
* 判断图片是否可以上传
* @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/')) {
const imageCount = formData.value.fileList?.filter((item) => item.fileType === 1).length
if (imageCount == 6) {
showFailToast('只能上传六张图片!')
return false
}
return isValidImage(file)
}
//
if (file.type.startsWith('video/')) {
const videoCount = formData.value.fileList?.filter((item) => item.fileType === 2).length
if (videoCount == 1) {
showFailToast('只能上传一个视频文件!')
return false
}
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
const url = res.data
const type = isImageFile({ fileUrl: url }) ? 1 : isVideoFile({ fileUrl: url }) ? 2 : 3
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 isEmpty = (value) => {
return value === null || value === undefined || value === ''

View File

@ -0,0 +1,174 @@
<template>
<van-field :label="label" center>
<template #input>
<van-uploader
accept="video/*,image/*"
:model-value="uploaderFileList"
:after-read="afterRead"
multiple
:max-count="maxCount"
@delete="removeFile"
/>
</template>
</van-field>
</template>
<script setup>
import { computed } from 'vue'
import { showToast, showFailToast, showLoadingToast } from 'vant'
import { request } from '@shared/utils/request'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
label: {
type: String,
default: '附件'
},
maxCount: {
type: Number,
default: 7
}
})
const emit = defineEmits(['update:modelValue'])
const uploaderFileList = computed(() => {
return props.modelValue.map((item) => {
return {
url: item.fileUrl,
name: item.fileName
}
})
})
const isImageFile = (file) => {
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) => {
const videoExtensions = /\.(mp4|avi|mov|wmv|flv|mkv)$/i
if (file.fileUrl && videoExtensions.test(file.fileUrl)) return true
return false
}
const isValidImage = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
showFailToast('只能上传 JPG/PNG 格式的图片!')
return false
}
const isLt500k = file.size / 1024 < 500
if (!isLt500k) {
showFailToast(`图片大小不能超过 500KB当前大小${(file.size / 1024).toFixed(2)}KB`)
return false
}
return true
}
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
}
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
}
const checkFile = (file) => {
if (file.type.startsWith('image/')) {
const imageCount = props.modelValue.filter((item) => item.fileType === 1).length
if (imageCount === 6) {
showFailToast('只能上传六张图片!')
return false
}
return isValidImage(file)
}
if (file.type.startsWith('video/')) {
const videoCount = props.modelValue.filter((item) => item.fileType === 2).length
if (videoCount === 1) {
showFailToast('只能上传一个视频文件!')
return false
}
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
})
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 type = isImageFile({ fileUrl: res.data }) ? 1 : isVideoFile({ fileUrl: res.data }) ? 2 : 3
emit('update:modelValue', [
...props.modelValue,
{
fileName: file.name,
fileUrl: res.data,
fileType: type,
fileSize: file.size
}
])
return
}
throw new Error(res.message)
} catch (error) {
toast.close()
showToast({
type: 'fail',
message: error.message
})
}
}
const removeFile = (_file, index) => {
emit(
'update:modelValue',
props.modelValue.filter((_, currentIndex) => currentIndex !== index)
)
}
</script>