1.?支持图片上传进度条
2. 支持粘贴上传图片行为
3. 支持最大图片上传数量
4. 支持图片大小限制
5. 支持图片类型限制
6. 支持图片预览
由于 :on-progress 钩子没触发,因此使用了 :on-change 钩子代替实现。进度条的值先用定时器递增,等图片上传完成将进度条的值改成100即可。
使用@paste 绑定粘贴事件,获取粘贴的对象,使用getAsFile 获取图片内容即可实现上传
<template>
<div class="upload-box" @paste="handlePaste">
<div>
<div class="upload-body">
<el-input class="input-wrap" readonly></el-input>
<el-upload
action="#"
list-type="picture-card"
:class="['upload', self_disabled ? 'disabled' : '', drag ? 'no-border' : '']"
v-model:file-list="fileList"
:multiple="true"
:disabled="self_disabled"
:limit="limit"
:http-request="handleHttpUpload"
:on-change="onProgress"
:on-progress="handleProgress"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
:on-success="uploadSuccess"
:on-error="uploadError"
:drag="drag"
:accept="fileType.join(',')"
>
<!-- 空状态可以使用插槽 -->
<div class="upload-empty">
<slot name="empty">
<el-icon><Plus /></el-icon>
</slot>
</div>
<template #file="{ file }">
<img :src="showImage(file)" class="upload-image" />
<!-- 在上传中的时候 不出现删除按钮 -->
<el-icon class="delete-icon" @click.stop="handleRemove(file)" v-if="!self_disabled && !showProgress(file)"
><CircleCloseFilled
/></el-icon>
<!-- 在上传中的时候 不出现查看按钮 -->
<div v-if="!showProgress(file)" class="upload-handle" @click.stop="handlePictureCardPreview(file)">
<div class="handle-icon">
<el-icon><ZoomIn /></el-icon>
<span>查看</span>
</div>
</div>
<!-- 进度条 -->
<div class="el-progress-wrap" v-if="showProgress(file)">
<el-progress type="circle" :percentage="progressValue(file)" :show-text="false" />
</div>
</template>
</el-upload>
</div>
<!-- 提示语 -->
<div class="el-upload__tip">
<el-popover placement="top-start" :width="200" trigger="hover">
<template #reference>
<el-icon :size="16" class="mr-4"><QuestionFilled /></el-icon>
</template>
<div class="tip-fixed-content warn-color mt-4 fontSize-12">
<div>1. 支持扩展名:jpeg .jpg .png</div>
<div>2. 可以直接点击实现图片上传</div>
<div>3. 将图片拖拽至 请上传照片盒子内 即可上传图片</div>
<div>4. 复制图片Ctrl+V至灰色框内 即可上传图片</div>
</div>
</el-popover>
<slot name="tip-content">
<slot name="tip" :fileSize="fileSize"> 单张照片大小不能超过 {{ fileSize }}M, 最多上传{{ limit }}张 </slot>
</slot>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="UploadImgs">
import { ref, computed, inject, watch } from "vue";
import { Plus } from "@element-plus/icons-vue";
import { ApiUpload } from "@/api/modules/index";
import { windowOrigin } from "@/utils/util";
import { KeepAliveStore } from "@/stores/modules/keepAlive";
import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
import { ElNotification, formContextKey, formItemContextKey, ElMessage } from "element-plus";
interface UploadFileProps {
// action? :string; // 上传的地址
fileList: any[];
api?: (params: any) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
limit?: number; // 最大图片上传数 ==> 非必传(默认为 无限张)
fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
fileType?: File.ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
height?: string; // 组件高度 ==> 非必传(默认为 150px)
width?: string; // 组件宽度 ==> 非必传(默认为 150px)
borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
progress?: boolean; // 展示上传进度盒子
deleteIconSize?: string; // 删除icon的大小
progressWidth?: string; // 进度条大小
}
const props = withDefaults(defineProps<UploadFileProps>(), {
// action: `${import.meta.env.VITE_BASE_URL }//backend/entryamount/upload`,
fileList: () => [],
drag: true,
disabled: false,
limit: Infinity,
fileSize: 2,
fileType: () => ["image/jpeg", "image/png", "image/gif"],
height: "100px",
width: "100px",
borderRadius: "0px",
progress: true,
deleteIconSize: "18px",
progressWidth: "80px"
});
// 获取 el-form 组件上下文
const formContext = inject(formContextKey, void 0);
// 获取 el-form-item 组件上下文
const formItemContext = inject(formItemContextKey, void 0);
// 判断是否禁用上传和删除
const self_disabled = computed(() => {
return props.disabled || formContext?.disabled;
});
const fileList = ref<any[]>(props.fileList); // 图片地址集合
const baseUrl = windowOrigin(); // 图片路径的前缀
// 展示的图片路径
const showImage = (uploadFile: UploadFile) => {
// 成功则展示完整路径
if (uploadFile.status === "success") {
return `${baseUrl}${uploadFile.url}`;
}
// 否则展示临时路径
return uploadFile.url;
};
// 监听 props.fileList 列表默认值改变
watch(
() => props.fileList,
(n: UploadUserFile[]) => {
fileList.value = n;
}
);
// ==========【 处理进度条 】==========
type Progress = {
uid: number;
value: number;
show: boolean;
};
const progressList = ref<Progress[]>([]); // 存储进度条集合
// 增加进度条
const addUploadProgress = (uid: number) => {
progressList.value?.push({ uid, value: 0, show: true });
};
const handleProgress = (uploadFile: UploadFile) => {
console.log(uploadFile, "====>handleProgress");
}
// 监听文件上传进度时的钩子
const onProgress = (uploadFile: UploadFile) => {
console.log(uploadFile, "=====>onProgress")
if (uploadFile.status === "ready") {
// 新增一个进度条
addUploadProgress(uploadFile.uid);
}
// 找到匹配的进度条
const target = progressList.value.find(item => item.uid === uploadFile.uid);
if (!target) return;
// 这里是模拟进度条加载,一开始先手动的慢慢加载到90,等图片上传完,再改成100
const interval = setInterval(() => {
if (target.value >= 90) {
clearInterval(interval);
return;
}
target.value += 10;
}, 100);
if (uploadFile.status === "success") {
clearInterval(interval);
target.value = 100;
target.show = false;
}
};
// 控制是否展示上传进度条
const showProgress = (uploadFile: UploadFile) => {
const target = progressList.value.find(item => item.uid === uploadFile.uid);
return target?.show;
};
// 控制上传进度条
const progressValue = (uploadFile: UploadFile) => {
const target = progressList.value.find(item => item.uid === uploadFile.uid);
return target?.value;
};
// ==========【 处理图片上传前的校验 】==========
// 文件数超出
const handleExceed = () => {
ElNotification({
title: "温馨提示",
message: `当前最多只能上传 ${props.limit} 张图片,请移除后上传!`,
type: "warning"
});
};
// 文件类型出错
const handleImgTypeError = () => {
ElNotification({
title: "温馨提示",
message: "上传图片不符合所需的格式!",
type: "warning"
});
};
// 文件大小出错
const handleImgSizeError = () => {
ElNotification({
title: "温馨提示",
message: `上传图片大小不能超过 ${props.fileSize}M!`,
type: "warning"
});
};
// 图片上传错误
const uploadError = () => {
ElNotification({
title: "温馨提示",
message: "图片上传失败,请您重新上传!",
type: "error"
});
};
// 文件上传之前判断
const beforeUpload = (rawFile: { size: number; type: File.ImageMimeType; uid: number }) => {
const imgSize = rawFile.size / 1024 / 1024 < props.fileSize; // 返回布尔值
const imgType = props.fileType.includes(rawFile.type as File.ImageMimeType); // 返回布尔值
if (!imgType) {
// 上传图片类型错误
handleImgTypeError();
}
if (!imgSize)
// 上传图片大小超出限制
setTimeout(() => {
handleImgSizeError();
}, 0);
return imgType && imgSize;
};
// ==========【 处理图片上传 】==========
/**
* @description 图片上传成功
* @param response 上传响应结果
* @param uploadFile 上传的文件
* */
interface UploadEmits {
(e: "update:fileList", value: UploadUserFile[]): void;
}
const emit = defineEmits<UploadEmits>();
const uploadSuccess = (response: { fileUrl: string } | undefined, uploadFile: UploadFile) => {
if (!response) return;
uploadFile.url = response.fileUrl;
emit("update:fileList", fileList.value); // 传递数据给父组件
// 调用 el-form 内部的校验方法(可自动校验)
formItemContext?.prop && formContext?.validateField([formItemContext.prop as string]);
};
// 监听请求图片上传
const handleHttpUpload = async (options: UploadRequestOptions) => {
let formData = new FormData();
formData.append("image", options.file);
try {
const api = props.api ?? ApiUpload.uploadImg;
const { data } = await api(formData);
const params = { ...data, fileUrl: data.url };
options.onSuccess(params);
} catch (error) {
options.onError(error as any);
}
};
// 监听复制粘贴操作
const handlePaste = async (event: any) => {
// 超出图片数量的前置判断
if (fileList.value.length >= props.limit) {
handleExceed();
return;
}
let file = event.clipboardData.items[0]; // 获取clipboardData对象
if (!file.type.includes("image")) {
ElMessage.error("粘贴内容非图片!");
return;
}
let imgFile = file.getAsFile(); // 获取图片内容
if (!beforeUpload({ size: imgFile.size, type: imgFile.type, uid: event._vts })) {
return;
}
const formData = new FormData();
formData.append("image", imgFile);
fileList.value.push({ uid: event._vts, url: "/" });
// 处理进度条
addUploadProgress(event._vts);
const progressTarget = progressList.value.find(item => item.uid === event._vts);
if (!progressTarget) return;
const interval = setInterval(() => {
if (progressTarget.value >= 90) {
clearInterval(interval);
return;
}
progressTarget.value += 10;
}, 100);
const { data } = await ApiUpload.uploadImg(formData);
const target = fileList.value.find(item => item.uid === event._vts);
target.url = data.url;
clearInterval(interval);
progressTarget.value = 100;
setTimeout(() => {
progressTarget.show = false;
}, 500);
emit("update:fileList", fileList.value);
};
// 删除图片
const handleRemove = (file: UploadFile) => {
fileList.value = fileList.value.filter(item => item.uid !== file.uid);
progressList.value = progressList.value.filter(item => item.uid !== file.uid);
emit("update:fileList", fileList.value);
};
// 图片预览
const keepAliveStore = KeepAliveStore();
const handlePictureCardPreview: UploadProps["onPreview"] = file => {
keepAliveStore.openImageViewer({ url: file.url as string });
};
</script>
<style scoped lang="scss">
.is-error {
.upload {
:deep(.el-upload--picture-card),
:deep(.el-upload-dragger) {
border: 1px dashed var(--el-color-danger) !important;
&:hover {
border-color: var(--el-color-primary) !important;
}
}
}
}
.tip-fixed-content {
line-height: 18px;
}
:deep(.disabled) {
.el-upload--picture-card,
.el-upload-dragger {
cursor: not-allowed;
background: var(--el-disabled-bg-color) !important;
border: 1px dashed var(--el-border-color-darker);
&:hover {
border-color: var(--el-border-color-darker) !important;
}
}
}
.upload-box {
display: flex;
.tips {
font-size: 12px;
color: var(--el-color-info-light-5);
}
.no-border {
:deep(.el-upload--picture-card) {
border: none !important;
}
}
.upload-body {
position: relative;
display: block;
min-width: 200px;
padding: 12px;
}
:deep(.upload) {
.el-upload-dragger {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 0;
// overflow: hidden;
border: 1px dashed var(--el-border-color-darker);
border-radius: v-bind(borderRadius);
&:hover {
border: 1px dashed var(--el-color-primary);
}
}
.el-upload-dragger.is-dragover {
background-color: var(--el-color-primary-light-9);
border: 2px dashed var(--el-color-primary) !important;
}
.el-upload-list__item,
.el-upload--picture-card {
width: v-bind(width);
height: v-bind(height);
background-color: transparent;
border-radius: v-bind(borderRadius);
}
.upload-image {
width: 100%;
height: 100%;
object-fit: contain;
cursor: pointer;
}
.delete-icon {
position: absolute;
top: 0;
right: 0;
z-index: 99;
font-size: v-bind(deleteIconSize);
color: var(--el-color-error-dark-2);
cursor: pointer;
}
.upload-handle {
position: absolute;
top: 0;
right: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
cursor: pointer;
background: rgb(0 0 0 / 60%);
opacity: 0;
transition: var(--el-transition-duration-fast);
.handle-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 6%;
color: aliceblue;
.el-icon {
margin-bottom: 15%;
font-size: 12px;
}
span {
font-size: 12px;
}
}
}
.el-upload-list__item {
&:hover {
.upload-handle {
opacity: 1;
}
}
}
.upload-empty {
display: flex;
flex-direction: column;
align-items: center;
font-size: 12px;
line-height: 30px;
color: var(--el-color-info);
.el-icon {
font-size: 28px;
color: var(--el-text-color-secondary);
}
}
}
.el-upload__tip {
display: flex;
align-items: center;
line-height: 15px;
text-align: left;
}
}
.input-wrap {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
:deep(.el-input__wrapper) {
background-color: var(--el-color-info-light-9);
}
}
.el-progress-wrap {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba($color: #ffffff, $alpha: 70%);
:deep(.el-progress) {
width: v-bind(progressWidth) !important;
height: v-bind(progressWidth) !important;
}
:deep(.el-progress-circle) {
width: v-bind(progressWidth) !important;
}
}
</style>
<template>
<UploadImgs
v-model:file-list="imageList"
:limit="6"
height="60px"
width="60px"
delete-icon-size="14px"
progress-width="30px"
>
<template #empty>
<el-icon><Picture /></el-icon>
</template>
</UploadImgs>
</template>