feat: 文件上传基本结构
This commit is contained in:
parent
2c75d16ba4
commit
ed1f51b86b
58
packages/screen/src/component/FileUpload/FileUpload.vue
Normal file
58
packages/screen/src/component/FileUpload/FileUpload.vue
Normal 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>
|
||||
237
packages/screen/src/component/FileUpload/PreviewBlock.vue
Normal file
237
packages/screen/src/component/FileUpload/PreviewBlock.vue
Normal 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>
|
||||
141
packages/screen/src/component/FileUpload/UploadBlock.vue
Normal file
141
packages/screen/src/component/FileUpload/UploadBlock.vue
Normal 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>
|
||||
@ -35,8 +35,8 @@
|
||||
<BlockItem title="路况事件信息">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="事件类型" prop="roadConditionType">
|
||||
<el-select v-model="formData.roadConditionType" placeholder="请选择" style="width: 100%">
|
||||
<el-form-item label="事件类型">
|
||||
<el-select v-model="eventType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="水毁事件" value="水毁事件" />
|
||||
<el-option label="冰雪事件" value="冰雪事件" />
|
||||
</el-select>
|
||||
@ -206,35 +206,14 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="图片上传" prop="fileList">
|
||||
<el-upload
|
||||
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>
|
||||
<FileUpload type="image" :limit="9" v-model="formData.fileList" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="视频上传" prop="fileList">
|
||||
<el-upload v-model:file-list="videoFileList" action="#" :auto-upload="false" :limit="1" :before-upload="beforeVideoUpload" accept="video/*">
|
||||
<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>
|
||||
<FileUpload type="video" :limit="9" v-model="formData.fileList" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -394,7 +373,7 @@ import mockData from './waterMockJson.json'
|
||||
import { request } from '@/utils/request'
|
||||
import LossList from './LossList.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 route = useRoute()
|
||||
@ -411,6 +390,8 @@ const disposalMeasuresArray = ref([])
|
||||
const imageFileList = ref([])
|
||||
const videoFileList = ref([])
|
||||
|
||||
const eventType = ref('水毁事件')
|
||||
|
||||
const formData = reactive({
|
||||
// 顶层字段
|
||||
occurLocation: null, // 发生地点/路况位置
|
||||
@ -644,7 +625,12 @@ const handleSubmit = async () => {
|
||||
|
||||
// 加载编辑数据
|
||||
const loadEditData = async () => {
|
||||
initFormData(mockData)
|
||||
|
||||
if(route.query.mock) {
|
||||
initFormData(mockData)
|
||||
} else {
|
||||
initFormData({})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user