二次封装el-upload组件包含文件上传进度条、复制粘贴上传等的功能

发布时间:2024年01月17日

功能

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>

~~ END ~~

文章来源:https://blog.csdn.net/weixin_45313351/article/details/135649353
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。