<template>
<view class="cl-updata">
<view class="file-list" :style="[listRowStyle]">
<view v-for="(item, index) in modelValue" @tap.stop="clickSelectedFile(item, index)" class="file-list-item">
<image
class="_image"
v-if="item.fileType === 'image'"
:src="setFileUrl(item.watermarkUrl) || setFileUrl(item.url)"
:style="{borderRadius:'12rpx',overflow:'hidden'}"
mode="aspectFill">
</image>
<div class="_image" style="background-color: #d3e3fd;" v-if="item.fileType === 'video'">
<image
class="_video"
:src="setFileUrl(item.poster) || setFileUrl(item.url)"
:style="{borderRadius:'12rpx',overflow:'hidden'}"
:fade-show="false"
mode="aspectFill">
</image>
<!-- <image :src="playImg" mode="aspectFill" ></image> -->
<!-- <div class="play"></div> -->
<u-icon name="play-right-fill" class="play" color="#fff" size="30" ></u-icon>
</div>
<image
class="_image"
v-if="item.fileType !== 'image' && item.fileType !== 'video' "
:src="setFileUrl(item.watermarkUrl) || setFileUrl(item.url)"
:style="{borderRadius:'12rpx',overflow:'hidden'}"
mode="aspectFill">
</image>
<view class="remove" v-if="remove" @tap.stop="deleteSelectedFile(item, index)">
<u-icon name="close" class="image" size="16" color="#fff" ></u-icon>
</view>
</view>
<view v-if="add && FileList.length < max" @tap.stop="selectFileTypeOnAdd" class="file-list-item">
<slot name="addImg">
<div class="add-image">
<image style="width: 100%;height: 100%;" src="@/static/img/xiangji.png" mode="widthFix"></image>
</div>
</slot>
</view>
</view>
<Teleport to="body">
<div v-if="tempVideoUrl"
style="
background-color: #000 !important;
position: fixed !important;
top: 0 !important;
right: 0 !important;
bottom: 0 !important;
left: 0 !important;
z-index: 99999 !important;
display: flex;
flex-direction: column;
"
>
<u-icon @click="tempVideoUrl = ''" :blod="true" name="close" style="margin-left: 30rpx;margin-top: 50rpx;" size="24" color="#fff" ></u-icon>
<div style="box-sizing: border-box;padding: 30rpx;width: 100%;flex: 1;overflow: hidden;background-color: transparent;" @tap.stop>
<video style="width: 100%;height: 100%;background-color: transparent;" autoplay :src="setFileUrl(tempVideoUrl)"></video>
</div>
</div>
</Teleport>
</view>
</template>
<script>
export default {
name: "cl-upload",
components: { },
props: {
//受控图片列表
// #ifdef VUE2
value: {
type: Array,
default: () => [],
},
// #endif
// #ifdef VUE3
modelValue: {
type: Array,
default: () => [],
},
// #endif
// 存储云类型 oss阿里云 vframe七牛云 process腾讯云 other其他
cloudType: {
type: String,
default: 'oss'
},
// 标识符,即后端接口参数名
fileName: {
type: String,
default: 'file'
},
// 文件类型 'image', 'video', 'all'
fileType: {
type: String,
default: 'all'
},
// 上传图片参数
imageFormData: {
type: Object,
default: () => {
return {}
}
},
// 上传视频参数
videoFromData: {
type: Object,
default: () => { }
},
// 必选参数,上传的地址
action: {
type: String,
default: ''
},
// 启用目录, 仅unicloud阿里云支持
// https://uniapp.dcloud.net.cn/uniCloud/storage.html#storage-dir
cloudPathAsRealPath: {
type: Boolean,
default: false
},
// 设置上传的请求头部
headers: {
type: Object,
default: () => { }
},
// 上传时附带的额外参数
data: {
type: Object,
default: () => { }
},
// 是否开启预览图片
isPreviewImage: {
type: Boolean,
default: true
},
// 图片指示器样式,可取值:"default" - 底部圆点指示器; "number" - 顶部数字指示器; "none" - 不显示指示器。
indicator: {
type: String,
default: 'none'
},
// 是否在选取文件后立即进行上传
autoUpload: {
type: Boolean,
default: true
},
// 是否显示删除按钮
remove: {
type: Boolean,
default: true
},
// 是否添加按钮
add: {
type: Boolean,
default: true
},
// 最多显示数量
max: {
type: Number,
default: 5
},
// 视频最大上传数量
maxVideo: {
type: Number,
default: 0
},
// 列表样式
listStyle: {
type: Object,
default: () => { }
},
// 删除提示弹窗标题
deleteTitle: {
type: String,
default: '提示'
},
// 删除提示弹窗文案
deleteText: {
type: String,
default: '您确认要删除吗?'
},
// 加载文案
loadingText: {
type: String,
default: '正在上传中...'
},
// 是否开启删除前钩子
useBeforeDelete: {
type: Boolean,
default: false
},
// 是否开启上传前钩子
useBeforeUpload: {
type: Boolean,
default: false
},
},
data() {
return {
// 渲染列表
FileList: [],
// 预览视频地址
tempVideoUrl: '',
// 临时文件列表
tempFile_paths: [],
};
},
watch: {
// #ifdef VUE2
'value': {
handler: function (newVal, oldVal) {
this.FileList = newVal;
},
deep: true,
immediate: true
},
// #endif
// #ifdef VUE3
'modelValue': {
handler: function (newVal, oldVal) {
this.FileList = newVal;
},
deep: true,
immediate: true
},
// #endif
},
computed: {
listRowStyle() {
const style = {
'grid-template-columns': `repeat(${this.listStyle?.columns || 4}, 1fr)`, // 每行数量
'grid-column-gap': this.listStyle?.columnGap || '40rpx', // 行间距
'grid-row-gap': this.listStyle?.rowGap || '40rpx', // 列间距
'padding': this.listStyle?.padding || '0rpx' // 列表内边距
}
return style;
},
imgStyle() {
const style = {
'border-radius': '6rpx', // 图片圆角
}
return style;
}
},
methods: {
/**
* 删除已选择文件
* @param {object} item 文件信息
* @param {number} selectedFileIndex 文件索引
* */
deleteSelectedFile(item, selectedFileIndex) {
this.$emit('beofreDelete',selectedFileIndex,item.fileType)
},
/**
* 点击已选择文件
* @param {object} item 文件信息
* @param {number} index 文件索引
* */
clickSelectedFile(item, index) {
if(this.previewImage(item, index) === 'video' ){
this.previewVideo(item,index)
}
},
/**
* 点击选择图片按钮
* */
selectFileTypeOnAdd() {
switch (this.fileType) {
case 'image':
this.handleFileSelection(1);
break;
case 'video':
this.handleFileSelection(2);
break;
case 'all':
uni.showActionSheet({
itemList: ['图片', '视频'],
success: (res) => {
const tapIndex = res.tapIndex || res;
console.log(res,'选择');
console.log(typeof res.tapIndex,'选择框类型');
if (tapIndex == 1) {
this.handleFileSelection(2);
} else {
this.handleFileSelection(1);
}
},
fail: (res) => {
console.error(res.errMsg);
}
});
break;
default:
this.handleFileSelection(1);
break;
}
},
/**
* 从本地选择文件。
* @param { number } updataType 选择类型 1:图片 2视频
* */
async handleFileSelection(updataType) {
const that = this;
if (updataType === 1) {
const data = Object.assign({}, {
// 最多可以选择的图片张数,默认9
count: 1,
// 仅对 mediaType 为 image 时有效,是否压缩所选文件
// album 从相册选图,camera 使用相机,默认二者都有。
sourceType: ['camera', 'album'],
compress: false
}, this.imageFormData)
data['count'] = this.max - this.FileList.length
uni.chooseImage({
...data,count:1,compress: false,
success: async (res) => {
res.tempFile = res.tempFiles[0];
res.tempFile.fileType = 'image'
this.$emit('change',res)
},
fail(err) {
console.error('选择图片失败', err)
that.$emit('onError', err)
}
})
}
if (updataType === 2) {
const data = Object.assign({}, {
// 拍摄视频最长拍摄时间,单位秒。最长支持 60 秒。
maxDuration: 60,
// album 从相册选视频,camera 使用相机拍摄,默认二者都有。
sourceType: ['camera', 'album'],
// 是否压缩所选的视频源文件,默认值为 true,需要压缩。
compressed: false,
count: 1,
// 'front'、'back',默认'back'
}, this.videoFromData)
uni.chooseVideo({
...data,count:1,compressed: false,
success: (res) => {
let tempFilePath = { ...res }
tempFilePath['path'] = res.tempFilePath
tempFilePath.type = tempFilePath.type || tempFilePath?.tempFile?.type
res.tempFile.fileType = 'video'
this.$emit('change',res)
},
fail(err) {
console.error('选择视频失败', err)
}
})
}
},
/**
* 压缩图片
* @param {array} tempFilePaths 临时路径数组
* @return {array} 被压缩过的路径数组
* */
async compressImage(tempFilePaths) {
const that = this;
return new Promise((resolve, reject) => {
if (typeof tempFilePaths !== 'string') {
console.error('压缩路径错误')
reject([])
}
uni.showLoading({
title: '压缩中...',
icon: 'loading',
})
// #ifdef H5
this.canvasDataURL(tempFilePaths, {
quality: that.imageFormData.quality / 100
}, (base64Codes) => {
resolve(base64Codes);
uni.hideLoading();
})
// #endif
// #ifndef H5
uni.compressImage({
src: tempFilePaths,
quality: that.imageFormData.quality || 80,
success: res => {
resolve(res.tempFilePath);
uni.hideLoading();
},
fail(err) {
reject(err);
uni.hideLoading();
}
})
// #endif
})
},
/**
* H5压缩图片质量
* @param {string} path 图片路径
* @param {object} obj 压缩配置
* @param {function} callback 回调函数
* @return {string} base64
* */
canvasDataURL(path, obj, callback) {
var img = new Image();
img.src = path;
img.onload = function () {
var that = this;
// 默认按比例压缩
var w = that.width,
h = that.height,
scale = w / h;
w = obj.width || w;
h = obj.height || (w / scale);
var quality = 0.8; // 默认图片质量为0.8
//生成canvas
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
// 创建属性节点
var anw = document.createAttribute("width");
anw.nodeValue = w;
var anh = document.createAttribute("height");
anh.nodeValue = h;
canvas.setAttributeNode(anw);
canvas.setAttributeNode(anh);
ctx.drawImage(that, 0, 0, w, h);
// 图像质量
if (obj.quality && obj.quality <= 1 && obj.quality > 0) {
quality = obj.quality;
}
// quality值越小,所绘制出的图像越模糊
var base64 = canvas.toDataURL('image/jpeg', quality);
// 回调函数返回base64的值
callback(base64);
}
},
/**
* 预览图片
* @param {string, object} item 文件信息
* */
previewImage(item) {
if (item.fileType === 'video') return 'video';
if (!this.isPreviewImage) return false;
const imgs = this.FileList.filter(item => {
return item.fileType !== 'video'
}).map(item => ( this.setFileUrl(item.watermarkUrl) || this.setFileUrl(item.url) ) )
const current = imgs.indexOf(this.setFileUrl(item.watermarkUrl) || this.setFileUrl(item.url) );
uni.previewImage({
current: current,
urls: imgs,
success() {
},
fail(err) {
console.log(err);
}
})
},
/**
* 预览视频
* @param {string, object} item 文件信息
* @param {number} index 索引
* */
previewVideo(item, index) {
this.$emit('onVideo', {
item,
index
})
this.tempVideoUrl = this.setFileUrl(item.watermarkUrl) || this.setFileUrl(item.url);
},
}
}
</script>
<style lang="scss" scoped>
.cl-updata {
.file-list {
display: flex;
flex-wrap: wrap;
.file-list-item{
width: 100px;
height: 100px;
position: relative;
.play-img {
width: 100%;
}
._image {
height: 100%;
width: 100%;
border-radius: 12rpx;
}
._video {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.video-fixed {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
border-radius: 10rpx;
z-index: 96;
}
.play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 95;
}
.app_play {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50rpx;
height: 50rpx;
}
.remove {
position: absolute;
top: 0;
right: 0;
background-color: #373737;
height: 30px;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
z-index: 97;
.image {
// width: 20rpx;
// height: 20rpx;
}
}
.add-image {
display: flex;
align-items: center;
justify-content: center;
// border: 2rpx dashed #ccc;
width: 100%;
height: 100%;
border-radius: 5rpx;
&:active {
opacity: 0.8;
}
._image {
width: 40%;
}
}
}
}
}
</style>