431 lines
13 KiB
Vue
431 lines
13 KiB
Vue
<template>
|
||
<view class="preview-container">
|
||
<!-- 右上角按钮组 -->
|
||
<view class="btn-group">
|
||
<!-- 缩小按钮 -->
|
||
<!-- <view class="btn zoom-out-btn" @click="zoomOut">
|
||
<text class="btn-icon">−</text>
|
||
</view> -->
|
||
|
||
<!-- 放大按钮 -->
|
||
<!-- <view class="btn zoom-in-btn" @click="zoomIn">
|
||
<text class="btn-icon">+</text>
|
||
</view> -->
|
||
|
||
<!-- 下载按钮 -->
|
||
<view class="btn download-btn" @click="downloadImage">
|
||
<text class="btn-icon">⬇</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 图片预览区域,使用movable-area和movable-view实现缩放移动 -->
|
||
<movable-area class="movable-area" :style="{ width: '100vw', height: '100vh' }">
|
||
<movable-view
|
||
class="movable-view"
|
||
direction="all"
|
||
:scale="true"
|
||
:scale-min="0.2"
|
||
:scale-max="0.5"
|
||
:scale-value="scaleValue"
|
||
|
||
@touchstart="onTouchStart"
|
||
@touchend="onTouchEnd"
|
||
:x="x"
|
||
:y="y"
|
||
:animation="true"
|
||
:animation-duration="300"
|
||
:style="{
|
||
width: rotatedWidth + 'px',
|
||
height: rotatedHeight + 'px',
|
||
}"
|
||
>
|
||
<view
|
||
class="image-wrapper"
|
||
:style="{
|
||
width: imgWidth + 'px',
|
||
height: imgHeight + 'px',
|
||
transform: 'rotate(90deg)',
|
||
transformOrigin: 'center center',
|
||
}"
|
||
>
|
||
<image
|
||
:src="imageUrl"
|
||
class="preview-img"
|
||
mode="aspectFill"
|
||
:style="{
|
||
width: imgWidth + 'px',
|
||
height: imgHeight + 'px',
|
||
}"
|
||
@load="onImageLoad"
|
||
></image>
|
||
</view>
|
||
</movable-view>
|
||
</movable-area>
|
||
|
||
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
onLoad(options) {
|
||
this.imageUrl = decodeURIComponent(options.url) // 接收传递的图片URL,需要解码
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
imageUrl: '',
|
||
// 缩放相关 - 默认0.5
|
||
scaleValue: 0.2,
|
||
x: 0,
|
||
y: 0,
|
||
// 图片原始尺寸
|
||
imgWidth: 0,
|
||
imgHeight: 0,
|
||
// 旋转后的尺寸(交换宽高)
|
||
rotatedWidth: 0,
|
||
rotatedHeight: 0,
|
||
// 提示显示控制
|
||
showScaleTip: false,
|
||
tipTimer: null,
|
||
// 屏幕尺寸
|
||
windowWidth: 0,
|
||
windowHeight: 0,
|
||
// 缩放步长
|
||
zoomStep: 0.1,
|
||
// 动画控制
|
||
isTouching: false,
|
||
animationTimer: null
|
||
}
|
||
},
|
||
|
||
mounted() {
|
||
// 获取屏幕尺寸
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
this.windowWidth = systemInfo.windowWidth
|
||
this.windowHeight = systemInfo.windowHeight
|
||
},
|
||
|
||
methods: {
|
||
// 图片加载完成后获取尺寸
|
||
onImageLoad(e) {
|
||
const { width, height } = e.detail
|
||
|
||
// 保存原始尺寸
|
||
this.imgWidth = width
|
||
this.imgHeight = height
|
||
|
||
// 旋转90度后,宽度和高度互换
|
||
this.rotatedWidth = height // 旋转后宽度 = 原高度
|
||
this.rotatedHeight = width // 旋转后高度 = 原宽度
|
||
|
||
// 计算初始位置居中(考虑0.5缩放)
|
||
this.resetPosition()
|
||
},
|
||
|
||
// 重置位置到中心(考虑当前缩放比例)
|
||
resetPosition() {
|
||
// 计算居中的偏移量,考虑缩放比例
|
||
const displayWidth = this.rotatedWidth * this.scaleValue
|
||
const displayHeight = this.rotatedHeight * this.scaleValue
|
||
|
||
this.x = (this.windowWidth - displayWidth) / 2
|
||
this.y = (this.windowHeight - displayHeight) / 2
|
||
},
|
||
|
||
// 触摸开始
|
||
onTouchStart() {
|
||
this.isTouching = true
|
||
},
|
||
|
||
// 触摸结束
|
||
onTouchEnd() {
|
||
this.isTouching = false
|
||
},
|
||
|
||
// 缩放事件处理
|
||
onScale(e) {
|
||
this.scaleValue = e.detail.scale
|
||
|
||
// 显示缩放提示
|
||
this.showScaleTip = true
|
||
if (this.tipTimer) {
|
||
clearTimeout(this.tipTimer)
|
||
}
|
||
this.tipTimer = setTimeout(() => {
|
||
this.showScaleTip = false
|
||
}, 1500)
|
||
},
|
||
|
||
// 放大
|
||
zoomIn() {
|
||
// 计算新的缩放值,不超过最大值
|
||
const newScale = Math.min(this.scaleValue + this.zoomStep, 0.5)
|
||
this.setScaleWithAnimation(newScale)
|
||
},
|
||
|
||
// 缩小
|
||
zoomOut() {
|
||
// 计算新的缩放值,不低于最小值
|
||
const newScale = Math.max(this.scaleValue - this.zoomStep, 0.2)
|
||
this.setScaleWithAnimation(newScale)
|
||
},
|
||
|
||
// 带动画的设置缩放值
|
||
setScaleWithAnimation(newScale) {
|
||
// 保存旧的缩放值用于动画
|
||
const oldScale = this.scaleValue
|
||
|
||
// 计算缩放中心点(屏幕中心)
|
||
const centerX = this.windowWidth / 2
|
||
const centerY = this.windowHeight / 2
|
||
|
||
// 计算当前图片中心点
|
||
const oldDisplayWidth = this.rotatedWidth * oldScale
|
||
const oldDisplayHeight = this.rotatedHeight * oldScale
|
||
const oldCenterX = this.x + oldDisplayWidth / 2
|
||
const oldCenterY = this.y + oldDisplayHeight / 2
|
||
|
||
// 计算偏移量,使缩放后图片中心保持在屏幕中心
|
||
const newDisplayWidth = this.rotatedWidth * newScale
|
||
const newDisplayHeight = this.rotatedHeight * newScale
|
||
const newX = centerX - newDisplayWidth / 2
|
||
const newY = centerY - newDisplayHeight / 2
|
||
|
||
// 更新缩放值和位置
|
||
this.scaleValue = newScale
|
||
this.x = newX
|
||
this.y = newY
|
||
|
||
// 显示缩放提示
|
||
this.showScaleTip = true
|
||
if (this.tipTimer) {
|
||
clearTimeout(this.tipTimer)
|
||
}
|
||
this.tipTimer = setTimeout(() => {
|
||
this.showScaleTip = false
|
||
}, 1500)
|
||
|
||
console.log('缩放:', {oldScale, newScale, x: this.x, y: this.y})
|
||
},
|
||
|
||
// 下载图片
|
||
downloadImage() {
|
||
uni.showLoading({
|
||
title: '下载中,请稍等...',
|
||
mask: true,
|
||
})
|
||
|
||
// 先获取图片信息(如果是网络图片需要先下载)
|
||
uni.downloadFile({
|
||
url: this.imageUrl,
|
||
success: (res) => {
|
||
if (res.statusCode === 200) {
|
||
// 保存图片到相册
|
||
uni.saveImageToPhotosAlbum({
|
||
filePath: res.tempFilePath,
|
||
success: () => {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '保存成功',
|
||
icon: 'success',
|
||
})
|
||
},
|
||
fail: (err) => {
|
||
uni.hideLoading()
|
||
console.error('保存失败', err)
|
||
|
||
// 处理用户拒绝权限的情况
|
||
if (err.errMsg.includes('auth deny')) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '需要您授权保存图片到相册',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.openSetting({
|
||
success: (settingRes) => {
|
||
console.log('打开设置页面', settingRes)
|
||
},
|
||
})
|
||
}
|
||
},
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: '保存失败',
|
||
icon: 'none',
|
||
})
|
||
}
|
||
},
|
||
})
|
||
} else {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '下载失败',
|
||
icon: 'none',
|
||
})
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
uni.hideLoading()
|
||
console.error('下载失败', err)
|
||
uni.showToast({
|
||
title: '下载失败',
|
||
icon: 'none',
|
||
})
|
||
},
|
||
})
|
||
},
|
||
},
|
||
|
||
// 页面返回前清理定时器
|
||
onUnload() {
|
||
if (this.tipTimer) {
|
||
clearTimeout(this.tipTimer)
|
||
}
|
||
if (this.animationTimer) {
|
||
cancelAnimationFrame(this.animationTimer)
|
||
}
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.preview-container {
|
||
position: relative;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: #000;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.movable-area {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: #000;
|
||
}
|
||
|
||
.movable-view {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
// 添加硬件加速
|
||
transform: translateZ(0);
|
||
will-change: transform;
|
||
}
|
||
|
||
.image-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
// 添加硬件加速
|
||
transform: translateZ(0) rotate(90deg);
|
||
will-change: transform;
|
||
backface-visibility: hidden;
|
||
-webkit-backface-visibility: hidden;
|
||
transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||
}
|
||
|
||
.preview-img {
|
||
display: block;
|
||
// 添加硬件加速
|
||
transform: translateZ(0);
|
||
will-change: transform;
|
||
backface-visibility: hidden;
|
||
-webkit-backface-visibility: hidden;
|
||
}
|
||
|
||
// 按钮组
|
||
.btn-group {
|
||
position: fixed;
|
||
top: 30rpx;
|
||
right: 30rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 20rpx;
|
||
z-index: 1000;
|
||
}
|
||
|
||
// 通用按钮样式
|
||
.btn {
|
||
width: 70rpx;
|
||
height: 70rpx;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
// 按钮不参与动画
|
||
transform: translateZ(0);
|
||
transition: background-color 0.2s ease;
|
||
|
||
&:active {
|
||
background: rgba(0, 0, 0, 0.8);
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.btn-icon {
|
||
color: #fff;
|
||
font-size: 40rpx;
|
||
line-height: 1;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
// 缩小按钮
|
||
.zoom-out-btn {
|
||
.btn-icon {
|
||
font-size: 50rpx;
|
||
}
|
||
}
|
||
|
||
// 放大按钮
|
||
.zoom-in-btn {
|
||
.btn-icon {
|
||
font-size: 40rpx;
|
||
}
|
||
}
|
||
|
||
// 下载按钮
|
||
.download-btn {
|
||
.btn-icon {
|
||
font-size: 35rpx;
|
||
}
|
||
}
|
||
|
||
// 缩放提示
|
||
.scale-tip {
|
||
position: fixed;
|
||
bottom: 100rpx;
|
||
left: 50%;
|
||
transform: translateX(-50%) translateZ(0);
|
||
background: rgba(0, 0, 0, 0.6);
|
||
color: #fff;
|
||
padding: 10rpx 30rpx;
|
||
border-radius: 40rpx;
|
||
font-size: 28rpx;
|
||
z-index: 1000;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
animation: fadeInOut 1.5s ease;
|
||
// 提示不参与动画
|
||
will-change: opacity;
|
||
}
|
||
|
||
@keyframes fadeInOut {
|
||
0% {
|
||
opacity: 0;
|
||
}
|
||
15% {
|
||
opacity: 1;
|
||
}
|
||
85% {
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
</style> |