效果展示:
利用input的type='file'类型,获取上传file文件,用compressorjs工具压缩到合适的尺寸大小,调用后端接口上传,后端返回对应访问地址,前端然后put直推oss
组件封装代码:
<template>
<div>
<div class='upload-box'>
<div v-for="(file, index) in files" :key="index" class="file-preview">
<div style="position: relative;">
<img :src="file.preview" :alt="file.name" class="preview-image" @click="previewImage(file)">
<img :src="require('../assets/paper/analysis/delIcon.png')" class="delIcon"
@click.stop="delFile(index)" v-if="file.progress == 100">
<div class="loading" v-else>
<van-loading size="24px" vertical color="#fff">上传中...</van-loading>
</div>
</div>
<!-- <div class="progress">
<div class="progress-bar" :style="{ width: `${file.progress}%` }"></div>
</div> -->
</div>
<div class='upload-btn' v-if="show">
<img :src="require('../assets/paper/analysis/input.png')" class="addIcon" @click="triggerFileInput">
<input type='file' :accept='accept' @change='handleFileChange' ref="fileInput" :multiple="multiple">
</div>
</div>
</div>
</template>
<script>
import Compressor from 'compressorjs';
import { ImagePreview } from 'vant';
import { Loading } from 'vant';
import { getUploadTokenUrl } from '@/api/paper';
export default {
props: {
// 是否开启多选
multiple: {
type: Boolean,
default: true
},
// 允许上传的文件类型
accept: {
type: String,
default: 'image/*'
},
// 最大上传数量
maxNum: {
type: Number,
default: 20
},
},
data() {
return {
files: [],
show: true,
abortController: new AbortController(), // 用于取消请求
imageUrl: [],
};
},
watch: {
files: {
handler(val) {
this.$emit('uploadSuccess', { imageUrl: this.imageUrl, files: this.files });
},
deep: true // 开启深度监听
}
},
beforeDestroy() {
this.abortController.abort()
},
methods: {
handleFileChange(e) {
const selectedFiles = e.target.files;
if (!selectedFiles.length) return;
if (selectedFiles.length + this.files.length == this.maxNum) {
this.show = false
}
// 检查文件数量
if (selectedFiles.length + this.files.length > this.maxNum) {
this.$toast(`一次最多只能上传${this.maxNum}张图片`);
return;
}
// 生成文件预览并添加到 files 数组
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i];
const reader = new FileReader();
reader.onload = (event) => {
this.files.push({
suffix: file.name.substring(file.name.lastIndexOf('.')),// 截取图片后辍名,
name: file.name,
size: file.size,
preview: event.target.result,
progress: 0,
file
});
};
reader.readAsDataURL(file);
}
// 开始上传文件
setTimeout(() => {
this.uploadFiles();
}, 100);
},
async uploadFiles() {
// 压缩并添加文件到 formData 中
const promises = this.files.map(el => {
return new Promise((resolve, reject) => {
new Compressor(el.file, {
quality: 0.45,
minWidth: 15,
maxHeight: 2048,
checkOrientation: false,
success: (compressedFile) => {
el.file = compressedFile
resolve(el);
},
error: (err) => {
reject(err);
}
});
});
});
try {
let fileList = await Promise.all(promises);
this.imageUrl = []
fileList.forEach(file => {
this.sendRequest(file);
})
} catch (error) {
console.error('Error during compression:', error);
}
},
sendRequest(e) {
let params = { suffix: e.suffix }
getUploadTokenUrl(params, this.abortController.signal).then(res => {
if (res.code == 200) {
let xhr = new XMLHttpRequest()
xhr.open('PUT', res.data.ossTokenUrl, true)
xhr.setRequestHeader("Content-Type", "application/octet-stream")
// 监听上传进度
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
e.progress = Math.round((event.loaded / event.total) * 100);
}
};
xhr.send(e.file)
// 判断
xhr.onreadystatechange = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
let url = xhr.responseURL
let str = url.substring(0, url.lastIndexOf("?") + 1).replace('?', ''); //截取?之前
this.imageUrl.push(str)
}
}
}
})
},
//点击上传
triggerFileInput() {
this.$refs.fileInput.click(); // 触发 input 元素的 click 事件
},
//删除图片
delFile(index) {
// console.log(index);
this.files.splice(index, 1);
this.imageUrl.splice(index, 1);
if (this.files.length < this.maxNum) {
this.show = true
console.log(this.files.length < this.maxNum);
}
},
previewImage(e) {
ImagePreview({
images: [e.preview]
});
},
}
}
</script>
<style scoped>
.upload-box {
width: 100%;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: auto auto;
gap: 10px;
}
.upload-btn {
position: relative;
display: inline-block;
}
.preview-image {
width: 150px;
height: 150px;
border-radius: 12px;
}
.loading {
width: 150px;
height: 150px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.5);
position: absolute;
top: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.delIcon {
width: 24px;
height: 24px;
position: absolute;
top: -10px;
right: 0px;
}
.addIcon {
width: 150px;
height: 150px;
cursor: pointer;
}
input {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
cursor: pointer;
}
.progress {
width: 100%;
background-color: #f3f3f3;
border-radius: 5px;
overflow: hidden;
}
.progress-bar {
height: 20px;
background-color: #4caf50;
width: 0;
transition: width 0.1s;
}
</style>