【uniapp】---- 使用 uniapp 扩展组件 uni-file-picker 实现视频和图片都可以预览展示
1. 前言
接手得 uniapp 开发的微信小程序项目,新的开发需求是需要同时上传图片和视频,但是之前的上传都没有进行封装,都是每个页面需要的时候单独实现,现在新的需求,有多个地方都需要上传图片、视频或语音等,这样就需要封装一个组件,然后发现部分地方使用了 uni-file-picker 组件,但是 uni-file-picker 在 grid 的时候只能进行图片的展示,如果是 video 或者 all 的时候,就会直接列表展示的文件名列表,不满足我当前的需求,因此在 uni-file-picker 基础上进行再次适配当前需求的封装。
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 上传
- 注意:是对 list 模式的文件上传,所以直接将 mode 的值写死 list;
- 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. 总结
- 在工具上看着很完美,达到了我们的预期,然后使用真机进行测试,问题出现了,选择文件需要到用户列表,然后再选择文件,这一下不满足需求了;
- 查看 uni-file-picker 组件的上传方法,发现在上传文件使用的是 chooseMessageFile【从客户端会话选择文件。】,所以这就导致在真机会先进入客户端会话;
- 解决办法一:界面将上传视频和图片分开;
- 解决办法二:自己使用 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 实现文件的选择
- chooseMedia 可以选择图片、视频以及混合选择;
- 如果对每类文件上传有个数限制,那么进行判断文件数量是否超出限制;
- 将上传数据进行上传调用;
- 注意对单个文件是存在判断,因此需要判断是否所有图片都符合上传要求,如果符合就 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. 总结
- 本来想直接二次封装直接使用 uni-file-picker 的,结果发现不符合预期,查询文档后,改用 chooseMedia 进行实现,因为是对应需求开发组件,发现结果还相对简单一些。