<template>
<div class="custom-upload">
<el-upload
ref="uploadRef"
v-bind="$attrs"
:action="action"
:headers="headers"
:name="name"
:data="data"
:multiple="multiple"
:drag="drag"
:accept="accept"
:limit="limit"
:list-type="listType"
:auto-upload="autoUpload"
:http-request="httpRequest"
:disabled="disabled"
:show-file-list="showFileList"
:file-list="fileList"
:before-upload="handleBeforeUpload"
:before-remove="handleBeforeRemove"
:on-exceed="handleExceed"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-progress="handleProgress"
>
<!-- 默认插槽:自定义触发按钮 -->
<template v-if="!$slots.default && !drag">
<i class="el-icon-plus" v-if="listType == 'picture-card'"></i>
<div style="text-align: left;" v-else>
<el-button size="small" type="primary">{{ uploadText }}</el-button>
</div>
<p v-if="tip" class="el-upload__tip">{{ tip }}</p>
</template>
<!-- 拖拽模式默认区域 -->
<template v-else-if="drag && !$slots.default">
<div class="drag-area">
<i class="el-icon-upload"></i>
<div class="drag-text">{{ dragText }}</div>
<div v-if="tip" class="el-upload__tip">{{ tip }}</div>
</div>
</template>
<!-- 完全自定义触发区域 -->
<slot v-else name="default" />
<!-- 自定义文件列表(可选) -->
<template v-if="$slots.file" #file="{ file }">
<slot name="file" :file="file" />
</template>
</el-upload>
<!-- 图片预览对话框(Element UI Dialog + Image 需单独处理) -->
<el-dialog
title="图片预览"
:visible.sync="previewVisible"
width="600px"
append-to-body
@close="handleClosePreview"
>
<img :src="previewUrl" style="width: 100%" alt="预览图片" />
</el-dialog>
</div>
</template>
<script>
import { uploadFile, downloadFile } from "@/api/file/index.js";
export default {
name: "FileUploader",
props: {
// v-model:file-list 绑定
value: {
type: Array,
default: () => []
},
//分类必填
category: {
type: String,
default: "default",
required: true
},
// 上传地址
action: {
type: String,
default: "#"
},
headers: {
type: Object,
default: () => ({})
},
name: {
type: String,
default: "file"
},
data: {
type: Object,
default: () => ({})
},
multiple: Boolean,
drag: Boolean,
accept: String,
limit: {
type: Number,
default: 5
},
maxSize: {
type: Number,
default: 5 // MB
},
listType: {
type: String,
default: "text",
validator: val => ["text", "picture", "picture-card"].includes(val)
},
autoUpload: {
type: Boolean,
default: true
},
disabled: Boolean,
showFileList: {
type: Boolean,
default: true
},
uploadText: {
type: String,
default: "点击上传"
},
dragText: {
type: String,
default: "将文件拖到此处,或点击上传"
},
tip: String,
customBeforeUpload: Function,
customBeforeRemove: Function
},
data() {
return {
fileList: this.value, // 内部文件列表
previewVisible: false,
previewUrl: ""
};
},
watch: {
value: {
handler(newVal) {
this.fileList = newVal;
},
immediate: true,
deep: true
},
fileList: {
handler(newVal) {
this.$emit("input", newVal);
},
deep: true
}
},
methods: {
// 上传前校验
handleBeforeUpload(file) {
// 自定义校验钩子
if (this.customBeforeUpload) {
const result = this.customBeforeUpload(file);
if (result === false) return false;
if (result && result.then) {
return result.catch(err => {
this.$message.error(err.message || "上传校验失败");
return false;
});
}
}
// 大小校验
if (this.maxSize > 0) {
const isOver = file.size / 1024 / 1024 > this.maxSize;
if (isOver) {
this.$message.error(
`文件 ${file.name} 大小不能超过 ${this.maxSize} MB`
);
return false;
}
}
// 类型校验(基于 accept 简单处理)
if (this.accept) {
const acceptTypes = this.accept.split(",").map(t => t.trim());
const fileType = file.type;
const fileExt =
"." +
file.name
.split(".")
.pop()
.toLowerCase();
const isValid = acceptTypes.some(type => {
if (type === fileType) return true;
if (type === fileExt) return true;
if (
type.endsWith("/*") &&
fileType.startsWith(type.replace("/*", "/"))
)
return true;
return false;
});
if (!isValid) {
this.$message.error(
`文件 ${file.name} 类型不支持,仅支持 ${this.accept}`
);
return false;
}
}
return true;
},
// 移除前校验
handleBeforeRemove(file, fileList) {
if (this.customBeforeRemove) {
return this.customBeforeRemove(file, fileList);
}
return true;
},
// 超出数量限制
handleExceed(files, fileList) {
this.$message.warning(
`最多只能上传 ${this.limit} 个文件,请先移除多余文件`
);
this.$emit("exceed", files, fileList);
},
// 文件列表变化
handleChange(file, fileList) {
this.fileList = fileList;
this.$emit("change", file, fileList);
},
// 上传成功
handleSuccess(response, file, fileList) {
setTimeout(() => {
this.$message.success(`文件 ${file.name} 上传成功`);
}, 0);
this.$emit("success", response, file, fileList);
},
// 上传失败
handleError(error, file, fileList) {
this.$message.error(`文件 ${file.name} 上传失败`);
this.$emit("error", error, file, fileList);
},
// 上传进度
handleProgress(event, file, fileList) {
this.$emit("progress", event, file, fileList);
},
// 移除文件
handleRemove(file, fileList) {
setTimeout(() => {
this.$message.info(`已移除文件 ${file.name}`);
}, 0);
this.fileList = fileList;
this.$emit("remove", file, fileList);
},
// 预览文件(增强图片预览)
handlePreview(file) {
this.$emit("preview", file);
let url = file.response.url || file.url;
if (!url && file.raw) {
url = URL.createObjectURL(file.raw);
}
if (url) {
this.$preview.open(url);
} else {
this.$message.info("该文件暂不支持预览");
}
},
handleClosePreview() {
if (this.previewUrl && this.previewUrl.startsWith("blob:")) {
URL.revokeObjectURL(this.previewUrl);
}
this.previewUrl = "";
},
httpRequest({ file, onProgress, onSuccess, onError }) {
const formData = new FormData();
formData.append("category", this.category);
formData.append("file", file);
uploadFile(formData)
.then(res => {
if (res.code === 200) {
onSuccess({
...res.result,
url: `${process.env.API_ROOT}/api/v4/files/download?name=${res.result.objectName}`
});
} else {
onError(new Error(res.message || "上传失败"));
}
})
.catch(err => {
onError(err);
});
},
// 暴露方法供父组件调用
submit() {
this.$refs.uploadRef.submit();
},
clearFiles() {
this.$refs.uploadRef.clearFiles();
},
abort(file) {
this.$refs.uploadRef.abort(file);
}
}
};
</script>
<style scoped>
.custom-upload {
width: 100%;
}
.drag-area {
text-align: center;
padding: 40px 20px;
border: 1px dashed #dcdfe6;
border-radius: 6px;
background-color: #fafafa;
transition: all 0.3s;
cursor: pointer;
}
.drag-area:hover {
border-color: #004fee;
}
.drag-area i {
font-size: 48px;
color: #909399;
margin-bottom: 12px;
display: inline-block;
}
.drag-text {
font-size: 14px;
color: #606266;
margin-bottom: 8px;
}
.el-upload__tip {
font-size: 12px;
color: #909399;
margin-top: 8px;
}
</style>