组件代码:
<template>
<div class="advanced-file-upload">
<!-- 上传区域 -->
<div
class="upload-area"
:class="{ 'drag-over': isDragOver, disabled: disabled }"
@click="handleAreaClick"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
<div class="upload-content">
<i class="el-icon-upload upload-icon"></i>
<div class="upload-text">
<p class="main-text">点击或拖拽文件到此处上传</p>
<p class="sub-text">
支持 {{ acceptTypes.join(", ") }} 格式,单个文件不超过
{{ maxSize / 1024 / 1024 }}MB
</p>
</div>
</div>
<input
ref="fileInput"
type="file"
:accept="accept"
:multiple="multiple"
:disabled="disabled"
@change="handleFileChange"
class="file-input"
/>
</div>
<!-- 文件列表 -->
<div class="file-list" v-if="fileList.length > 0">
<div
v-for="(file, index) in fileList"
:key="file.uid || file.name + index"
class="file-item"
:class="{
uploading: file.status === 'uploading',
success: file.status === 'success',
error: file.status === 'error',
}"
>
<div class="file-info">
<i class="el-icon-document file-icon"></i>
<div class="file-details">
<div class="file-name" :title="file.name">{{ file.name }}</div>
<div class="file-size">{{ formatFileSize(file.size) }}</div>
</div>
</div>
<div class="file-actions">
<!-- 上传进度 -->
<div class="upload-progress" v-if="file.status === 'uploading'">
<el-progress
:percentage="file.percentage || 0"
:stroke-width="2"
:show-text="false"
/>
</div>
<!-- 状态图标 -->
<div class="status-icons">
<i
v-if="file.status === 'success'"
class="el-icon-success success-icon"
title="上传成功"
></i>
<i
v-if="file.status === 'error'"
class="el-icon-error error-icon"
:title="file.errorMessage || '上传失败'"
></i>
<!-- 操作按钮 -->
<i
v-if="file.status === 'success' && showPreview"
class="el-icon-view preview-icon"
@click="handlePreview(file)"
title="预览"
></i>
<i
v-if="file.status === 'success' && showDownload"
class="el-icon-download download-icon"
@click="handleDownload(file)"
title="下载"
></i>
<i
class="el-icon-close remove-icon"
@click="handleRemove(file, index)"
title="删除"
></i>
</div>
</div>
</div>
</div>
<!-- 上传统计 -->
<div class="upload-stats" v-if="fileList.length > 0">
<span class="stats-text">
已选择 {{ fileList.length }} 个文件,总大小
{{ formatFileSize(totalSize) }}
</span>
<span v-if="uploadedCount > 0" class="success-text">
({{ uploadedCount }} 个成功)
</span>
<span v-if="failedCount > 0" class="error-text">
({{ failedCount }} 个失败)
</span>
</div>
</div>
</template>
<script>
export default {
name: "AdvancedFileUpload",
props: {
// 接受的文件类型
accept: {
type: String,
default:
".jpg,.jpeg,.png,.gif,.bmp,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar",
},
// 是否支持多选
multiple: {
type: Boolean,
default: true,
},
// 最大文件数量限制
maxCount: {
type: Number,
default: 10,
},
// 单个文件最大大小(字节)
maxSize: {
type: Number,
default: 10 * 1024 * 1024, // 10MB
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
// 是否显示预览按钮
showPreview: {
type: Boolean,
default: true,
},
// 是否显示下载按钮
showDownload: {
type: Boolean,
default: true,
},
// 上传API地址
action: {
type: String,
default: "/api/upload",
},
// 自定义请求头
headers: {
type: Object,
default: () => ({}),
},
// 自定义表单数据
data: {
type: Object,
default: () => ({}),
},
},
data() {
return {
fileList: [],
isDragOver: false,
uploadedCount: 0,
failedCount: 0,
};
},
computed: {
// 获取可接受的文件类型列表
acceptTypes() {
return this.accept
.split(",")
.map((type) => type.trim().replace(/^\./, "").toUpperCase());
},
// 计算总文件大小
totalSize() {
return this.fileList.reduce((total, file) => total + (file.size || 0), 0);
},
},
methods: {
/**
* 处理文件选择
* @param {Event} event - 文件选择事件
*/
handleFileChange(event) {
const files = Array.from(event.target.files);
this.processFiles(files);
// 清空input值,允许重复选择相同文件
event.target.value = "";
},
/**
* 处理拖拽进入
* @param {Event} event - 拖拽事件
*/
handleDragOver(event) {
event.preventDefault();
if (!this.disabled) {
this.isDragOver = true;
}
},
/**
* 处理拖拽离开
* @param {Event} event - 拖拽事件
*/
handleDragLeave(event) {
event.preventDefault();
this.isDragOver = false;
},
/**
* 处理拖拽放置
* @param {Event} event - 拖拽事件
*/
handleDrop(event) {
event.preventDefault();
this.isDragOver = false;
if (this.disabled) return;
const files = Array.from(event.dataTransfer.files);
this.processFiles(files);
},
/**
* 处理上传区域点击
*/
handleAreaClick() {
if (!this.disabled) {
this.$refs.fileInput.click();
}
},
/**
* 处理文件列表
* @param {Array} files - 文件数组
*/
processFiles(files) {
if (this.disabled) return;
// 检查文件数量限制
if (this.fileList.length + files.length > this.maxCount) {
this.$message.warning(`最多只能选择 ${this.maxCount} 个文件`);
return;
}
const validFiles = [];
files.forEach((file) => {
// 验证文件类型
if (!this.validateFileType(file)) {
this.$message.warning(`文件 ${file.name} 类型不支持`);
return;
}
// 验证文件大小
if (!this.validateFileSize(file)) {
this.$message.warning(`文件 ${file.name} 大小超过限制`);
return;
}
// 检查重复文件
if (this.isFileDuplicate(file)) {
this.$message.warning(`文件 ${file.name} 已存在`);
return;
}
validFiles.push(file);
});
if (validFiles.length === 0) return;
// 添加到文件列表
validFiles.forEach((file) => {
this.fileList.push({
uid: "uuid" + Math.random(),
name: file.name,
size: file.size,
raw: file,
status: "ready",
percentage: 0,
});
});
// 触发文件选择事件
this.$emit("file-selected", validFiles);
},
/**
* 验证文件类型
* @param {File} file - 文件对象
* @returns {boolean} 是否通过验证
*/
validateFileType(file) {
if (!this.accept) return true;
const acceptedTypes = this.accept
.split(",")
.map((type) => type.trim().toLowerCase());
const fileExtension = "." + file.name.split(".").pop().toLowerCase();
const fileType = file.type.toLowerCase();
return acceptedTypes.some(
(type) =>
fileExtension === type || fileType.includes(type.replace(".", ""))
);
},
/**
* 验证文件大小
* @param {File} file - 文件对象
* @returns {boolean} 是否通过验证
*/
validateFileSize(file) {
return file.size <= this.maxSize;
},
/**
* 检查重复文件
* @param {File} file - 文件对象
* @returns {boolean} 是否重复
*/
isFileDuplicate(file) {
return this.fileList.some(
(existingFile) =>
existingFile.name === file.name && existingFile.size === file.size
);
},
/**
* 格式化文件大小
* @param {number} size - 文件大小(字节)
* @returns {string} 格式化后的文件大小
*/
formatFileSize(size) {
if (size === 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const exponent = Math.floor(Math.log(size) / Math.log(1024));
const value = (size / Math.pow(1024, exponent)).toFixed(2);
return `${value} ${units[exponent]}`;
},
/**
* 处理文件删除
* @param {Object} file - 文件对象
* @param {number} index - 文件索引
*/
handleRemove(file, index) {
this.fileList.splice(index, 1);
this.$emit("file-removed", file);
// 更新统计计数
if (file.status === "success") {
this.uploadedCount--;
} else if (file.status === "error") {
this.failedCount--;
}
},
/**
* 处理文件预览
* @param {Object} file - 文件对象
*/
handlePreview(file) {
this.$emit("file-preview", file);
},
/**
* 处理文件下载
* @param {Object} file - 文件对象
*/
handleDownload(file) {
this.$emit("file-download", file);
},
/**
* 开始上传所有文件
*/
async uploadAll() {
const filesToUpload = this.fileList.filter(
(file) => file.status === "ready"
);
if (filesToUpload.length === 0) {
this.$message.info("没有需要上传的文件");
return;
}
this.$emit("upload-start", filesToUpload);
for (const file of filesToUpload) {
await this.uploadFile(file);
}
this.$emit("upload-complete", {
success: this.uploadedCount,
failed: this.failedCount,
total: this.fileList.length,
});
},
/**
* 上传单个文件
* @param {Object} file - 文件对象
*/
async uploadFile(file) {
file.status = "uploading";
try {
const formData = new FormData();
formData.append("file", file.raw);
// 添加自定义表单数据
Object.entries(this.data).forEach(([key, value]) => {
formData.append(key, value);
});
const response = await this.$QueryService.fetch({
url: this.action,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
...this.headers,
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
file.percentage = percentCompleted;
this.$emit("upload-progress", { file, progress: percentCompleted });
},
});
if (response.code === "1") {
file.status = "success";
file.response = response.result;
this.uploadedCount++;
this.$emit("upload-success", { file, response });
} else {
throw new Error(response.message || "上传失败");
}
} catch (error) {
file.status = "error";
file.errorMessage = error.message;
this.failedCount++;
this.$emit("upload-error", { file, error });
}
},
/**
* 清空文件列表
*/
clearFiles() {
this.fileList = [];
this.uploadedCount = 0;
this.failedCount = 0;
this.$emit("files-cleared");
},
/**
* 获取所有文件
* @returns {Array} 文件列表
*/
getFiles() {
return this.fileList;
},
/**
* 获取成功上传的文件
* @returns {Array} 成功文件列表
*/
getSuccessFiles() {
return this.fileList.filter((file) => file.status === "success");
},
/**
* 获取上传失败的文件
* @returns {Array} 失败文件列表
*/
getFailedFiles() {
return this.fileList.filter((file) => file.status === "error");
},
},
watch: {
fileList: {
handler(newVal) {
this.$emit("file-list-change", newVal);
},
deep: true,
},
},
};
</script>
<style scoped>
.advanced-file-upload {
width: 100%;
}
.upload-area {
border: 2px dashed #dcdfe6;
border-radius: 6px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fafafa;
}
.upload-area:hover {
border-color: #409eff;
background-color: #f0f7ff;
}
.upload-area.drag-over {
border-color: #409eff;
background-color: #ecf5ff;
transform: scale(1.02);
}
.upload-area.disabled {
cursor: not-allowed;
opacity: 0.6;
background-color: #f5f7fa;
}
.upload-area.disabled:hover {
border-color: #dcdfe6;
background-color: #f5f7fa;
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.upload-icon {
font-size: 48px;
color: #c0c4cc;
}
.upload-area:hover .upload-icon {
color: #409eff;
}
.main-text {
font-size: 16px;
font-weight: 500;
color: #606266;
margin: 0;
}
.sub-text {
font-size: 12px;
color: #909399;
margin: 4px 0 0 0;
}
.file-input {
display: none;
}
.file-list {
margin-top: 16px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border: 1px solid #ebeef5;
border-radius: 4px;
margin-bottom: 8px;
background-color: #fff;
transition: all 0.3s ease;
}
.file-item:hover {
border-color: #409eff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.file-item.uploading {
border-left: 3px solid #e6a23c;
}
.file-item.success {
border-left: 3px solid #67c23a;
}
.file-item.error {
border-left: 3px solid #f56c6c;
}
.file-info {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.file-icon {
font-size: 24px;
color: #909399;
margin-right: 12px;
}
.file-details {
min-width: 0;
flex: 1;
}
.file-name {
font-size: 14px;
color: #606266;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
.file-actions {
display: flex;
align-items: center;
gap: 12px;
}
.upload-progress {
width: 80px;
}
.status-icons {
display: flex;
align-items: center;
gap: 8px;
}
.status-icons i {
font-size: 16px;
cursor: pointer;
transition: color 0.3s ease;
}
.success-icon {
color: #67c23a;
}
.error-icon {
color: #f56c6c;
}
.preview-icon:hover {
color: #409eff;
}
.download-icon:hover {
color: #67c23a;
}
.remove-icon:hover {
color: #f56c6c;
}
.upload-stats {
margin-top: 12px;
font-size: 12px;
color: #909399;
}
.stats-text {
margin-right: 8px;
}
.success-text {
color: #67c23a;
}
.error-text {
color: #f56c6c;
}
</style>
组件使用:
<template>
<div class="file-upload-demo">
<div class="demo-header">
<el-button
icon="el-icon-back"
size="small"
@click="$router.push({ name: 'demo' })"
class="back-button"
>
返回
</el-button>
<h2>高级文件上传组件演示</h2>
<p>这是一个功能丰富的文件上传组件演示页面</p>
</div>
<div class="demo-content">
<!-- 基本用法示例 -->
<div class="demo-section">
<h3>基本文件上传</h3>
<AdvancedFileUpload
ref="basicUploader"
:action="'/api/rmp/file/fillUpload'"
:max-size="5 * 1024 * 1024"
:max-count="3"
accept=".jpg,.png,.pdf,.doc,.docx"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
@upload-complete="handleUploadComplete"
/>
<div class="demo-actions">
<el-button
type="primary"
size="small"
@click="$refs.basicUploader.uploadAll()"
>
开始上传
</el-button>
<el-button size="small" @click="$refs.basicUploader.clearFiles()">
清空列表
</el-button>
</div>
</div>
<!-- 高级配置示例 -->
<div class="demo-section">
<h3>高级配置示例</h3>
<div class="config-panel">
<el-form :model="config" label-width="100px" size="small">
<el-form-item label="文件类型">
<el-select v-model="config.accept" placeholder="请选择文件类型">
<el-option
label="图片文件"
value=".jpg,.jpeg,.png,.gif,.bmp"
></el-option>
<el-option
label="文档文件"
value=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
></el-option>
<el-option label="压缩文件" value=".zip,.rar,.7z"></el-option>
<el-option label="所有文件" value="*"></el-option>
</el-select>
</el-form-item>
<el-form-item label="最大数量">
<el-input-number
v-model="config.maxCount"
:min="1"
:max="20"
></el-input-number>
</el-form-item>
<el-form-item label="最大大小">
<el-select
v-model="config.maxSize"
placeholder="请选择文件大小限制"
>
<el-option :value="1 * 1024 * 1024" label="1MB"></el-option>
<el-option :value="5 * 1024 * 1024" label="5MB"></el-option>
<el-option :value="10 * 1024 * 1024" label="10MB"></el-option>
<el-option :value="20 * 1024 * 1024" label="20MB"></el-option>
</el-select>
</el-form-item>
<el-form-item label="多选">
<el-switch v-model="config.multiple"></el-switch>
</el-form-item>
</el-form>
</div>
<AdvancedFileUpload
ref="advancedUploader"
:action="'/api/rmp/file/fillUpload'"
:accept="config.accept"
:multiple="config.multiple"
:max-count="config.maxCount"
:max-size="config.maxSize"
:data="{ category: 'demo' }"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
/>
</div>
<!-- 禁用状态示例 -->
<div class="demo-section">
<h3>禁用状态</h3>
<el-switch
v-model="disabled"
active-text="禁用"
inactive-text="启用"
></el-switch>
<AdvancedFileUpload
:disabled="disabled"
:action="'/api/rmp/file/fillUpload'"
:max-size="2 * 1024 * 1024"
/>
</div>
<!-- 状态显示 -->
<div class="status-panel" v-if="uploadStatus">
<h4>上传状态</h4>
<pre>{{ uploadStatus }}</pre>
</div>
</div>
</div>
</template>
<script>
import AdvancedFileUpload from "@/components/AdvancedFileUpload.vue";
export default {
name: "FileUploadDemo",
components: {
AdvancedFileUpload,
},
data() {
return {
config: {
accept: ".jpg,.jpeg,.png,.gif,.bmp",
maxCount: 5,
maxSize: 5 * 1024 * 1024,
multiple: true,
},
disabled: false,
uploadStatus: "",
};
},
methods: {
/**
* 处理上传成功
* @param {Object} param0 - 包含文件和响应信息的对象
*/
handleUploadSuccess({ file, response }) {
this.updateStatus(
`✅ ${file.name} 上传成功: ${JSON.stringify(response)}`
);
this.$message.success(`${file.name} 上传成功`);
},
/**
* 处理上传失败
* @param {Object} param0 - 包含文件和错误信息的对象
*/
handleUploadError({ file, error }) {
this.updateStatus(`❌ ${file.name} 上传失败: ${error.message}`);
this.$message.error(`${file.name} 上传失败: ${error.message}`);
},
/**
* 处理上传完成
* @param {Object} param0 - 包含统计信息的对象
*/
handleUploadComplete({ success, failed, total }) {
this.updateStatus(
`📊 上传完成: ${success} 成功, ${failed} 失败, 共 ${total} 个文件`
);
if (failed === 0) {
this.$message.success("所有文件上传成功");
} else {
this.$message.warning(`有 ${failed} 个文件上传失败`);
}
},
/**
* 更新状态信息
* @param {string} message - 状态消息
*/
updateStatus(message) {
this.uploadStatus = `${new Date().toLocaleTimeString()}: ${message}\n${
this.uploadStatus
}`;
// 限制状态记录数量
const lines = this.uploadStatus.split("\n");
if (lines.length > 10) {
this.uploadStatus = lines.slice(0, 10).join("\n");
}
},
/**
* 清空状态信息
*/
clearStatus() {
this.uploadStatus = "";
},
},
};
</script>
<style scoped>
.file-upload-demo {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.demo-header {
text-align: center;
margin-bottom: 30px;
position: relative;
}
.back-button {
position: absolute;
left: 0;
top: 0;
}
.demo-header h2 {
color: #303133;
margin-bottom: 8px;
}
.demo-header p {
color: #909399;
font-size: 14px;
}
.demo-content {
display: flex;
flex-direction: column;
gap: 30px;
}
.demo-section {
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 20px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.demo-section h3 {
color: #303133;
margin-bottom: 16px;
border-bottom: 1px solid #ebeef5;
padding-bottom: 8px;
}
.config-panel {
margin-bottom: 20px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 4px;
border: 1px dashed #dcdfe6;
}
.demo-actions {
margin-top: 16px;
display: flex;
gap: 12px;
}
.status-panel {
border: 1px solid #e6e8eb;
border-radius: 6px;
padding: 16px;
background-color: #fafafa;
}
.status-panel h4 {
color: #303133;
margin-bottom: 12px;
}
.status-panel pre {
background-color: #2d2d2d;
color: #f8f8f2;
padding: 12px;
border-radius: 4px;
font-size: 12px;
line-height: 1.4;
overflow-x: auto;
max-height: 200px;
overflow-y: auto;
}
/* 响应式设计 */
@media (max-width: 768px) {
.file-upload-demo {
padding: 12px;
}
.demo-section {
padding: 16px;
}
.config-panel {
padding: 12px;
}
}
</style>