Uniapp之自定义图片预览
场景
在一些场景,uniapp 的原生图片预览无法满足要求,比如自定义图片预览元素。另外原生的图片预览无法阻止截屏问题。
示例
<template><preview-img-popup ref="preImgPopup" img-field="imageUrl" @menu="onPreImgMenu"></preview-img-popup>
</template>
<script>
export default {data() {return {allDownload: false}},onBackPress() {// 返回时判断是否关闭图片预览,否则关闭并阻止返回if (this.$refs.preImgPopup.isOpen()) {this.$refs.preImgPopup.close();return true;}return false;},methods: {previewImage(url, dataList) {this.$refs.preImgPopup.open(dataList, url)},onPreImgMenu(data) {const _this = this;const allDownload = this.allDownload;uni.showActionSheet({itemList: [allDownload ? '保存图片' : '作者禁止下载'],itemColor: allDownload ? '#000' : 'rgb(243,140,140)',success: (res) => {if (!allDownload) {uni.showToast({title: '作者禁止下载',icon: "none",})return}if (res.tapIndex === 0) {_this.$refs.preImgPopup.save(data.imageUrl).then((res) => {console.log(res)uni.showToast({title: '图片已保存到相册',icon: "none",})}).catch((err) => {console.error('图片保存失败', err);})}}})}}
}
</script>
源码
<script>
export default {name: 'PreviewImgPopup',props: {// 图片字段,当传入urls为对象数组时需要指定imgField: {type: String},// 是否显示右上角的菜单showMenu: {type: Boolean,default: false}},data() {return {urls: [],currentIndex: 0,showVal: false,// 每张图片独立的缩放状态imageStates: [],minScale: 1,maxScale: 3,// swiper控制swiperDisabled: false,// movable-view的尺寸movableWidth: 100,movableHeight: 100,scaleValue: 1}},computed: {preNum() {return `${this.currentIndex + 1}/${this.urls.length}`},// 当前图片的状态currentImageState() {return this.imageStates[this.currentIndex] || {scale: 1, translateX: 0, translateY: 0}},// 当前图片的缩放比例scale() {return this.currentImageState.scale},// 当前图片的X位移translateX() {return this.currentImageState.translateX},// 当前图片的Y位移translateY() {return this.currentImageState.translateY}},methods: {open(urls, indexOrUrl = 0) {this.showVal = truethis.urls = urls || []// 初始化每张图片的状态this.imageStates = this.urls.map(() => ({scale: 1,translateX: 0,translateY: 0}))const maxIndex = this.urls.length - 1if (typeof indexOrUrl === 'string') {// 如果是字符串,则根据图片地址查找let index = -1;for (let i = 0; i < this.urls.length; i++) {const item = this.urls[i]if (this.imgField) {if (item[this.imgField] === indexOrUrl) {index = ibreak}} else {if (item === indexOrUrl) {index = ibreak}}}if (index !== -1) {this.currentIndex = index} else {this.currentIndex = 0}} else {this.currentIndex = indexOrUrl > maxIndex ? maxIndex : indexOrUrl}},isOpen() {return this.showVal},close() {this.showVal = false},onClose() {this.showVal = falsethis.$emit('close')},onSwiperChange(e) {this.currentIndex = e.detail.currentif (this.imageStates[this.currentIndex].scale > 1) {this.swiperDisabled = true}},onImageTap() {this.close()},getImgUrl(data) {if (this.imgField) {return data[this.imgField]}return data},handleMenuClick() {this.$emit('menu', this.urls[this.currentIndex])},/*** 保存图片* @param url 图片地址* @return {Promise<string>}*/save(url) {return new Promise((resolve, reject) => {uni.downloadFile({url: url,success(res) {if (res.tempFilePath) {uni.saveImageToPhotosAlbum({filePath: res.tempFilePath,success(re) {resolve(re.path)},fail(err) {reject(err)}})}},fail(err) {reject(err)}})})},// 重置指定图片的缩放状态resetScale(index = this.currentIndex) {if (this.imageStates[index]) {this.scaleValue = 1this.minScale = 1this.imageStates[index].scale = 1this.imageStates[index].translateX = 0this.imageStates[index].translateY = 0this.$nextTick(() => {this.minScale = 0.6})}},// movable-view事件处理onMovableChange(e, index) {if (index === this.currentIndex && this.imageStates[index]) {const x = this.imageStates[index].translateX = e.detail.xthis.imageStates[index].translateY = e.detail.yconst systemInfo = uni.getSystemInfoSync()const scale = this.imageStates[index].scaleconst winWidth = systemInfo.windowWidthconst scleWidth = (winWidth - 1) * scaleif (scale > 1) {if (x < 0) {if (winWidth - x >= scleWidth) {this.swiperDisabled = false} else {this.swiperDisabled = true}} else if (x >= 0) {this.swiperDisabled = false} else {this.swiperDisabled = true}}}},onMovableScale(e, index) {if (index === this.currentIndex && this.imageStates[index]) {const scale = this.imageStates[index].scale = e.detail.scaleif (scale > 1) {this.swiperDisabled = true} else {this.swiperDisabled = false}// 缩放时的swiper控制:只有在缩放<=1时才允许swiper// 放大时的精确边界检测由onMovableChange处理if (e.detail.scale <= 1) {this.swiperDisabled = false}// 注意:不在这里设置 swiperDisabled = true,让onMovableChange来精确控制}},onTouchEnd(e, index) {return;// uni.$u.throttle(() => {// if (index === this.currentIndex && this.imageStates[index]) {// const scale = this.imageStates[index].scale// console.log('scale', scale)//// // 如果缩放小于1,自动恢复到默认大小// if (scale < 1) {// setTimeout(() => {// this.resetScale(index)// }, 100)// }// }// }, 500)}},mounted() {uni.setNavigationBarColor({frontColor: '#ffffff',backgroundColor: '#000'})}
}
</script><template><u-popup :show="showVal" mode="center" @close="onClose" bgColor="#000" :safeAreaInsetBottom="false"closeOnClickOverlay safe-area-inset-top><view class="preview-img-container"><!-- 顶部工具栏 --><slot name="top"><view class="top-tools"><view class="pre-num">{{ preNum }}</view><view class="menu"><u-icon name="more-dot-fill" color="#fff" size="20px" @click="handleMenuClick"></u-icon></view></view></slot><!-- 图片轮播区域 --><swiper class="img-swiper" :current="currentIndex" @change="onSwiperChange" :indicator-dots="false"indicator-active-color="#fff" indicator-color="rgba(255, 255, 255, .3)" :autoplay="false":circular="false":disable-touch="swiperDisabled" :disable-programmatic-animation="swiperDisabled"><swiper-item v-for="(img, index) in urls" :key="index" class="swiper-item"><movable-area class="movable-area" :scale-area="true"><movable-view class="movable-view" :scale="true" direction="all" :scale-min="minScale" :scale-max="maxScale":inertia="true" :out-of-bounds="false" :damping="100" :scale-value="scaleValue":x="imageStates[index] ? imageStates[index].translateX : 0":y="imageStates[index] ? imageStates[index].translateY : 0"@change="(e) => onMovableChange(e, index)"@scale="(e) => onMovableScale(e, index)" @tap="onImageTap"@touchend="(e) => onTouchEnd(e, index)"><image :src="getImgUrl(img)" class="preview-img" mode="aspectFit"/></movable-view></movable-area></swiper-item></swiper><slot name="bottom"></slot></view></u-popup>
</template><style scoped lang="scss">
.preview-img-container {width: 100vw;height: 100vh;position: relative;display: flex;flex-direction: column;
}.top-tools {position: absolute;top: 0;left: 0;right: 0;z-index: 10;padding: 60rpx 40rpx 40rpx;box-sizing: border-box;display: flex;justify-content: space-between;align-items: center;background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 70%, transparent 100%);.pre-num {color: #fff;font-size: 16px;font-weight: 500;}
}.img-swiper {width: 100%;height: 100%;.swiper-item {width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;}.movable-area {width: 100%;height: 100%;}.movable-view {width: 100%;height: 100%;}.preview-img {width: 100%;height: 100%;max-width: 100%;max-height: 100%;}
}
</style>