老规矩,使用简单,同样只需要传递对应的接口数据及参数,其他就不用你操心了。
支持切片上传,大小配置,基础样式以及文件类型限制等
index.vue
<template>
<div
:class="['uploader', { 'single-file': !multiple }]"
@drop.prevent="onDrop"
@dragover.prevent
@click="onClick"
:style="{
width: containerWidth,
height: containerHeight ? containerHeight + 'px' : '220px'
}"
>
<input
type="file"
:multiple="multiple"
ref="fileInput"
@change="onFileChange"
style="display: none;"
/>
<div class="upload-progress" v-if="isUploading">
<p class="current-file-name">{
{ currentFileName }}</p>
<div class="progress-bar">
<div
v-show="overallProgress > 1"
:style="{ width: overallProgress + '%' }"
></div>
</div>
<p class="progress-text">{
{ overallProgress }}%</p>
</div>
<div class="upload-preview" v-if="!isUploading && previewItems.length">
<ul style="width: 100%;">
<li
v-for="(item, index) in previewItems"
:key="index"
class="preview-item"
:style="{
height: containerHeight ? containerHeight - 30 + 'px' : '190px'
}"
>
<img
v-if="item.type.startsWith('image/')"
:src="item.url"
alt="File Preview"
class="imgStyle"
/>
<video
v-else-if="item.type.startsWith('video/')"
controls
class="videoStyle"
>
<source :src="item.url" :type="item.type" />
</video>
<div v-else class="file-preview">
<slot name="file-preview" :file="item">
<span class="file-icon">📄</span>
<span class="file-name">{
{ item.name }}</span>
</slot>
</div>
<button
class="delete-button"
@click.stop="deleteFile(item, index)"
type="button"
>
❌
</button>
</li>
</ul>
</div>
<span v-if="!files.length && !isUploading && !previewItems.length">
<p class="drop-text">点击或将文件拖拽</p>
<p class="drop-text">到这里来上传</p>
</span>
</div>
</template>
<script>
import axios from "axios";
export default {
props: {
multiple: {
type: Boolean,
default: false
},
maxSize: {
type: Number,
default: 1024 * 1024 * 1024 // 1G
},
acceptedTypes: {
type: Array,
default: () => [
"image/jpeg",
"image/png",
// "application/msword",
// "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
// "application/vnd.ms-excel",
// "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"video/mp4",
"video/x-m4v",
"video/x-matroska",
"video/quicktime",
"video/x-ms-wmv",
"video/x-flv"
]
},
chunkSize: {
type: Number,
default: 500 * 1024 * 1024 // 500MB
},
containerHeight: {
type: Number,
default: 220 // 默认高度为220
},
containerWidth: {
type: String,
default: "" // 默认宽度为100%
},
errorMessage: {
type: String,
default: ""
},
deleteEvent: {
type: Object,
default: function() {
return {
api: null, // function
params: null
};
}
},
customInterface: {
type: Object,
default: function() {
return {
api: "adminFile/upload",
params: {
isPublic: 1,
type: "img"
}
};
}
}
},
data() {
return {
BASE_API: process.env.BASE_API,
typeData: ["图片", "视频"],
files: [],
previewItems: [],
isUploading: false,
uploadedData: [],
overallProgress: 0,
totalSize: 0,
uploadedSize: 0,
currentFileName: ""
};
},
mounted() {},
methods: {
onDrop(event) {
const newFiles = Array.from(event.dataTransfer.files).filter(
file => !this.files.includes(file)
);
if (!this.multiple && newFiles.length > 1) {
this.showError("只能选择一个文件");
return;
}
this.handleNewFiles(newFiles);
},
onClick() {
this.$refs.fileInput.click();
},
onFileChange(event) {
const newFiles = Array.from(event.target.files).filter(
file => !this.files.includes(file)
);
this.handleNewFiles(newFiles);
},
handleNewFiles(newFiles) {
if (!this.multiple && newFiles.length > 0) {
this.reset(); // 重置所有状态
this.files = [...newFiles]; // 替换文件
} else {
this.files = [...this.files, ...newFiles]; // 追加文件
}
this.handleFiles();
},
handleFiles() {
if (this.files.length) {
this.totalSize = this.files.reduce(
(total, file) => total + file.size,
0
);
this.uploadedSize = 0;
this.overallProgress = 0;
this.isUploading = true;
this.uploadNextFile(0);
}
},
createPreviewItem(file) {
if (file.type.startsWith("image/") || file.type.startsWith("video/")) {
return {
url: URL.createObjectURL(file),
name: file.name,
type: file.type
};
} else {
return { name: file.name, type: file.type };
}
},
showError(message) {
this.$message.error(message);
},
removeFile(index) {
this.files.splice(index, 1);
},
deleteFile(item, index) {
this.previewItems.splice(index, 1);
this.uploadedData.splice(index, 1); // 同步删除
if (this.previewItems.length === 0) {
this.reset();
}
// 抛出事件
this.$emit("change", this.uploadedData, this.previewItems);
// 如果存在自定义删除方法 则调用
if (this.deleteEvent.api) {
let apiFun = this.deleteEvent.api
let data = this.deleteEvent.params
apiFun({
...data
}).then((res)=>{
if(res.code!=0) return this.$message.error(res.msg)
})
}
},
uploadNextFile(index) {
if (index >= this.files.length) {
this.isUploading = false;
this.$emit("all-files-uploaded", this.uploadedData, this.previewItems);
return;
}
const file = this.files[index];
if (file.size > this.maxSize) {
this.showError(`文件 ${file.name} 太大,超过 ${this.maxSize / (1024 * 1024)} MB`);
this.removeFile(index);
this.uploadNextFile(index); // 继续下一个文件
return;
}
if (!this.acceptedTypes.includes(file.type)) {
this.showError(`文件 ${file.name} 类型不正确,仅支持${this.errorMessage ? this.errorMessage : this.typeData.join(", ")}`);
this.removeFile(index);
this.uploadNextFile(index); // 继续下一个文件
return;
}
this.currentFileName = file.name;
this.uploadFile(file, index);
},
uploadFile(file, index) {
this.overallProgress = 0; // 重置进度条
let completedChunks = 0; // 已上传分块数量
const totalChunks = Math.ceil(file.size / this.chunkSize);
const timestamp = new Date().getTime();// 生成当前时间戳
this.customInterface.params.size = totalChunks // 分块数量
const uploadChunk = chunkIndex => {
this.customInterface.params.chunkId = `${timestamp}-${totalChunks}` // 生成唯一标识符
this.customInterface.params.index = chunkIndex + 1 // 分块索引
console.log(this.customInterface,'<===uploadChunk');
const start = chunkIndex * this.chunkSize;
const end = Math.min(file.size, start + this.chunkSize);
const chunk = file.slice(start, end);
let formData = new FormData();
formData.append("file", chunk, file.name);
let data = Object.keys(this.customInterface.params);
data && data.forEach(item => {
formData.append(item, this.customInterface.params[item] || "");
});
//
axios.post(this.BASE_API + this.customInterface.api, formData, {
headers: { "Content-Type": "multipart/form-data" },
onUploadProgress: progressEvent => {
if (progressEvent.lengthComputable) {
// 计算当前分块的进度
let currentProgress = (progressEvent.loaded / progressEvent.total) * 100;
// 根据已上传的分块数量加上当前分块的进度比例
this.overallProgress = Math.floor(((completedChunks + currentProgress / 100) / totalChunks) * 100);
// 确保进度不超过100%
this.overallProgress = Math.min(this.overallProgress, 100);
}
}
})
.then(response => {
completedChunks++;
// 更新总进度(考虑到可能的浮点数运算误差)
this.overallProgress = Math.floor((completedChunks / totalChunks) * 100);
if (completedChunks < totalChunks) {
uploadChunk(chunkIndex + 1);
} else {
let fileData = response.data;
if (fileData.code != 0) {
this.showError(`文件 ${file.name} 上传失败!${fileData.msg}`);
}
this.uploadedData.push(fileData);
this.previewItems.push(this.createPreviewItem(file));
this.removeFile(index);
this.uploadNextFile(index); // 递归调用,传递当前索引
this.resetFileInput();
// 抛出事件
this.$emit("change", this.uploadedData, this.previewItems);
}
})
.catch(error => {
this.showError(`文件 ${file.name} 上传失败`);
this.removeFile(index);
this.uploadNextFile(index); // 递归调用,传递当前索引
this.resetFileInput();
// 抛出错误事件
this.$emit("error", error, this.uploadedData, this.previewItems);
});
};
uploadChunk(0);
},
resetFileInput() {
if (this.$refs.fileInput) {
this.$refs.fileInput.value = "";
}
},
reset() {
this.files = [];
this.previewItems = [];
this.isUploading = false;
this.uploadedData = [];
this.overallProgress = 0;
this.totalSize = 0;
this.uploadedSize = 0;
this.currentFileName = "";
this.resetFileInput();
}
}
};
</script>
<style scoped>
.uploader {
border: 2px dashed #ccc;
padding: 10px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s ease;
position: relative;
/* height: 220px; */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow-y: auto;
}
.uploader:hover {
border-color: #888;
}
.uploader p {
margin: 0;
font-size: 18px;
color: #c3c7cf;
}
.upload-progress {
width: 100%;
margin-bottom: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.current-file-name {
font-size: 16px;
color: #666;
margin-bottom: 10px;
/* 最多显示2行 超出省略 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.progress-bar {
height: 20px;
background-color: #f3f3f3;
border-radius: 5px;
overflow: hidden;
margin: 10px 0;
width: 100%;
}
.progress-bar > div {
height: 100%;
background-color: #7ac943;
transition: width 1s ease;
}
.progress-text {
font-size: 16px;
color: #7ac943;
font-weight: bold;
margin-top: 10px;
}
.upload-preview {
width: 100%;
overflow: auto;
}
.preview-item {
height: 190px;
overflow: hidden;
position: relative;
}
.file-preview {
display: flex;
align-items: center;
gap: 10px;
}
.file-icon,
.success-icon {
font-size: 24px;
}
.file-name {
font-size: 16px;
color: #666;
/* 最多显示2行 超出省略 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.delete-button {
position: absolute;
top: 0;
right: 0;
background: none;
border: none;
font-size: 12px;
cursor: pointer;
color: red;
}
.imgStyle {
width: 100%;
height: 100%;
}
.videoStyle {
width: 100%;
height: 100%;
object-fit: cover; /* 保证图片或视频填充整个容器,不留白 */
}
.upload-preview > ul > li + li {
margin-top: 10px;
}
.single-file .upload-preview {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.drop-text {
line-height: 40px;
}
</style>
使用方法
html
<MyUpload
:acceptedTypes="acceptedTypes"
:containerHeight="220"
:containerWidth="220"
errorMessage="请上传图片或视频"
>
</MyUpload>
js
import MyUpload from "@/components/MyUpload";
export default {
components: {
MyUpload
},
data(){
return{
multiple: false,
acceptedTypes:["image/jpeg","image/png","video/mp4","video/x-m4v","video/x-matroska","video/quicktime","video/x-ms-wmv","video/x-flv"],
}
},
}
## 属性
| 属性名 | 类型 | 默认值 | 说明 |
| --------------- | ------- | ---------------------- | -------------- |
| maxSize | Number | 1024 _1024_ 1024 | 最大文件大小 |
| chunkSize | Number | 500 _1024_ 1024 | 切片大小 |
| deleteEvent | Object | {api:"",params:object} | 自定义删除接口 |
| customInterface | Object | {api:"",params:object} | 自定义上传接口 |
| multiple | Boolean | false | 是否多选上传 |