Compare commits

...

3 Commits

4 changed files with 467 additions and 36 deletions

View File

@ -0,0 +1,58 @@
<!-- FileUpload.vue -->
<template>
<div class="file-upload">
<UploadBlock v-model="modelValue" :type="type" :multiple="multiple" :limit="limit" :placeholder="placeholder" />
<PreviewBlock v-model:file-list="modelValue" :type="type" />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import UploadBlock from './UploadBlock.vue'
import PreviewBlock from './PreviewBlock.vue'
const modelValue = defineModel('modelValue', {
type: Array,
default: () => []
})
const props = defineProps({
// : 'image'
type: {
type: String,
default: 'image'
},
//
action: {
type: String
},
//
headers: {
type: Object,
default: () => ({})
},
//
multiple: {
type: Boolean,
default: true
},
//
limit: {
type: Number,
default: 9
},
//
placeholder: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue', 'upload-success', 'upload-error', 'remove'])
</script>
<style scoped>
.file-upload {
width: 100%;
}
</style>

View File

@ -0,0 +1,237 @@
<!-- PreviewBlock.vue - 预览模块 -->
<template>
<div class="preview-block">
<div v-if="showFileList && showFileList.length" class="preview-list">
<div
v-for="file in showFileList"
:key="file.uid"
class="preview-item"
>
<!-- 图片类型且为图片文件时显示预览图 -->
<div v-if="type === 'image' && isImageFile(file)" class="preview-image">
<el-image
:src="file.fileUrl"
:preview-src-list="getImageUrlList()"
fit="cover"
:initial-index="getImageIndex(file)"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
<!-- 非图片或非图片类型显示图标 -->
<div v-else class="preview-icon">
<el-icon :size="40">
<component :is="getFileIcon(file)" />
</el-icon>
</div>
<div class="preview-info">
<span class="file-name" :title="file.fileName">{{ file.fileName }}</span>
<span class="file-size">{{ formatFileSize(file.fileSize) }}</span>
</div>
<div class="preview-actions">
<el-button
type="danger"
:icon="Delete"
circle
size="small"
@click="handleRemove(file)"
/>
</div>
</div>
</div>
<!-- <div v-else class="preview-empty">
<el-empty description="暂无文件" :image-size="80" />
</div> -->
</div>
</template>
<script setup>
import { computed } from 'vue'
import { Picture, Document, VideoCamera, Delete } from '@element-plus/icons-vue'
const props = defineProps({
fileList: {
type: Array,
default: () => []
},
type: {
type: String,
default: 'image'
}
})
const showFileList = computed(() => {
if(props.type == 'image') return props.fileList.filter(file => isImageFile(file))
if(props.type == 'video') return props.fileList.filter(file => isVideoFile(file))
})
const emit = defineEmits(['update:fileList'])
//
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
}
// URL
const getImageUrlList = () => {
return props.fileList
.filter(file => isImageFile(file))
.map(file => file.fileUrl)
}
//
const getImageIndex = (currentFile) => {
const imageUrls = getImageUrlList()
return imageUrls.findIndex(url => url === currentFile.fileUrl)
}
//
const getFileIcon = (file) => {
const ext = file.fileName?.split('.').pop()?.toLowerCase() || ''
//
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(ext)) {
return VideoCamera
}
//
if (['mp3', 'wav', 'flac', 'aac', 'ogg'].includes(ext)) {
return VideoCamera
}
//
return Document
}
//
const formatFileSize = (size) => {
if (!size) return ''
if (size < 1024) return size + ' B'
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'
return (size / (1024 * 1024)).toFixed(2) + ' MB'
}
//
const handleRemove = (file) => {
const index = props.fileList.findIndex(item => item.fileUrl === file.fileUrl)
emit('update:fileList', [...props.fileList.slice(0, index), ...props.fileList.slice(index + 1)] )
}
</script>
<style scoped>
.preview-block {
width: 100%;
}
.preview-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 16px;
}
.preview-item {
position: relative;
width: 120px;
border: 1px solid #ebebeb;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
background: #fafafa;
}
.preview-item:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.preview-image {
width: 120px;
height: 120px;
overflow: hidden;
}
.preview-image :deep(.el-image) {
width: 100%;
height: 100%;
}
.preview-image :deep(.el-image__inner) {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-slot {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
color: #909399;
}
.preview-icon {
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
color: #409eff;
}
.preview-info {
padding: 8px;
text-align: center;
border-top: 1px solid #ebebeb;
background: white;
}
.file-name {
display: block;
font-size: 12px;
color: #606266;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
display: block;
font-size: 10px;
color: #909399;
margin-top: 4px;
}
.preview-actions {
position: absolute;
top: 4px;
right: 4px;
opacity: 0;
transition: opacity 0.3s;
}
.preview-item:hover .preview-actions {
opacity: 1;
}
.preview-empty {
padding: 40px 0;
}
</style>

View File

@ -0,0 +1,141 @@
<!-- UploadBlock.vue -->
<template>
<div class="upload-block">
<el-button type="primary" @click="showFileInput">
<el-icon><Upload /></el-icon>选择文件</el-button
>
<input class="inner-file" ref="fileRef" type="file" @change="uploadFiles" :accept="getAccept()" />
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { Upload } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { request } from '@/utils/request'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
type: {
type: String,
default: 'image'
},
headers: {
type: Object,
default: () => ({})
},
multiple: {
type: Boolean,
default: true
},
limit: {
type: Number,
default: 9
},
//
placeholder: {
type: String,
default: ''
}
})
const fileRef = ref(null)
const emit = defineEmits(['update:modelValue'])
//
const buttonText = computed(() => {
return props.placeholder || '点击上传'
})
//
const defaultTip = computed(() => {
if (props.type === 'image') {
return '支持图片格式,单个文件不超过 10MB'
}
return '支持文件格式,单个文件不超过 10MB'
})
const showFileInput = () => {
fileRef.value.click()
}
//
const beforeUpload = (file) => {
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
ElMessage.error('文件大小不能超过 10MB!')
return false
}
if (props.type === 'image') {
const isImage = file.type.startsWith('image/')
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
}
return true
}
const getAccept = () => {
if (props.type === 'image') {
return 'image/*'
}
if (props.type == 'video') {
return 'video/*'
}
return '*/*'
}
const uploadFiles = async (event) => {
const file = event.target.files[0]
const name = file.name
try {
const formData = new FormData()
formData.append('file', file)
const res = await request({
url: '/snow-ops-platform/file/upload',
method: 'post',
data: formData
})
if (res.code === '00000') {
let fileType = 3
if(props.type == 'image') fileType = 1
if(props.type == 'video') fileType = 2
const url = res.data
const fileData = {
fileName: name,
fileUrl: url,
fileType,
fileSize: file.size
}
emit('update:modelValue', [...props.modelValue, fileData])
} else {
throw new Error(res.message)
}
} catch (error) {
console.log(error)
}
fileRef.value.value = null
}
</script>
<style scoped>
.upload-block {
position: relative;
margin-bottom: 20px;
}
.inner-file {
z-index: 0;
position: absolute;
width: 0;
height: 0;
overflow: hidden;
visibility: hidden;
}
</style>

View File

@ -35,8 +35,8 @@
<BlockItem title="路况事件信息"> <BlockItem title="路况事件信息">
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="8"> <el-col :span="8">
<el-form-item label="事件类型" prop="roadConditionType"> <el-form-item label="事件类型">
<el-select v-model="formData.roadConditionType" placeholder="请选择" style="width: 100%"> <el-select v-model="eventType" placeholder="请选择" style="width: 100%">
<el-option label="水毁事件" value="水毁事件" /> <el-option label="水毁事件" value="水毁事件" />
<el-option label="冰雪事件" value="冰雪事件" /> <el-option label="冰雪事件" value="冰雪事件" />
</el-select> </el-select>
@ -206,35 +206,14 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="12"> <el-col :span="8">
<el-form-item label="图片上传" prop="fileList"> <el-form-item label="图片上传" prop="fileList">
<el-upload <FileUpload type="image" :limit="9" v-model="formData.fileList" />
v-model:file-list="imageFileList"
action="#"
list-type="picture-card"
:auto-upload="false"
:limit="9"
:on-preview="handlePicturePreview"
:on-remove="handlePictureRemove"
:before-upload="beforeImageUpload"
accept="image/jpeg,image/png"
>
<el-icon><Plus /></el-icon>
</el-upload>
<div class="upload-tip">只能上传jpg/png格式且不超过500kb</div>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="8">
<el-form-item label="视频上传" prop="fileList"> <el-form-item label="视频上传" prop="fileList">
<el-upload v-model:file-list="videoFileList" action="#" :auto-upload="false" :limit="1" :before-upload="beforeVideoUpload" accept="video/*"> <FileUpload type="video" :limit="9" v-model="formData.fileList" />
<el-button type="primary"
><el-icon><Upload /></el-icon> 选择文件</el-button
>
</el-upload>
<div class="upload-tip">仅支持20s内的视频不超过20MB</div>
<div v-if="videoFileList.length > 0 && videoFileList[0].url" class="video-preview">
<video :src="videoFileList[0].url" controls style="width: 100%; max-height: 200px"></video>
</div>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -307,24 +286,33 @@
<!-- 已投入机械 --> <!-- 已投入机械 -->
<el-col :span="8"> <el-col :span="8">
<el-form-item label="已投入机械" prop="report.investedMachinery"> <el-form-item label="已投入机械" prop="report.investedMachinery">
<el-input-number v-model="formData.report.investedMachinery" :min="0" :precision="1" style="width: 100%" /> <el-input-number v-model="formData.report.investedMachinery" :min="0" :precision="1" style="width: 100%" placeholder="请填写">
<span class="unit-suffix">/</span> <template #suffix>
<span class="unit-text">/</span>
</template>
</el-input-number>
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 已投入人力 --> <!-- 已投入人力 -->
<el-col :span="8"> <el-col :span="8">
<el-form-item label="投入人力" prop="report.investedManpower"> <el-form-item label="投入人力" prop="report.investedManpower">
<el-input-number v-model="formData.report.investedManpower" :min="0" :step="1" style="width: 100%" /> <el-input-number v-model="formData.report.investedManpower" :min="0" :step="1" style="width: 100%" placeholder="请填写">
<span class="unit-suffix">人次</span> <template #suffix>
<span class="unit-text">人次</span>
</template>
</el-input-number>
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- 已投入资金 --> <!-- 已投入资金 -->
<el-col :span="8"> <el-col :span="8">
<el-form-item label="已投入资金" prop="report.investedFunds"> <el-form-item label="已投入资金" prop="report.investedFunds">
<el-input-number v-model="formData.report.investedFunds" :min="0" :precision="2" style="width: 100%" /> <el-input-number v-model="formData.report.investedFunds" :min="0" :precision="2" style="width: 100%" placeholder="请填写">
<span class="unit-suffix">万元</span> <template #suffix>
<span class="unit-text">万元</span>
</template>
</el-input-number>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -351,7 +339,7 @@
<!-- 恢复重建预估费用 --> <!-- 恢复重建预估费用 -->
<el-col :span="8" v-if="!isContinue"> <el-col :span="8" v-if="!isContinue">
<el-form-item label="恢复重建预估费用" prop="event.estimatedRecoveryCost"> <el-form-item label="恢复重建预估费用" prop="event.estimatedRecoveryCost">
<el-input-number v-model="formData.event.estimatedRecoveryCost" :min="0" :precision="2" style="width: 100%"> <el-input-number v-model="formData.event.estimatedRecoveryCost" :min="0" :precision="2" style="width: 100%" placeholder="请填写">
<template #suffix> <template #suffix>
<span class="unit-text">万元</span> <span class="unit-text">万元</span>
</template> </template>
@ -385,7 +373,7 @@ import mockData from './waterMockJson.json'
import { request } from '@/utils/request' import { request } from '@/utils/request'
import LossList from './LossList.vue' import LossList from './LossList.vue'
import BlockItem from '@/component/BlockItem.vue' import BlockItem from '@/component/BlockItem.vue'
import { el } from 'element-plus/es/locale/index.mjs' import FileUpload from '@/component/FileUpload/FileUpload.vue'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@ -402,6 +390,8 @@ const disposalMeasuresArray = ref([])
const imageFileList = ref([]) const imageFileList = ref([])
const videoFileList = ref([]) const videoFileList = ref([])
const eventType = ref('水毁事件')
const formData = reactive({ const formData = reactive({
// //
occurLocation: null, // / occurLocation: null, // /
@ -635,7 +625,12 @@ const handleSubmit = async () => {
// //
const loadEditData = async () => { const loadEditData = async () => {
initFormData(mockData)
if(route.query.mock) {
initFormData(mockData)
} else {
initFormData({})
}
} }
onMounted(() => { onMounted(() => {