console
<template>
<div class="file-upload-container">
<div class="drop-zone" @dragover.prevent @drop.prevent="onDrop" :class="{ 'disabled': disabled }">
<input type="file" multiple :accept="getAcceptedFileTypes()" @change="onFileSelect" ref="fileInput" style="display: none" :disabled="disabled">
<button @click="openFileDialog" :disabled="disabled || readonly">
<span>选择文件</span>
</button>
</div>
<div class="file-list-container">
<ul class="file-list">
<li v-for="file in selectedFiles" :key="file.id">
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<button @click="confirmRemoveFile(file.id)" class="delete-icon" :disabled="disabled || readonly">
<i class="el-icon-delete"></i>
</button>
</div>
<div class="progress-container" v-if="file.progress!== undefined && file.progress < 100 &&!file.uploaded">
<div class="progress-bar">
<div class="progress" :style="{ width: file.progress + '%' }"></div>
</div>
<span class="progress-text">{{ file.progress }}%</span>
</div>
</li>
</ul>
<div v-if="invalidFiles.length > 0" class="invalid-files">
<p v-if="invalidFiles.some((file) => file.invalidType)">以下文件类型不被支持:</p>
<ul>
<li v-for="file in invalidFiles.filter((file) => file.invalidType)" :key="file.id">
{{ file.name }}
<button @click="confirmRemoveInvalidFile(file.id, 'invalidType')" class="delete-icon" :disabled="disabled || readonly">
<i class="el-icon-delete"></i>
</button>
</li>
</ul>
<p v-if="invalidFiles.some((file) => file.invalidSize)">以下文件大小超出限制:</p>
<ul>
<li v-for="file in invalidFiles.filter((file) => file.invalidSize)" :key="file.id">
{{ file.name }}
<button @click="confirmRemoveInvalidFile(file.id, 'invalidSize')" class="delete-icon" :disabled="disabled || readonly">
<i class="el-icon-delete"></i>
</button>
</li>
</ul>
<p v-if="invalidFiles.some((file) => file.invalidCount)">以下文件因数量限制无法添加:</p>
<ul>
<li v-for="file in invalidFiles.filter((file) => file.invalidCount)" :key="file.id">
{{ file.name }}
<button @click="confirmRemoveInvalidFile(file.id, 'invalidCount')" class="delete-icon" :disabled="disabled || readonly">
<i class="el-icon-delete"></i>
</button>
</li>
</ul>
</div>
<button @click="clearFiles" v-if="selectedFiles.length > 0" :disabled="disabled || readonly">清空已选文件</button>
<button @click="startUpload" v-if="selectedFiles.some(file => file.progress === undefined || (file.progress < 100 &&!file.uploaded))" :disabled="disabled || readonly">开始上传</button>
</div>
</div>
</template>
<script>
import { MessageBox, Message } from 'element-ui';
export default {
model: {
prop: 'value',
event: 'input'
},
props: {
value: {
type: Array,
default: () => []
},
acceptedFileTypes: {
type: String,
default: ''
},
maxFileSize: {
type: Number,
default: Infinity
},
maxFileCount: {
type: Number,
default: Infinity
},
disabled: {
type: Boolean,
default: false
},
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
isDragging: false,
selectedFiles: this.value.map(file => ({...file, uploaded: file.uploaded || false })),
invalidFiles: [],
uploadInProgress: false,
initialFiles: this.value.map(file => file.id)
};
},
watch: {
value: {
handler(newValue) {
this.selectedFiles = newValue.map(file => ({...file, uploaded: file.uploaded || false }));
this.initialFiles = newValue.map(file => file.id);
},
deep: true
}
},
methods: {
getAcceptedFileTypes() {
if (this.acceptedFileTypes) {
return this.acceptedFileTypes;
} else {
return 'application/*,text/*';
}
},
openFileDialog() {
if (!this.disabled &&!this.readonly) {
this.$refs.fileInput.click();
}
},
onFileSelect(event) {
console.log('原生的 onFileSelect', event.target.files);
if (!this.disabled &&!this.readonly) {
const files = Array.from(event.target.files);
this.processFiles(files);
}
},
onDrop(event) {
if (!this.disabled &&!this.readonly) {
this.isDragging = false;
const files = Array.from(event.dataTransfer.files);
this.processFiles(files);
}
},
onDragOver() {
this.isDragging = true;
},
onDragLeave() {
this.isDragging = false;
},
processFiles(files) {
const validFiles = [];
const newInvalidFiles = [];
const allowedTypes = this.getAcceptedFileTypes().split(',').map((type) => type.trim());
const availableCount = this.maxFileCount - this.selectedFiles.length;
files.forEach((file) => {
const invalidType =!allowedTypes.includes('*') &&!allowedTypes.some((type) => file.type === type);
const invalidSize = file.size > this.maxFileSize;
const invalidCount = availableCount <= 0;
if (!invalidType &&!invalidSize &&!invalidCount) {
const newFile = {
id: Date.now() + Math.random(),
name: file.name,
progress: 0,
uploaded: false,
};
validFiles.push(newFile);
} else {
const newInvalidFile = {
id: Date.now() + Math.random(),
name: file.name,
invalidType,
invalidSize,
invalidCount,
};
newInvalidFiles.push(newInvalidFile);
}
});
this.selectedFiles = this.selectedFiles.concat(validFiles);
this.invalidFiles = this.invalidFiles.concat(newInvalidFiles);
this.$emit('input', this.selectedFiles);
this.$emit('files-changed', { validFiles, invalidFiles: newInvalidFiles });
},
resInputValue(refKey) {
this.$refs[refKey].value = '';
},
clearFiles() {
if (!this.disabled &&!this.readonly) {
this.selectedFiles = [];
this.invalidFiles = [];
this.resInputValue('fileInput');
this.$emit('input', []);
this.$emit('files-cleared');
}
},
startUpload() {
console.log('startUpload');
if (!this.disabled &&!this.readonly) {
this.resInputValue('fileInput');
this.uploadInProgress = true;
this.selectedFiles.forEach((file) => {
if (!this.initialFiles.includes(file.id) &&!file.uploaded) {
file.progress = 0;
this.getUploadCredential()
.then((credentialId) => {
this.uploadToTerr(file, credentialId, {
onProgress: (credentialId, progress) => {
const updatedFiles = this.selectedFiles.map((f) =>
f.id === file.id? { ...f, progress } : f
);
this.selectedFiles = updatedFiles;
this.$emit('input', this.selectedFiles);
},
onSuccess: (credentialId, status) => {
console.log(`文件 ${file.name} 上传成功,凭证 ID: ${credentialId},状态: ${status}`);
const updatedFiles = this.selectedFiles.map((f) =>
f.id === file.id? { ...f, progress: 100, uploaded: true } : f
);
this.selectedFiles = updatedFiles;
this.$emit('input', this.selectedFiles);
},
onFailure: (credentialId, status) => {
console.error(`文件 ${file.name} 上传失败,凭证 ID: ${credentialId},状态: ${status}`);
this.removeFile(file.id);
Message.error(`文件 ${file.name} 上传失败,已删除`);
},
});
})
.catch((error) => {
console.error(`获取上传凭证失败: ${error}`);
});
}
});
}
},
getUploadCredential() {
return new Promise((resolve) => {
setTimeout(() => {
const credentialId = `credential_${Date.now()}`;
resolve(credentialId);
}, 500);
});
},
uploadToTerr(file, credentialId, callbacks) {
let progress = 0;
const interval = setInterval(() => {
if (Math.random() < 0.05) {
clearInterval(interval);
callbacks.onFailure(credentialId, '上传失败');
} else {
progress += 10;
if (progress >= 100) {
clearInterval(interval);
callbacks.onSuccess(credentialId, '上传成功');
} else {
callbacks.onProgress(credentialId, progress);
}
}
}, 500);
},
confirmRemoveFile(id) {
if (!this.disabled &&!this.readonly) {
MessageBox.confirm('确定要删除这个文件吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.removeFile(id);
})
.catch(() => {
});
}
},
confirmRemoveInvalidFile(id, reason) {
if (!this.disabled &&!this.readonly) {
MessageBox.confirm('确定要删除这个无效文件吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.removeInvalidFile(id, reason);
})
.catch(() => {
});
}
},
removeFile(id) {
this.selectedFiles = this.selectedFiles.filter((file) => file.id!== id);
this.$emit('input', this.selectedFiles);
this.$emit('file-removed', id);
},
removeInvalidFile(id, reason) {
this.invalidFiles = this.invalidFiles.filter((file) => file.id!== id);
this.$emit('invalid-file-removed', id, reason);
},
},
mounted() {
const dropArea = this.$el.querySelector('.drop-zone');
dropArea.addEventListener('dragover', this.onDragOver);
dropArea.addEventListener('dragleave', this.onDragLeave);
},
beforeDestroy() {
const dropArea = this.$el.querySelector('.drop-zone');
dropArea.removeEventListener('dragover', this.onDragOver);
dropArea.removeEventListener('dragleave', this.onDragLeave);
},
};
</script>
<style scoped>
.file-upload-container {
display: flex;
flex-direction: column;
align-items: center;
}
.drop-zone {
position: relative;
margin-bottom: 20px;
}
.drop-zone button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s ease;
}
.drop-zone button:hover {
background-color: #0056b3;
}
.drop-zone.disabled button {
background-color: #ccc;
cursor: not-allowed;
}
.file-list-container {
width: 100%;
}
.file-list {
list-style-type: none;
padding: 0;
margin-top: 20px;
text-align: left;
}
.file-list li {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
padding: 10px;
border-bottom: 1px solid #eee;
transition: background-color 0.3s ease;
}
.file-list li:hover {
background-color: #f9f9f9;
}
.file-info {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.file-name {
font-size: 14px;
color: #333;
}
.progress-container {
display: flex;
align-items: center;
width: 100%;
margin-top: 5px;
}
.progress-bar {
flex: 1;
height: 10px;
background-color: #eee;
border-radius: 5px;
overflow: hidden;
margin-right: 10px;
}
.progress {
height: 100%;
background-color: #007bff;
transition: width 0.3s ease;
}
.progress-text {
font-size: 12px;
color: #007bff;
}
.invalid-files {
color: red;
margin-top: 10px;
text-align: left;
}
.delete-icon {
background: none;
border: none;
cursor: pointer;
color: #dc3545;
font-size: 18px;
}
.delete-icon:disabled {
color: #ccc;
cursor: not-allowed;
}
.delete-icon:hover {
color: #c82333;
}
</style>