场景
在一些场景,uniapp 的原生图片预览无法满足要求,比如自定义图片预览元素。另外原生的图片预览无法阻止截屏问题。
示例
html
<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>
源码
当前实现有几个缺点:
- 拖拽图片不流畅,movable-area 与 swiper 会有冲突
- 长图放大后可能无法拖拽查看不全
暂时没有优化,如果不需要放大后能够切换 swiper,可是使用下面的 简化的源码
,相对来说要流畅一些
html
<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 = true
this.urls = urls || []
// 初始化每张图片的状态
this.imageStates = this.urls.map(() => ({
scale: 1,
translateX: 0,
translateY: 0
}))
const maxIndex = this.urls.length - 1
if (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 = i
break
}
} else {
if (item === indexOrUrl) {
index = i
break
}
}
}
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 = false
this.$emit('close')
},
onSwiperChange(e) {
this.currentIndex = e.detail.current
if (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 = 1
this.minScale = 1
this.imageStates[index].scale = 1
this.imageStates[index].translateX = 0
this.imageStates[index].translateY = 0
this.$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.x
this.imageStates[index].translateY = e.detail.y
const systemInfo = uni.getSystemInfoSync()
const scale = this.imageStates[index].scale
const winWidth = systemInfo.windowWidth
const scleWidth = (winWidth - 1) * scale
if (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.scale
if (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>
简化的源码
注意,当前实现图片放大后无法切换 swiper
html
<script>
export default {
name: 'PreviewImgPopup',
props: {
// 图片字段,当传入urls为对象数组时需要指定
imgField: {
type: String
},
// 是否显示右上角的菜单
showMenu: {
type: Boolean,
default: false
}
},
data() {
return {
urls: [],
currentIndex: 0,
showVal: false,
curScale: 1,
minScale: 1,
maxScale: 3,
scaleValue: 1,
// swiper控制,用于拖拽图片时禁用swiper
swiperDisabled: false,
safeAreaInsetBottom: 0,
safeAreaInsetTop: 0,
systemInfo: {},
// 是否swiper移动中,用于滑动swiper时禁用图片拖拽
swiperMoving: false
}
},
computed: {
// 右上角的数字显示
preNum() {
return `${this.currentIndex + 1}/${this.urls.length}`
},
},
methods: {
open(urls, indexOrUrl = 0) {
// 初始化数据
this.currentIndex = 0
this.curScale = 1
this.minScale = 1
this.maxScale = 3
this.scaleValue = 1
this.swiperDisabled = false
this.swiperMoving = false
this.showVal = true
this.urls = urls || []
const maxIndex = this.urls.length - 1
if (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 = i
break
}
} else {
if (item === indexOrUrl) {
index = i
break
}
}
}
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 = false
this.$emit('close')
},
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)
}
})
})
},
onMovableScale(e) {
const scale = this.curScale = e.detail.scale
if (scale > 1) {
this.swiperDisabled = true
} else {
this.swiperDisabled = false
}
},
onSwiperTran() {
this.swiperMoving = true
},
onSwiperitemEnd() {
this.swiperMoving = false
},
},
mounted() {
uni.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: '#000'
})
const systemInfo = this.systemInfo = uni.getSystemInfoSync()
this.safeAreaInsetTop = systemInfo.safeArea.top;
}
}
</script>
<template>
<u-popup :show="showVal" mode="center" @close="onClose" bgColor="#000" :safeAreaInsetBottom="false"
closeOnClickOverlay :safe-area-inset-top="false">
<view class="preview-img-container">
<!-- 顶部工具栏 -->
<slot name="top">
<view class="top-tools">
<view class="safeTop" :style="{height: safeAreaInsetTop + 'px'}"></view>
<view class="top-tools-content">
<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>
</view>
</slot>
<!-- 图片轮播区域 -->
<swiper class="img-swiper" :current="currentIndex" :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"
@transition="onSwiperTran"
@animationfinish="onSwiperitemEnd">
<swiper-item v-for="(img, index) in urls" :key="index" class="swiper-item">
<movable-area class="movable-area" :scale-area="false">
<movable-view class="movable-view" :scale="true" :direction="swiperMoving ? 'none' : 'all'"
:scale-min="minScale" :scale-max="maxScale"
:inertia="true" :out-of-bounds="false" :damping="100" :scale-value="scaleValue"
@scale="onMovableScale" @tap="onImageTap">
<scroll-view v-show="!swiperDisabled && curScale === 1" :scroll-y="true"
class="scroll-view" :scroll-x="false">
<view class="image-content">
<image :src="getImgUrl(img)" class="preview-img" mode="widthFix"/>
</view>
</scroll-view>
<view v-show="!(!swiperDisabled && curScale === 1)" class="image-content">
<image :src="getImgUrl(img)" class="preview-img" mode="widthFix"/>
</view>
</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: 40rpx;
box-sizing: border-box;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 70%, transparent 100%);
.top-tools-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.pre-num {
color: #fff;
font-size: 16px;
font-weight: 500;
}
}
.img-swiper {
width: 100vw;
height: 100vh;
.swiper-item {
width: 100vw;
height: 100vh;
}
.movable-area {
width: 100vw;
height: 100vh;
}
.movable-view {
width: 100vw;
height: auto;
}
.scroll-view {
height: 100vh;
}
.image-content {
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
.preview-img {
width: 100vw;
}
}
}
.safeBottom, .safeTop {
width: 100%;
background-color: transparent;
}
</style>