uniapp 封装上传文件组件

380 阅读5分钟

封装原因

  • 项目总会有一些奇奇怪怪的需求,但是当前项目使用的UI库的上传组件又无法满足需要
  • 上传后文件的展示有特殊的要求
  • 上传前、上传中、上传后要有特殊处理
  • 满足大多数基本的文件上传功能
  • 使用的是uniapp提供的API实现的上传功能,无需依赖第三方UI库
  • 根据项目需求更改、新增功能代码(修改方便)
  • ...

效果与功能

满足大多数基本的文件上传功能

image.png

组件参数

属性名描述示例
v-model:fileList数据双向绑定的上传的文件列表Array:[{}]
url上传的服务器地址(必传)字符串:'pmccapi.wsandos.com//common/fil…'
accept上传的文件类型字符串:'all'、'file'、'media'、'image'、'video'
multiple是否支持多选Boolean:true
maxCount最大上传数量Number:9
maxSize限制单个文件的上传大小,单位MB字符串或数字:'5'、5
maxSizeAll限制上传的所有文件的总大小,单位MB字符串或数字:'10'、10
sourceType图片和视频选择的来源Array:['album', 'camera']
compressed当accept为video时生效:是否压缩视频Boolean:true
camera当accept为video时生效:使用前置还是后置摄像头字符串:'back'、'front'
maxDuration当accept为video时生效:拍摄视频最长拍摄时间,单位秒Number:60
extension根据文件拓展名过滤Array:['.zip','.exe','.js']
isEditable文件上传组件是否可编辑(操作)Boolean:true

参数解释

accept

  • media和file只支持微信小程序
  • all只支持H5和微信小程序

sourceType

  • 当accept为image、video、media、all时生效
  • album 从相册选图,camera 使用相机
  • 如需直接开相机或直接选相册,请只使用一个选项

maxDuration

在不同平台的效果不一样,自行查看

extension

  • 暂只支持文件后缀名,例如['.zip','.exe','.js'],不支持application/msword等类似值
  • 支持场景1:accept为image并且使用平台为H5
  • 支持场景2:accept为video并且使用平台为H5
  • 支持场景3:accept为file并且使用平台为微信小程序
  • 支持场景4:accept为all并且使用平台为H5

isEditable

  • 当编辑/上传文件时值为true
  • 当只是需要展示文件时值为false

封装代码

<template>
	<div class="media-box">
		<div
			class="media-item"
			v-for="(item, index) in mediaFileList"
			:key="index"
			@click="clickFile(item)"
		>
			<image v-if="item.type == 'image'" :src="item.url" mode="scaleToFill" />
			<video
				v-if="item.type == 'video'"
				:src="item.url"
				:controls="true"
				:show-center-play-btn="false"
				object-fit="fill"
			></video>
			<div class="media-item-delete" @click.stop="deleteFile(item.uuid)" v-if="props.isEditable">
				X
			</div>
		</div>
		<div
			class="add-box"
			v-if="
				((props.multiple && fileList.length < props.maxCount) ||
					(!props.multiple && fileList.length == 0)) &&
				props.isEditable &&
				['image', 'video', 'media'].includes(props.accept)
			"
			@click="selectFile"
		>
			+
		</div>
	</div>
	<div
		class="other-box"
		v-for="(item, index) in otherFileList"
		:key="index"
		@click="clickFile(item)"
	>
		<div class="other-box-name">{{ item.name }}</div>
		<div class="other-box-delete" @click.stop="deleteFile(item.uuid)" v-if="props.isEditable">
			删除
		</div>
	</div>

	<div
		class="add-box"
		v-if="
			((props.multiple && fileList.length < props.maxCount) ||
				(!props.multiple && fileList.length == 0)) &&
			props.isEditable &&
			['file', 'all'].includes(props.accept)
		"
		@click="selectFile"
	>
		+
	</div>

	<div class="tip-overlay" :style="{ padding: `${placeholderHeight}px 0 60rpx 0` }" v-if="isTip">
		<div class="tip-title">以下文件上传失败</div>
		<div class="tip-box">
			<div class="tip-box-item" v-for="(tipFile, index) in tipFileList" :key="index">
				<div class="tip-box-item-reasons">{{ tipFile.tipReasons }}</div>
				<div class="tip-media-box">
					<div
						class="tip-media-item"
						v-for="(item, index) in tipFile.tipMediaFileList"
						:key="index"
					>
						<image v-if="item.type == 'image'" :src="item.url" mode="scaleToFill" />
						<video
							v-if="item.type == 'video'"
							:src="item.url"
							:controls="true"
							:show-center-play-btn="false"
							object-fit="fill"
						></video>
					</div>
				</div>
				<div class="tip-other-box" v-for="(item, index) in tipFile.tipOtherFileList" :key="index">
					{{ item.name }}
				</div>
			</div>
		</div>
		<div class="tip-close" @click="isTip = false">关闭</div>
	</div>

	<div class="video-popup" v-if="videoPopupShow">
		<video :src="videoPopupUrl" controls></video>
		<div class="video-popup-close" @click="videoPopupShow = false">关闭</div>
	</div>

	<myProgress
		:state="state"
		:currentSize="progressCurrentSize"
		:totalSize="progressTotalSize"
		:isFailed="isFailed"
		@closeProgress="closeProgress"
	></myProgress>
</template>

<script setup>
	import { ref, onMounted, computed, watch, getCurrentInstance } from 'vue';
	import { onLoad } from '@dcloudio/uni-app';
	const { proxy } = getCurrentInstance();

	const props = defineProps({
		url: {
			type: String,
			required: true,
		},
		accept: {
			type: String,
			default: 'image',
		},
		multiple: {
			type: Boolean,
			default: true,
		},
		maxCount: {
			type: Number,
			default: 9,
		},
		maxSize: {
			type: [String, Number],
			default: '',
		},
		maxSizeAll: {
			type: [String, Number],
			default: '',
		},
		sourceType: {
			type: Array,
			default: ['album', 'camera'],
		},
		compressed: {
			type: Boolean,
			default: true,
		},
		camera: {
			type: String,
			default: 'back',
		},
		maxDuration: {
			type: Number,
			default: 9,
		},
		extension: {
			type: Array,
			default: [],
		},
		isEditable: {
			type: Boolean,
			default: true,
		},
	});

	onMounted(() => {
		geStatusBarHeight();
		getNavBarHeight();
	});

	// 获取 顶部导航栏高度 开始
	let statusBarHeight = ref(0);
	let navBarHeight = ref(0);
	// 计算占位盒子高度
	const placeholderHeight = computed(() => {
		return statusBarHeight.value + navBarHeight.value;
	});
	// 获取状态栏高度
	function geStatusBarHeight() {
		// #ifdef H5
		statusBarHeight.value = 0;
		// #endif

		// #ifdef MP-WEIXIN || APP-PLUS
		statusBarHeight.value = uni.getWindowInfo().statusBarHeight;
		// #endif
	}
	// 获取导航栏高度
	function getNavBarHeight() {
		// #ifdef MP-WEIXIN
		const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
		navBarHeight.value =
			menuButtonInfo.height + (menuButtonInfo.top - uni.getWindowInfo().statusBarHeight) * 2 + 2;
		// #endif

		// #ifdef APP-PLUS
		if (uni.getDeviceInfo().osName == 'android') {
			navBarHeight.value = 48;
		} else {
			navBarHeight.value = 44;
		}
		// #endif

		// #ifdef H5
		navBarHeight.value = 44;
		// #endif
	}
	// 获取 顶部导航栏高度 结束

	let fileList = defineModel('fileList'); // 文件列表
	const mediaFileList = ref([]); // 图片和视频文件列表
	const otherFileList = ref([]); // 其他文件列表
	let allFileSize = 0; // 总文件大小
	watch(
		() => fileList.value.length,
		() => {
			handleFileList();
			renewAllFileSize();
			mediaFileList.value = fileList.value.filter(
				(item) => item.type == 'image' || item.type == 'video'
			);
			otherFileList.value = fileList.value.filter(
				(item) => item.type == 'document' || item.type == 'other'
			);
		},
		{
			immediate: true,
		}
	);
	function handleFileList() {
		if (fileList.value.length > 0) {
			for (let index = 0; index < fileList.value.length; index++) {
				const element = fileList.value[index];
				let data = {
					...element,
					type: element.type || getFileType(element.url),
					name: element.name || element.url.split('/').pop(),
				};
				data.uuid = data.uuid || getUniqueKey(data.type, data.name);
				fileList.value[index] = data;
			}
			// console.log('处理后的fileList', fileList.value);
		}
	}
	// 更新总文件大小
	function renewAllFileSize() {
		allFileSize = 0;
		fileList.value.forEach((item) => {
			allFileSize += item.size;
		});
	}

	// 进度条相关
	import myProgress from './myProgress.vue';
	let state = ref(false); // 进度条组件状态
	let isFailed = ref(false); // 是否失败
	// 关闭进度条
	function closeProgress() {
		state.value = false;
		isFailed.value = false;
	}
	let progressTotalSize = ref(0); // 总上传数据大小
	// 计算需要上传的数据总大小
	function getTotalSize(file) {
		let totalSize = 0;
		if (props.multiple) {
			file.forEach((item) => {
				totalSize += item.size;
			});
		} else {
			totalSize = file.size;
		}
		progressTotalSize.value = totalSize;
	}
	let progressCurrentSize = ref(0); // 当前上传数据大小

	const platform = uni.getAppBaseInfo().uniPlatform;

	// 点击选择文件
	function selectFile() {
		switch (platform) {
			case 'web':
				switch (props.accept) {
					case 'image':
						choiceImageInWeb();
						break;
					case 'video':
						choiceVideoInWeb();
						break;
					case 'media':
						choiceMediaInWeb();
						break;
					case 'file':
						choiceFileInWeb();
						break;
					case 'all':
						choiceAllInWeb();
						break;
				}
				break;
			case 'mp-weixin':
				switch (props.accept) {
					case 'image':
						choiceImageInMini();
						break;
					case 'video':
						choiceVideoInMini();
						break;
					case 'media':
						choiceMediaInMini();
						break;
					case 'file':
						choiceFileInMini();
						break;
					case 'all':
						choiceAllInMini();
						break;
				}
				break;
			case 'app':
				switch (props.accept) {
					case 'image':
						choiceImageInApp();
						break;
					case 'video':
						choiceVideoInApp();
						break;
					case 'media':
						choiceMediaInApp();
						break;
					case 'file':
						choiceFileInApp();
						break;
					case 'all':
						choiceAllInApp();
						break;
				}
				break;
		}
	}

	// 选择图片 web端
	function choiceImageInWeb() {
		uni.chooseImage({
			count: props.multiple ? props.maxCount : 1,
			sourceType: props.sourceType,
			extension: props.extension.length ? props.extension : [''],
			success: (res) => {
				let arr = res.tempFiles.map((item) => {
					return {
						type: 'image',
						url: item.path,
						size: item.size,
						name: item.name || '',
					};
				});
				afterRead(arr);
			},
			fail: () => {},
		});
	}
	// 选择视频 web端
	function choiceVideoInWeb() {
		uni.chooseVideo({
			sourceType: props.sourceType,
			compressed: props.compressed,
			maxDuration: props.maxDuration,
			camera: props.camera,
			extension: props.extension.length ? props.extension : [''],
			success: (res) => {
				let arr = [
					{
						type: 'video',
						url: res.tempFilePath,
						size: res.size,
						name: res.name || '',
					},
				];
				afterRead(arr);
			},
			fail: () => {},
		});
	}
	// 选择媒体文件 web端
	function choiceMediaInWeb() {
		uni.showToast({
			title: 'web端暂不支持选择媒体文件',
			icon: 'none',
		});
	}
	// 选择file web端
	function choiceFileInWeb() {
		uni.showToast({
			title: 'web端暂不支持选择file文件',
			icon: 'none',
		});
	}
	// 选择全部文件 web端
	function choiceAllInWeb() {
		uni.chooseFile({
			count: props.multiple ? props.maxCount : 1,
			sourceType: props.sourceType,
			extension: props.extension.length ? props.extension : [''],
			success: (res) => {
				let arr = res.tempFiles.map((item) => {
					return {
						type: getFileType(item.name),
						url: item.path,
						size: item.size,
						name: item.name,
					};
				});
				afterRead(arr);
			},
			fail: () => {},
		});
	}

	// 选择图片 微信小程序端
	function choiceImageInMini() {
		uni.chooseImage({
			count: props.multiple ? props.maxCount : 1,
			sourceType: props.sourceType,
			success: (res) => {
				let arr = res.tempFiles.map((item) => {
					return {
						type: 'image',
						url: item.path,
						size: item.size,
						name: item.name || '',
					};
				});
				afterRead(arr);
			},
			fail: () => {},
		});
	}
	// 选择视频 微信小程序端
	function choiceVideoInMini() {
		uni.chooseVideo({
			sourceType: props.sourceType,
			compressed: props.compressed,
			maxDuration: props.maxDuration,
			camera: props.camera,
			success: (res) => {
				let arr = [
					{
						type: 'video',
						url: res.tempFilePath,
						size: res.size,
						name: res.name || '',
					},
				];
				afterRead(arr);
			},
			fail: () => {},
		});
	}
	// 选择媒体文件 微信小程序端
	function choiceMediaInMini() {
		uni.chooseMedia({
			count: props.multiple ? props.maxCount : 1,
			sourceType: props.sourceType,
			maxDuration: props.maxDuration,
			camera: props.camera,
			success: (res) => {
				let arr = res.tempFiles.map((item) => {
					return {
						type: item.fileType,
						url: item.tempFilePath,
						size: item.size,
						name: '',
					};
				});
				afterRead(arr);
			},
			fail: () => {},
		});
	}
	// 选择file 微信小程序端
	function choiceFileInMini() {
		wx.chooseMessageFile({
			count: props.multiple ? props.maxCount : 1,
			type: 'file',
			extension: props.extension.length ? props.extension : [''],
			success: (res) => {
				let arr = res.tempFiles.map((item) => {
					return {
						type: getFileType(item.name),
						url: item.path,
						size: item.size,
						name: item.name,
					};
				});
				afterRead(arr);
			},
			fail: () => {},
		});
	}
	// 选择全部文件 微信小程序端
	function choiceAllInMini() {
		wx.chooseMessageFile({
			count: props.multiple ? props.maxCount : 1,
			success: (res) => {
				let arr = res.tempFiles.map((item) => {
					return {
						type: getFileType(item.name),
						url: item.path,
						size: item.size,
						name: item.name,
					};
				});
				afterRead(arr);
			},
			fail: () => {},
		});
	}

	// 选择图片 app端
	function choiceImageInApp() {
		uni.chooseImage({
			count: props.multiple ? props.maxCount : 1,
			sourceType: props.sourceType,
			success: (res) => {
				let arr = res.tempFiles.map((item) => {
					return {
						type: 'image',
						url: item.path,
						size: item.size,
						name: item.name || '',
					};
				});
				afterRead(arr);
			},
			fail: () => {},
		});
	}
	// 选择视频 app端
	function choiceVideoInApp() {
		uni.chooseVideo({
			sourceType: props.sourceType,
			compressed: props.compressed,
			maxDuration: props.maxDuration,
			camera: props.camera,
			success: (res) => {
				let arr = [
					{
						type: 'video',
						url: res.tempFilePath,
						size: res.size,
						name: res.name || '',
					},
				];
				afterRead(arr);
			},
			fail: () => {},
		});
	}
	// 选择媒体文件 app端
	function choiceMediaInApp() {
		uni.showToast({
			title: 'app端暂不支持选择媒体文件',
			icon: 'none',
		});
	}
	// 选择file app端
	function choiceFileInApp() {
		uni.showToast({
			title: 'app端暂不支持选择file文件',
			icon: 'none',
		});
	}
	// 选择全部文件 app端
	function choiceAllInApp() {
		uni.showToast({
			title: 'app端暂不支持选择全部文件',
			icon: 'none',
		});
	}

	// 上传文件之后
	async function afterRead(file) {
		try {
			state.value = true;
			if (props.multiple) {
				await uploadFiles(file);
			} else {
				await uploadFile(file[0]);
			}
			if (tipFileList.value.length) {
				isTip.value = true;
			}
		} catch {
			isFailed.value = true;
		}
		state.value = false;
	}

	// 单文件上传
	async function uploadFile(file) {
		try {
			let { type, size, url, name } = file;

			// 检查文件大小是否超过限制
			let maxSize = props.maxSize * 1024 * 1024;
			if (maxSize && size > maxSize) {
				setTimeout(() => {
					uni.showToast({ title: `最多上传 ${maxSize / 1024 / 1024} MB 的文件`, icon: 'none' });
				}, 1500);
				isFailed.value = true;
				return;
			}

			// 检查所有文件的总大小是否超过限制
			let maxSizeAll = props.maxSizeAll * 1024 * 1024;
			if (maxSizeAll && size > maxSizeAll) {
				setTimeout(() => {
					uni.showToast({ title: `最多上传 ${maxSizeAll / 1024 / 1024} MB 的文件`, icon: 'none' });
				}, 1500);
				isFailed.value = true;
				return;
			}

			progressCurrentSize.value = 0;
			getTotalSize(file);

			// 开始上传
			let res = await upload(url, 0);
			fileList.value = [
				{
					type,
					name,
					size,
					url,
					remoteUrl: res.fullurl,
					uuid: getUniqueKey(type, name),
				},
			];
		} catch (error) {
			console.log('单文件上传失败', error);
		}
	}
	// 上传错误提示相关
	const isTip = ref(false);
	const tipFileList = ref([]);
	// 多文件上传
	async function uploadFiles(file) {
		tipFileList.value = [];
		try {
			// 检查所有的文件大小是否超过限制
			let maxSize = props.maxSize * 1024 * 1024;
			if (maxSize) {
				let unqualifiedArr = []; // 不符合限制的文件
				let qualifiedArr = []; // 符合限制的文件
				file.forEach((item) => {
					if (item.size > maxSize) {
						unqualifiedArr.push(item);
					} else {
						qualifiedArr.push(item);
					}
				});
				if (unqualifiedArr.length) {
					let tipMediaFileList = [];
					let tipOtherFileList = [];
					unqualifiedArr.forEach((item) => {
						if (item.type == 'image' || item.type == 'video') {
							tipMediaFileList.push(item);
						} else {
							tipOtherFileList.push(item);
						}
					});
					tipFileList.value.push({
						tipReasons: `失败原因 : 文件大小不能超过 ${maxSize / 1024 / 1024} MB`,
						tipMediaFileList,
						tipOtherFileList,
					});
				}
				file = qualifiedArr;
			}

			// 检查所有文件的总大小是否超过限制
			let maxSizeAll = props.maxSizeAll * 1024 * 1024;
			if (maxSizeAll) {
				let allSize = allFileSize;
				let arrIndex = -1;
				let unqualifiedArr = []; // 不符合限制的文件
				let qualifiedArr = []; // 符合限制的文件
				for (let index = 0; index < file.length; index++) {
					const size = file[index].size;
					allSize += size;
					if (allSize > maxSizeAll) {
						arrIndex = index;
						break;
					}
				}
				if (arrIndex == -1) {
					qualifiedArr = file;
				} else {
					unqualifiedArr = file.slice(arrIndex, file.length);
					qualifiedArr = file.slice(0, arrIndex);
				}

				if (unqualifiedArr.length) {
					let tipMediaFileList = [];
					let tipOtherFileList = [];
					unqualifiedArr.forEach((item) => {
						if (item.type == 'image' || item.type == 'video') {
							tipMediaFileList.push(item);
						} else {
							tipOtherFileList.push(item);
						}
					});
					tipFileList.value.push({
						tipReasons: `失败原因 : 所有文件的大小不能超过 ${maxSizeAll / 1024 / 1024} MB`,
						tipMediaFileList,
						tipOtherFileList,
					});
				}
				file = qualifiedArr;
			}

			// 开始上传
			if (file.length) {
				progressCurrentSize.value = 0;
				getTotalSize(file);
				let arr = [];
				for (let index = 0; index < file.length; index++) {
					let element = file[index];
					let res = await upload(element.url, index);
					arr.push({
						type: element.type,
						name: element.name,
						size: element.size,
						url: element.url,
						remoteUrl: res.fullurl,
						uuid: getUniqueKey(element.type, element.name),
					});
				}
				fileList.value = [...fileList.value, ...arr];
			}
		} catch (error) {
			console.log('多文件上传失败', error);
		}
	}
	// 获取文件类型
	function getFileType(params) {
		let fileType = '';
		const imageExtensions = [
			'jpg',
			'jpeg',
			'png',
			'gif',
			'bmp',
			'tiff',
			'tif',
			'webp',
			'heic',
			'ico',
			'svg',
			'eps',
			'ai',
			'raw',
			'psd',
			'xcf',
			'tga',
			'dds',
		];
		const videoExtensions = [
			'mp4',
			'mkv',
			'avi',
			'mov',
			'wmv',
			'flv',
			'webm',
			'mpg',
			'mpeg',
			'3gp',
			'm4v',
			'vob',
			'rmvb',
			'f4v',
			'ts',
			'ogv',
			'mxf',
			'asf',
			'swf',
		];
		const documentExtensions = ['doc', 'xls', 'ppt', 'pdf', 'docx', 'xlsx', 'pptx'];
		const extension = params.split('.').pop().toLowerCase();
		// console.log('文件后缀', extension);
		if (imageExtensions.includes(extension)) {
			fileType = 'image';
		} else if (videoExtensions.includes(extension)) {
			fileType = 'video';
		} else if (documentExtensions.includes(extension)) {
			fileType = 'document';
		} else {
			fileType = 'other';
		}
		return fileType;
	}
	// 生成一个唯一标识:type+name+10位随机数
	function getUniqueKey(type, name) {
		return type + name + Math.ceil(Math.random() * 10000000000);
	}

	// 发送请求
	function upload(url, index) {
		return new Promise((resolve, reject) => {
			var uploadTask = uni.uploadFile({
				url: props.url,
				filePath: url,
				name: 'file',
				formData: {},
				success: (res) => {
					const data = JSON.parse(res.data);
					// console.log('后端接口返回结果', data);
					if (data.code === 200) {
						resolve(data.data);
					} else {
						reject(data);
						uni.showToast({
							icon: 'error',
							title: data.msg,
						});
					}
				},
				fail: (e) => {
					uni.showToast({
						icon: 'error',
						title: '上传失败',
					});
					reject(e);
				},
			});
			let lastProgress = 0; // 上次上传进度
			let progressOfThisUpload = 0; // 当前上传进度
			uploadTask.onProgressUpdate((res) => {
				// console.log('已经上传的数据长度', res.totalBytesSent);
				if (index) {
					if (lastProgress) {
						progressOfThisUpload = res.totalBytesSent;
						progressCurrentSize.value += progressOfThisUpload - lastProgress;
						lastProgress = progressOfThisUpload;
					} else {
						lastProgress = res.totalBytesSent;
						progressCurrentSize.value += lastProgress;
					}
				} else {
					progressCurrentSize.value = res.totalBytesSent;
				}
			});
		});
	}

	// 点击文件
	function clickFile(data) {
		switch (data.type) {
			case 'image':
				preview(data.url);
				break;
			case 'video':
				videoPopupShow.value = true;
				videoPopupUrl.value = data.url;
				break;
			case 'document':
			case 'other':
				saveAndOpenDocument(data);
				break;
		}
	}
	// 点击图片 放大
	function preview(url) {
		try {
			let urls = [];
			let index = 0;
			if (props.multiple) {
				mediaFileList.value.forEach((item) => {
					if (item.type == 'image') {
						urls.push(item.url);
					}
				});
				index = urls.indexOf(url);
			} else {
				urls = [url];
				index = 0;
			}
			uni.previewImage({
				current: index,
				urls,
			});
		} catch (error) {
			console.log('图片放大失败', error);
		}
	}

	const videoPopupShow = ref(false); // 视频弹窗
	const videoPopupUrl = ref(''); // 视频弹窗url

	// 保存文档并打开预览
	function saveAndOpenDocument(data) {
		let { url, name, type, remoteUrl } = data;
		if (platform === 'web') {
			let a = document.createElement('a');
			a.href = url;
			a.download = name;
			document.body.appendChild(a);
			a.click();
			document.body.removeChild(a);
			if (type == 'document') {
				const fileUrl = encodeURIComponent(remoteUrl || url);
				// // 使用 Microsoft Office Online 预览
				window.open(`https://view.officeapps.live.com/op/view.aspx?src=${fileUrl}`, '_blank');
				// // 使用 Google Docs Viewer 预览
				// // window.open(`https://docs.google.com/gview?url=${fileUrl}&embedded=true`, '_blank');
			}
		} else {
			uni.downloadFile({
				url: url,
				filePath: `${uni.env.USER_DATA_PATH}/${name}`, // 指定保存的路径并指定文件名
				success: (res) => {
					if (res.statusCode === 200) {
						if (type == 'document') {
							uni.openDocument({
								filePath: res.filePath,
								showMenu: true,
							});
						}
					}
				},
			});
		}
	}
	// 删除文件
	function deleteFile(uuid) {
		fileList.value = fileList.value.filter((item) => item.uuid != uuid);
	}
</script>

<style lang="scss" scoped>
	.media-box {
		display: flex;
		align-items: center;
		flex-wrap: wrap;
		.media-item {
			width: 200rpx;
			height: 200rpx;
			margin: 15rpx;
			position: relative;
			image,
			video {
				width: 100%;
				height: 100%;
				border-radius: 20rpx;
				overflow: hidden;
			}
			.media-item-delete {
				position: absolute;
				top: 0;
				right: 0;
				transform: translate(50%, -50%);
				display: flex;
				justify-content: center;
				align-items: center;
				width: 40rpx;
				height: 40rpx;
				border-radius: 50%;
				background: #f8382a;
				color: #fff;
				font-size: 20rpx;
			}
		}
	}
	.other-box {
		height: 100rpx;
		display: flex;
		justify-content: space-between;
		align-items: center;
		margin: 15rpx;
		padding: 20rpx;
		background: #ccc;
		border-radius: 10rpx;
		.other-box-name {
			flex: 1;
			font-weight: 700;
			font-size: 28rpx;
			color: #44aeed;
			// 1行显示
			white-space: nowrap;
			overflow: hidden;
			text-overflow: ellipsis;
			// direction: rtl; // 文本从右到左
		}
		.other-box-delete {
			margin-left: 20rpx;
			padding: 10rpx 20rpx;
			border-radius: 20rpx;
			background: #f8382a;
			color: #fff;
			font-size: 28rpx;
		}
	}

	.add-box {
		display: flex;
		justify-content: center;
		align-items: center;
		background: #ccc;
		width: 200rpx;
		height: 200rpx;
		border-radius: 20rpx;
		overflow: hidden;
		margin: 15rpx;
		font-size: 100rpx;
	}

	.tip-overlay {
		position: fixed;
		top: 0;
		left: 0;
		background: rgba(0, 0, 0, 0.5);
		backdrop-filter: blur(10px);
		z-index: 9999;
		width: 100vw;
		height: 100vh;
		display: flex;
		justify-content: center;
		align-items: center;
		flex-direction: column;
		.tip-title {
			font-weight: 700;
			font-size: 28rpx;
			color: #44aeed;
			margin: 20rpx;
		}
		.tip-box {
			flex: 1;
			margin: 20rpx;
			overflow-y: auto;
			.tip-box-item {
				.tip-box-item-reasons {
					font-weight: 700;
					font-size: 28rpx;
					color: #44aeed;
					margin-bottom: 20rpx;
				}
				.tip-media-box {
					display: flex;
					align-items: center;
					flex-wrap: wrap;
					.tip-media-item {
						width: 200rpx;
						height: 200rpx;
						border-radius: 20rpx;
						overflow: hidden;
						margin: 15rpx;
						image,
						video {
							width: 100%;
							height: 100%;
						}
					}
				}
				.tip-other-box {
					padding: 10rpx 20rpx;
					background: #ccc;
					margin: 20rpx 0;
					width: 100%;
					font-weight: 700;
					font-size: 28rpx;
					color: #44aeed;
					// 1行显示
					white-space: nowrap;
					overflow: hidden;
					text-overflow: ellipsis;
					// direction: rtl; // 文本从右到左
				}
			}
		}
		.tip-close {
			margin: 20rpx;
			padding: 10rpx 20rpx;
			border-radius: 20rpx;
			background: #44aeed;
			color: #fff;
			font-size: 28rpx;
		}
	}

	.video-popup {
		position: fixed;
		top: 0;
		left: 0;
		background: rgba(0, 0, 0, 0.5);
		backdrop-filter: blur(10px);
		z-index: 9999;
		width: 100vw;
		height: 100vh;
		display: flex;
		justify-content: center;
		align-items: center;
		flex-direction: column;
		.video-popup-close {
			margin: 20rpx;
			padding: 10rpx 20rpx;
			border-radius: 20rpx;
			background: #44aeed;
			color: #fff;
			font-size: 28rpx;
		}
	}
</style>

注意点

  • 在页面中使用的myProgressBar组件是一个进度条组件,文章地址:juejin.cn/spost/73463…
  • upload函数:上传成功里的逻辑需要根据项目后端接口的实际情况进行修改
  • fullurl:这个参数需要根据项目后端接口返回的完整url地址的属性名进行修改

页面使用

<myUpload
    			url="https://pmcctestapi.wsandos.com/common/file/upload"
    			accept="all"
    			:multiple="true"
    			maxSize="1"
    			maxSizeAll="5"
    			v-model:fileList="fileList"
    		></myUpload>
                    
    let fileList = ref([
			{
				name: '测试图片',
				url: 'https://pic.20988.xyz/2024-08-29/1724899264-903459-preview.jpg',
			},
			{
				name: '测试视频',
				url: 'https://pmcctestapi.wsandos.com/uploads/20250322/1bef64feb00a0e602a1889419e5c715b.mp4',
			},
			{
				name: '测试excel',
				url: 'https://pmcctestapi.wsandos.com/uploads/20250317/7a1a857745663355e0ad5baee21522c7.xlsx',
			},
			{
				name: '测试pdf',
				url: 'https://pmcctestapi.wsandos.com/uploads/20250317/e22fb0b88dc9f735dce1d82395d5fc23.pdf',
			},
		]);

注意

当数据回显的时候(比如,查看表单详情、审核失败重新填写表单数据)fileList中的数据最少要有一个url字段,否则无法正常回显和操作

回显逻辑可查看源码中的handleFileList函数