diy-upload(开箱即用)
模仿element-plus里的el-upload实现自定义上传文件组件,实现了文件选择、文件类型验证、文件数量限制、文件移除等基本的文件上传功能,并且提供了自定义提示文字的功能。
技术栈:vue3+js
使用方式:
<template>
<DiyUpload
multiple
:limit="3"
accept="image/jpeg,image/png"
tip="请上传 JPEG 或 PNG 格式的图片文件"
@file-selected="onFileSelected"
@file-removed="onFileRemoved"
/>
</template>
<script setup>
import DiyUpload from "./DiyUpload.vue";
const onFileSelected = (newFileList) => {
console.log("文件选择后,新的文件列表:", newFileList);
};
const onFileRemoved = (newFileList) => {
console.log("文件移除后,新的文件列表:", newFileList);
};
</script>
<style scoped>
.diy-upload-wrapper {
position: absolute;
left: 20%;
top: 50%;
background: linear-gradient(
270deg,
rgba(38, 51, 67, 0.8) 16%,
rgba(16, 29, 47, 0.9) 100%
);
padding: 40px 60px;
}
</style>
DiyUpload.vue文件代码:
<template>
<div class="diy-upload">
<div class="diy-upload__file-button-wrapper">
<!-- 自定义按钮 -->
<button
aria-label="选择文件"
class="diy-upload__file-button"
@click="openFileSelector"
>
选择文件
</button>
<!-- 隐藏原生文件输入框 -->
<input
ref="fileInputRef"
type="file"
@change="handleFileChange"
:multiple="props.multiple"
:accept="props.accept"
style="display: none"
/>
<!-- 显示自定义提示文字 -->
<div v-if="props.tip" class="diy-upload__file-tip">
{
{ props.tip }}
</div>
</div>
<div v-if="fileList.length > 0" class="diy-upload__file-list">
<ul class="diy-upload__file-list-ul">
<li
class="diy-upload__file-list-li"
v-for="(file, index) in fileList"
:key="file.name"
>
<span class="diy-upload__file-list-name">{
{
file.name
}}</span>
<img
aria-label="删除文件"
class="diy-upload__file-list-icon"
@click="removeFile(index)"
src="/images/common/delete.png"
alt=""
/>
</li>
</ul>
</div>
<div v-if="showInvalidTypeMessage">
存在不允许的文件类型,请选择 {
{ props.accept }} 类型的文件。
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { ElMessage } from "element-plus";
// 定义组件的 props
const props = defineProps({
multiple: {
type: Boolean,
default: false,
},
limit: {
type: Number,
default: Infinity,
},
accept: {
type: String,
default: "*",
},
tip: {
type: String,
default: "",
},
});
const emits = defineEmits(["file-selected", "file-removed"]);
// 引用文件输入框
const fileInputRef = ref(null);
// 存储已选择的文件列表
const fileList = ref([]);
// 控制是否显示文件类型不允许的提示信息
const showInvalidTypeMessage = ref(false);
// 打开文件选择对话框
const openFileSelector = () => {
if (!fileInputRef.value) {
console.error("文件输入框引用失败");
return;
}
fileInputRef.value.click();
};
// 判断文件类型是否符合要求
const isValidFileType = (file, accept) => {
const acceptTypes = accept.split(",").map((type) => type.trim());
const fileType = file.type;
for (const acceptType of acceptTypes) {
if (
acceptType === "*" ||
acceptType === fileType ||
(acceptType.startsWith(".") && file.name.endsWith(acceptType))
) {
return true;
}
}
return false;
};
// 处理文件选择事件
const handleFileChange = () => {
if (!fileInputRef.value) return;
const newFiles = fileInputRef.value.files;
const validFiles = [];
let hasInvalidType = false;
// 过滤出符合文件类型要求的文件
for (const file of newFiles) {
if (isValidFileType(file, props.accept)) {
validFiles.push(file);
} else {
hasInvalidType = true;
}
}
const totalCount = fileList.value.length + validFiles.length;
if (totalCount <= props.limit) {
// 未超出数量限制,将符合类型要求的新文件添加到已有的文件列表中
fileList.value.push(...validFiles);
// 触发文件选择事件,传递新的文件列表
emits("file-selected", fileList.value);
} else {
// 超出数量限制,给出提示
if (ElMessage) {
ElMessage.warning(
`选择的文件数量超出限制(最多 ${props.limit} 个文件)。`
);
} else {
console.error("ElMessage 未正确引入");
}
}
showInvalidTypeMessage.value = hasInvalidType;
// 清空输入框的选择,以便下次选择文件时能正确触发 change 事件
fileInputRef.value.value = "";
};
// 移除文件的方法
const removeFile = (index) => {
fileList.value.splice(index, 1);
// 触发文件移除事件,传递新的文件列表
emits("file-removed", fileList.value);
};
</script>
<style scoped lang="less">
.diy-upload {
--font-color: #ffffff;
--button-bg-color: #4890ff;
--list-bg-color: rgba(49, 92, 155, 0.2);
.diy-upload__file-button-wrapper {
display: flex;
align-items: center;
margin-left: 6px;
.diy-upload__file-button {
font-weight: bold;
font-size: 12px;
color: var(--font-color);
line-height: 14px;
width: 68px;
height: 24px;
background: var(--button-bg-color);
border-radius: 2px 2px 2px 2px;
}
.diy-upload__file-tip {
font-weight: 400;
font-size: 12px;
color: var(--font-color);
line-height: 14px;
margin-left: 8px;
}
}
.diy-upload__file-list {
background: var(--list-bg-color);
border-radius: 4px 4px 4px 4px;
padding: 12px 16px;
margin-top: 8px;
.diy-upload__file-list-ul {
.diy-upload__file-list-li {
display: flex;
align-items: center;
justify-content: space-between;
.diy-upload__file-list-name {
font-weight: 400;
font-size: 12px;
color: var(--font-color);
line-height: 14px;
}
.diy-upload__file-list-icon {
width: 12px;
height: 12px;
cursor: pointer;
}
}
.diy-upload__file-list-li + .diy-upload__file-list-li {
margin-top: 8px;
}
}
}
}
</style>
成果图片:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理