Files
nl-im-uniapp/src/components/common/ImagePreview.vue
2025-12-15 11:06:42 +08:00

394 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view v-if="show" class="image-preview" @touchmove.stop.prevent>
<!-- 背景遮罩 -->
<view class="preview-backdrop" @click="handleClose" />
<!-- 主图区域 -->
<swiper
class="preview-swiper"
:current="currentIndex"
:indicator-dots="false"
@change="onSwiperChange"
>
<swiper-item v-for="(image, index) in images" :key="index" class="swiper-item">
<movable-area class="movable-area">
<movable-view
class="movable-view"
direction="all"
:scale="true"
:scale-min="1"
:scale-max="4"
:scale-value="scale"
@scale="onScale"
>
<image
class="preview-image"
:src="image"
mode="aspectFit"
@error="onImageError"
/>
</movable-view>
</movable-area>
</swiper-item>
</swiper>
<!-- 顶部工具栏 -->
<view class="top-bar">
<view class="close-btn" @click="handleClose">
<wd-icon name="close" size="40rpx" color="#fff" />
</view>
<view class="page-indicator">
<text>{{ currentIndex + 1 }} / {{ images.length }}</text>
</view>
<view class="actions">
<view class="action-btn" @click="saveImage">
<wd-icon name="download" size="36rpx" color="#fff" />
</view>
</view>
</view>
<!-- 底部缩略图 -->
<view v-if="images.length > 1" class="thumbnail-bar">
<scroll-view class="thumbnail-scroll" scroll-x>
<view
v-for="(image, index) in images"
:key="index"
class="thumbnail-item"
:class="{ active: index === currentIndex }"
@click="goToImage(index)"
>
<image :src="image" mode="aspectFill" class="thumbnail-image" />
</view>
</scroll-view>
</view>
<!-- 底部控制栏 -->
<view class="bottom-bar">
<!-- 缩放按钮 -->
<view class="zoom-controls">
<view class="zoom-btn" @click="zoomOut">
<wd-icon name="remove" size="36rpx" color="#fff" />
</view>
<text class="zoom-text">{{ Math.round(scale * 100) }}%</text>
<view class="zoom-btn" @click="zoomIn">
<wd-icon name="add" size="36rpx" color="#fff" />
</view>
</view>
<!-- 旋转按钮 -->
<view class="rotate-btn" @click="rotate">
<wd-icon name="refresh" size="36rpx" color="#fff" />
</view>
</view>
<wd-toast />
</view>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { useToast } from 'wot-design-uni'
const props = defineProps<{
show: boolean
images: string[]
initialIndex?: number
}>()
const emit = defineEmits<{
close: []
'update:show': [value: boolean]
}>()
const toast = useToast()
const currentIndex = ref(0)
const scale = ref(1)
const rotation = ref(0)
// 监听打开
watch(() => props.show, (val) => {
if (val) {
currentIndex.value = props.initialIndex || 0
scale.value = 1
rotation.value = 0
}
})
// swiper 切换
function onSwiperChange(e: any) {
currentIndex.value = e.detail.current
scale.value = 1 // 切换后重置缩放
}
// 跳转到指定图片
function goToImage(index: number) {
currentIndex.value = index
scale.value = 1
}
// 缩放回调
function onScale(e: any) {
scale.value = e.detail.scale
}
// 放大
function zoomIn() {
scale.value = Math.min(4, scale.value + 0.5)
}
// 缩小
function zoomOut() {
scale.value = Math.max(1, scale.value - 0.5)
}
// 旋转 (仅显示提示Uniapp 原生组件不支持旋转)
function rotate() {
toast.show('长按图片可保存')
}
// 保存图片
async function saveImage() {
const currentImage = props.images[currentIndex.value]
if (!currentImage) return
try {
// 先下载图片
const downloadResult = await new Promise<{ tempFilePath: string }>((resolve, reject) => {
uni.downloadFile({
url: currentImage,
success: (res) => {
if (res.statusCode === 200) {
resolve(res)
} else {
reject(new Error('下载失败'))
}
},
fail: reject
})
})
// 保存到相册
await new Promise<void>((resolve, reject) => {
uni.saveImageToPhotosAlbum({
filePath: downloadResult.tempFilePath,
success: () => resolve(),
fail: reject
})
})
toast.success('已保存到相册')
} catch (e: any) {
if (e?.errMsg?.includes('auth deny')) {
toast.error('请授权访问相册')
// 可以打开设置页面
uni.openSetting({})
} else {
toast.error('保存失败')
}
}
}
// 图片加载错误
function onImageError() {
toast.error('图片加载失败')
}
function handleClose() {
emit('close')
emit('update:show', false)
}
</script>
<style lang="scss" scoped>
.image-preview {
position: fixed;
inset: 0;
z-index: 10000;
background: #000;
}
.preview-backdrop {
position: absolute;
inset: 0;
}
.preview-swiper {
width: 100%;
height: 100%;
}
.swiper-item {
display: flex;
align-items: center;
justify-content: center;
}
.movable-area {
width: 100%;
height: 100%;
}
.movable-view {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-image {
width: 100%;
height: 100%;
}
.top-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 24rpx;
padding-right: 24rpx;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, transparent 100%);
z-index: 10;
/* #ifdef MP-WEIXIN */
height: calc(88rpx + var(--status-bar-height, 44px) + 88rpx);
padding-top: calc(var(--status-bar-height, 44px) + 88rpx);
/* #endif */
/* #ifndef MP-WEIXIN */
height: calc(88rpx + var(--status-bar-height, 0));
padding-top: var(--status-bar-height, 0);
/* #endif */
}
.close-btn,
.action-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
&:active {
background: rgba(255, 255, 255, 0.2);
}
}
.page-indicator {
font-size: 28rpx;
color: #fff;
font-weight: 500;
}
.actions {
display: flex;
gap: 16rpx;
}
.thumbnail-bar {
position: absolute;
bottom: 200rpx;
left: 0;
right: 0;
padding: 16rpx 0;
z-index: 10;
}
.thumbnail-scroll {
white-space: nowrap;
padding: 0 24rpx;
}
.thumbnail-item {
display: inline-block;
width: 100rpx;
height: 100rpx;
margin-right: 16rpx;
border-radius: 12rpx;
overflow: hidden;
border: 4rpx solid transparent;
opacity: 0.6;
transition: all 0.2s;
&.active {
border-color: #fff;
opacity: 1;
transform: scale(1.1);
}
.thumbnail-image {
width: 100%;
height: 100%;
}
}
.bottom-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: calc(120rpx + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
display: flex;
align-items: center;
justify-content: center;
gap: 60rpx;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.6) 0%, transparent 100%);
z-index: 10;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 24rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 40rpx;
padding: 8rpx 24rpx;
backdrop-filter: blur(8px);
}
.zoom-btn {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.7;
}
}
.zoom-text {
font-size: 24rpx;
color: #fff;
min-width: 80rpx;
text-align: center;
font-family: monospace;
}
.rotate-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
&:active {
background: rgba(255, 255, 255, 0.2);
}
}
</style>