功能
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-uploadaction="#"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,等图片上传完,再改成100const 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><UploadImgsv-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>