使用 uniapp 扩展组件 uni-file-picker 实现视频和图片都可以预览展示

【uniapp】---- 使用 uniapp 扩展组件 uni-file-picker 实现视频和图片都可以预览展示

1. 前言

接手得 uniapp 开发的微信小程序项目,新的开发需求是需要同时上传图片和视频,但是之前的上传都没有进行封装,都是每个页面需要的时候单独实现,现在新的需求,有多个地方都需要上传图片、视频或语音等,这样就需要封装一个组件,然后发现部分地方使用了 uni-file-picker 组件,但是 uni-file-picker 在 grid 的时候只能进行图片的展示,如果是 video 或者 all 的时候,就会直接列表展示的文件名列表,不满足我当前的需求,因此在 uni-file-picker 基础上进行再次适配当前需求的封装。

2. 实现效果

3. 分析

  1. 图片和视频同时预览展示,就需要判断上传的是否是视频或者图片;
  2. 根据接口判断是否上传文件的格式是否在允许范围内;
  3. 使用 uni-file-picker 需要改造,将文件预览列表隐藏,使用自定义的预览文件。

4. 判断是图片

javascript 复制代码
export const isImageFile = (filename) => {
	// 定义常见的图片文件扩展名
	const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
	// 获取文件扩展名
	const fileExtension = filename.split('.').pop().toLowerCase()
	// 判断文件扩展名是否在图片扩展名列表中
	return imageExtensions.includes(fileExtension)
}

5. 判断是视频

javascript 复制代码
// 判断是不是视频
export const isVideoFile = (filename) => {
	// 定义常见的视频文件扩展名
	const videoExtensions = [
		'mp4',
		'avi',
		'mov',
		'mkv',
		'flv',
		'wmv',
		'webm',
		'mpeg',
		'mpg'
	]
	// 获取文件扩展名
	const fileExtension = filename.split('.').pop().toLowerCase()
	// 判断文件扩展名是否在视频扩展名列表中
	return videoExtensions.includes(fileExtension)
}

6. 根据接口判断是否是可上传格式文件

javascript 复制代码
export const canUploadFile = (filename) => {
	const extensions = ['bmp','gif','jpg','jpe','png','mp4','mp3']
	// 获取文件扩展名
	const fileExtension = filename.split('.').pop().toLowerCase()
	// 判断文件扩展名是否在视频扩展名列表中
	return extensions.includes(fileExtension)
}

7. 预览图片和视频

uploadPreview 插槽是方便自定义预览,这里我是用 tailwindcss 实现了一个默认的视频和图片的预览样式。

ini 复制代码
<slot name="uploadPreview">
  <view class="cc mr-[20rpx] wh-[160] bd-[8] flex-none relative" v-for="(item,index) in fileLists" :key="item.url">
    <view class="icon-del-box" @click.stop="deletePic(item)">
      <view class="icon-del"></view>
      <view class="icon-del rotate"></view>
    </view>
    <image v-if="item.isImage" :src="item.url" class="wh-[160] bd-[8] flex-none"></image>
    <video v-else-if="item.isVideo" :src="item.url" class="wh-[160] bd-[8] flex-none"></video>
  </view>
</slot>

8. 使用 uni-file-picker 上传

  1. 注意:是对 list 模式的文件上传,所以直接将 mode 的值写死 list;
  2. uploadButtom 自定义上传样式。
ini 复制代码
<uni-file-picker
  :limit="limit"
  :file-mediatype="fileMediatype"
  @select="getUpload"
  :is-upload-file="false"
  :autoUpload="false"
  mode="list"
  :value="fileLists"
>
  <slot name="uploadButtom">
    <view
      class="cc bg-[#F9F9F9] wh-[160] bd-[8] b-[2rpx_#D0D0D0_dashed] flex-none"
    >
      <image :src="$icon.publishAddIcon" class="wh-[48]"></image>
    </view>
  </slot>
</uni-file-picker>

9. 完整组件实现

xml 复制代码
<template>
	<view class="ac">
		<slot name="uploadPreview">
			<view class="cc mr-[20rpx] wh-[160] bd-[8] flex-none relative" v-for="(item,index) in fileLists" :key="item.url">
				<view class="icon-del-box" @click.stop="deletePic(item)">
					<view class="icon-del"></view>
					<view class="icon-del rotate"></view>
				</view>
				<image v-if="item.isImage" :src="item.url" class="wh-[160] bd-[8] flex-none"></image>
				<video v-else-if="item.isVideo" :src="item.url" class="wh-[160] bd-[8] flex-none"></video>
			</view>
		</slot>
	  <uni-file-picker
	    :limit="limit"
	    :file-mediatype="fileMediatype"
	    @select="getUpload"
			:is-upload-file="false"
			:autoUpload="false"
			mode="list"
	    :value="fileLists"
	  >
	    <slot name="uploadButtom">
				<view
				  class="cc bg-[#F9F9F9] wh-[160] bd-[8] b-[2rpx_#D0D0D0_dashed] flex-none"
				>
				  <image :src="$icon.publishAddIcon" class="wh-[48]"></image>
				</view>
			</slot>
	  </uni-file-picker>
	</view>
</template>

<script>
	export default {
		name:"uploadFile",
		props: {
			value: {
				type: [Array],
				default () {
					return []
				}
			},
			// 最大选择个数 ,h5只能限制单选或是多选
			limit: {
				type: [Number, String],
				default: 9
			},
			// 选择文件类型  image/video/all
			fileMediatype: {
				type: String,
				default: 'image'
			},
		},
		data() {
			return {
				fileLists: []
			};
		},
		watch: {
			value: {
				handler(val){
					this.fileLists = val;
				},
				deep: true,
				immediate: true
			}
		},
		methods: {
			// 删除图片
			deletePic(e) {
				let tempFilePath = e.tempFilePath;
			  this.fileLists = this.fileLists.filter(item => item.url != tempFilePath);
			},
			getUpload(e) {
				const { name, url } = e.tempFiles[0];
				// 只允许传入图片、音频或视频,格式为 bmp、gif、jpg、jpe、png、mp3、mp4
				if(!this.$unitTool.canUploadFile(url)){
					this.$unitTool._toast({title: '只允许传入图片、音频或视频,格式为 bmp、gif、jpg、jpe、png、mp4!'})
					return false;
				}
				if(this.$unitTool.isVideoFile(url) && ['video','all'].includes(this.fileMediatype)){
					if(this.fileLists.find(item => item.url.includes("mp4"))){
						this.$unitTool._toast({title: '视频最多1个,不超过200MB!'})
						return false;
					}
				}
				uni.showLoading({
					title: "上传中...",
					mask: true
				})
				this.$axios.uploadFile(url).then(res => {
					this.fileLists = [
					  ...this.fileLists,
					  {
					    name: name,
					    extname: url.split('.').at(-1),
					    url: res.content,
							tempFilePath: res.content,
							isImage: this.$unitTool.isImageFile(url),
							isVideo: this.$unitTool.isVideoFile(url)
					  }
					]
					this.$emit('input', this.fileLists)
					uni.hideLoading()
				}).catch(err => {
					this.$unitTool._toast({title: err.msg})
					uni.hideLoading()
				})
			}
		}
	}
</script>

<style scoped>
	.rotate {
		position: absolute;
		transform: rotate(90deg);
	}
	
	.icon-del-box {
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		align-items: center;
		justify-content: center;
		position: absolute;
		top: 3px;
		right: 3px;
		height: 26px;
		width: 26px;
		border-radius: 50%;
		background-color: rgba(0, 0, 0, 0.5);
		z-index: 2;
		transform: rotate(-45deg);
	}
	
	.icon-del {
		width: 15px;
		height: 2px;
		background-color: #fff;
		border-radius: 2px;
	}
</style>

10. 实现效果

11. 总结

  1. 在工具上看着很完美,达到了我们的预期,然后使用真机进行测试,问题出现了,选择文件需要到用户列表,然后再选择文件,这一下不满足需求了;
  2. 查看 uni-file-picker 组件的上传方法,发现在上传文件使用的是 chooseMessageFile【从客户端会话选择文件。】,所以这就导致在真机会先进入客户端会话;
  3. 解决办法一:界面将上传视频和图片分开;
  4. 解决办法二:自己使用 chooseMedia 实现视频和图片的上传。

12. 方案一

注意

功能是实现了,基本也可以使用,就是感觉和最开始的需求差距有点远,因此决定使用 chooseMedia 来实现该组件。

13. 方案二

13.1 基础的 DOM 结构

ini 复制代码
<template>
	<view class="ac">
		<slot name="uploadPreview">
			<view class="cc mr-[20rpx] wh-[160] bd-[8] flex-none relative" v-for="(item,index) in fileLists" :key="item.url">
				<view class="icon-del-box" @click.stop="deletePic(item)">
					<view class="icon-del"></view>
					<view class="icon-del rotate"></view>
				</view>
				<image v-if="item.isImage" :src="item.url" class="wh-[160] bd-[8] flex-none"></image>
				<video v-else-if="item.isVideo" :src="item.url" class="wh-[160] bd-[8] flex-none"></video>
			</view>
		</slot>
		<view 
			v-if="fileLists.length < limit"
			@click="chooseMedia">
			<slot name="uploadButtom">
				<view  
				class="cc bg-[#F9F9F9] wh-[160] bd-[8] b-[2rpx_#D0D0D0_dashed] flex-none">
				  <image :src="$icon.publishAddIcon" class="wh-[48]"></image>
				</view>
			</slot>
		</view>
	</view>
</template><template>
	<view class="ac">
		<slot name="uploadPreview">
			<view class="cc mr-[20rpx] wh-[160] bd-[8] flex-none relative" v-for="(item,index) in fileLists" :key="item.url">
				<view class="icon-del-box" @click.stop="deletePic(item)">
					<view class="icon-del"></view>
					<view class="icon-del rotate"></view>
				</view>
				<image v-if="item.isImage" :src="item.url" class="wh-[160] bd-[8] flex-none"></image>
				<video v-else-if="item.isVideo" :src="item.url" class="wh-[160] bd-[8] flex-none"></video>
			</view>
		</slot>
		<view 
			v-if="fileLists.length < limit"
			@click="chooseMedia">
			<slot name="uploadButtom">
				<view  
				class="cc bg-[#F9F9F9] wh-[160] bd-[8] b-[2rpx_#D0D0D0_dashed] flex-none">
				  <image :src="$icon.publishAddIcon" class="wh-[48]"></image>
				</view>
			</slot>
		</view>
	</view>
</template>

13.2 使用 chooseMedia 实现文件的选择

  1. chooseMedia 可以选择图片、视频以及混合选择;
  2. 如果对每类文件上传有个数限制,那么进行判断文件数量是否超出限制;
  3. 将上传数据进行上传调用;
  4. 注意对单个文件是存在判断,因此需要判断是否所有图片都符合上传要求,如果符合就 Promise.all 一起上传,如果不符合,就提示不符合的要求。
kotlin 复制代码
chooseMedia(){
  uni.chooseMedia({
    count: this.limit,
    mediaType: this.fileMediatype,// ['image','video']
    success: res => {
      const files = res.tempFiles;
      const { imageCount, videoCount } = this.maxCount;
      // 判断文件上传是否超出对应限制
      if(imageCount){
        const imageFiles = [...this.fileLists,...files].filter(item => this.$unitTool.isImageFile(item.tempFilePath));
        if(imageFiles.length > imageCount){
          this.$unitTool._toast({title: `图片文件最多${imageCount}张!`})
          return false;
        }
      }
      if(videoCount){
        const videoFiles = [...this.fileLists,...files].filter(item => this.$unitTool.isVideoFile(item.tempFilePath));
        if(videoFiles.length > videoCount){
          this.$unitTool._toast({title: `视频文件最多${videoCount}个!`})
          return false;
        }
      }
      const list = files.map(item => {
        return this.uploadFile({
          url: item.tempFilePath,
          size: item.size,
          extname: item.tempFilePath.split('.').at(-1),
          name: item.tempFilePath
        })
      });
      if(!list.filter(item => item === false).length){
        this.uploadAll(list);
      }
    }
  })
}

13.3 上传多张

kotlin 复制代码
uploadAll(list){
  uni.showLoading({
    title: "上传中...",
    mask: true
  })
  Promise.all(list).then(res => {
    this.fileLists = [
      ...this.fileLists,
      ...res
    ]
    this.$emit('input', this.fileLists)
    uni.hideLoading()
  }).catch(err => {
    uni.hideLoading()
  })
},
uploadFile(opts){
  const { name, url, extname } = opts;
  // 只允许传入图片、音频或视频,格式为 bmp、gif、jpg、jpe、png、mp3、mp4
  if(!this.$unitTool.canUploadFile(url)){
    this.$unitTool._toast({title: '只允许传入图片、音频或视频,格式为 bmp、gif、jpg、jpe、png、mp4!'})
    return false;
  }
  if(this.$unitTool.isVideoFile(url) && this.maxSize.videoSize){
    if(opts.size > this.maxSize.videoSize){
      this.$unitTool._toast({title: '视频文件大小不能超过200MB!'})
      return false;
    }
  }
  if(this.$unitTool.isImageFile(url) && this.maxSize.imageSize){
    if(opts.size > this.maxSize.imageSize){
      this.$unitTool._toast({title: '图片文件大小不能超过20MB!'})
      return false;
    }
  }
  return new Promise((resolve, reject) => {
    this.$axios.uploadFile(url).then(res => {
      resolve({
        name: name,
        extname: extname,
        url: res.content,
        tempFilePath: res.content,
        isImage: this.$unitTool.isImageFile(url),
        isVideo: this.$unitTool.isVideoFile(url)
      })
    }).catch(err => {
      reject(err)
      this.$unitTool._toast({title: err.msg})
    })
  })
}

14. 组件全部代码

xml 复制代码
<template>
	<view class="ac">
		<slot name="uploadPreview">
			<view class="cc mr-[20rpx] wh-[160] bd-[8] flex-none relative" v-for="(item,index) in fileLists" :key="item.url">
				<view class="icon-del-box" @click.stop="deletePic(item)">
					<view class="icon-del"></view>
					<view class="icon-del rotate"></view>
				</view>
				<image v-if="item.isImage" :src="item.url" class="wh-[160] bd-[8] flex-none"></image>
				<video v-else-if="item.isVideo" :src="item.url" class="wh-[160] bd-[8] flex-none"></video>
			</view>
		</slot>
		<view 
			v-if="fileLists.length < limit"
			@click="chooseMedia">
			<slot name="uploadButtom">
				<view  
				class="cc bg-[#F9F9F9] wh-[160] bd-[8] b-[2rpx_#D0D0D0_dashed] flex-none">
				  <image :src="$icon.publishAddIcon" class="wh-[48]"></image>
				</view>
			</slot>
		</view>
	</view>
</template>

<script>
	export default {
		name:"uploadFile",
		props: {
			value: {
				type: [Array],
				default () {
					return []
				}
			},
			// 最大选择个数 ,h5只能限制单选或是多选
			limit: {
				type: [Number],
				default: 9
			},
			// 选择文件类型  image/video/all
			fileMediatype: {
				type: [Array],
				default(){
					return ['image','video']
				}
			},
			maxSize: {
				type: Object,
				default(){
					return {}
				}
			},
			maxCount: {
				type: Object,
				default(){
					return {}
				}
			},
		},
		data() {
			return {
				fileLists: []
			};
		},
		watch: {
			value: {
				handler(val){
					this.fileLists = val;
				},
				deep: true,
				immediate: true
			}
		},
		methods: {
			// 删除图片
			deletePic(e) {
				let tempFilePath = e.tempFilePath;
			  this.fileLists = this.fileLists.filter(item => item.url != tempFilePath);
			},
			chooseMedia(){
				uni.chooseMedia({
					count: this.limit,
					mediaType: this.fileMediatype,// ['image','video']
					success: res => {
						const files = res.tempFiles;
						const { imageCount, videoCount } = this.maxCount;
						// 判断文件上传是否超出对应限制
						if(imageCount){
							const imageFiles = files.filter(item => this.$unitTool.isImageFile(item.tempFilePath));
							if(imageFiles.length > imageCount){
								this.$unitTool._toast({title: `图片文件最多${imageCount}张!`})
								return false;
							}
						}
						if(videoCount){
							const videoFiles = files.filter(item => this.$unitTool.isVideoFile(item.tempFilePath));
							if(videoFiles.length > videoCount){
								this.$unitTool._toast({title: `视频文件最多${videoCount}个!`})
								return false;
							}
						}
						const list = files.map(item => {
							return this.uploadFile({
								url: item.tempFilePath,
								size: item.size,
								extname: item.tempFilePath.split('.').at(-1),
								name: item.tempFilePath
							})
						});
						if(!list.filter(item => item === false).length){
							this.uploadAll(list);
						}
					}
				})
			},
			uploadAll(list){
				uni.showLoading({
					title: "上传中...",
					mask: true
				})
				Promise.all(list).then(res => {
					this.fileLists = [
					  ...this.fileLists,
						...res
					]
					this.$emit('input', this.fileLists)
					uni.hideLoading()
				}).catch(err => {
					uni.hideLoading()
				})
			},
			uploadFile(opts){
				const { name, url, extname } = opts;
				// 只允许传入图片、音频或视频,格式为 bmp、gif、jpg、jpe、png、mp3、mp4
				if(!this.$unitTool.canUploadFile(url)){
					this.$unitTool._toast({title: '只允许传入图片、音频或视频,格式为 bmp、gif、jpg、jpe、png、mp4!'})
					return false;
				}
				if(this.$unitTool.isVideoFile(url) && this.maxSize.videoSize){
					if(opts.size > this.maxSize.videoSize){
						this.$unitTool._toast({title: '视频文件大小不能超过200MB!'})
						return false;
					}
				}
				if(this.$unitTool.isImageFile(url) && this.maxSize.imageSize){
					if(opts.size > this.maxSize.imageSize){
						this.$unitTool._toast({title: '图片文件大小不能超过20MB!'})
						return false;
					}
				}
				return new Promise((resolve, reject) => {
					this.$axios.uploadFile(url).then(res => {
						resolve({
							name: name,
							extname: extname,
							url: res.content,
							tempFilePath: res.content,
							isImage: this.$unitTool.isImageFile(url),
							isVideo: this.$unitTool.isVideoFile(url)
						})
					}).catch(err => {
						reject(err)
						this.$unitTool._toast({title: err.msg})
					})
				})
			}
		}
	}
</script>

<style scoped>
	.rotate {
		position: absolute;
		transform: rotate(90deg);
	}
	
	.icon-del-box {
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		align-items: center;
		justify-content: center;
		position: absolute;
		top: 3px;
		right: 3px;
		height: 26px;
		width: 26px;
		border-radius: 50%;
		background-color: rgba(0, 0, 0, 0.5);
		z-index: 2;
		transform: rotate(-45deg);
	}
	
	.icon-del {
		width: 15px;
		height: 2px;
		background-color: #fff;
		border-radius: 2px;
	}
</style>

15. 组件使用

ini 复制代码
<upload-file 
  v-model="fileLists" 
  :limit="3" 
  :maxSize="{
    imageSize: 20 * 1024 * 1024,
    videoSize: 200 * 1024 * 1024
  }"
  :maxCount="{
    imageCount: 3,
    videoCount: 1
  }"
  :fileMediatype="['mix']">
</upload-file>

16. 最终效果

17. 总结

  1. 本来想直接二次封装直接使用 uni-file-picker 的,结果发现不符合预期,查询文档后,改用 chooseMedia 进行实现,因为是对应需求开发组件,发现结果还相对简单一些。
相关推荐
Ratten3 小时前
uniapp的H5 在 UC 浏览器中返回上一页失效的解决方案
uni-app
Ratten3 小时前
uniapp 的 H5和微信小程序上传 base64 图片
uni-app
Ratten4 小时前
在 HBuilderX 中使用 tailwindcss
uni-app
江湖行骗老中医16 小时前
uniapp 引入使用u-view 完整步骤,u-view 样式不生效
uni-app
雪芽蓝域zzs17 小时前
uniapp 页面favicon.ico文件不存在提示404问题解决
uni-app
一嘴一个橘子17 小时前
uniapp 顶部tab + 占满剩余高度的内容区域swiper
javascript·uni-app
睡美人的小仙女12717 小时前
在 UniApp 中,实现下拉刷新
uni-app
iOS阿玮20 小时前
江湖传闻谷歌比苹果严格多了,那么到底有多狠?
uni-app·app·apple
!win !1 天前
uni-app支付宝端彻底禁掉下拉刷新效果
前端·小程序·uni-app