前端实现图片预览
最近有个移动端图片预览的需求,本来是不想自己再去手错的,就去翻了下 Vant 4 - 轻量、可定制的移动端组件库 (gitee.io) 的文档,还真的有图片预览组件,想着美滋滋直接用,直接开干。可是,不出意外的话要出意外了,滑动的效果不太符合产品想要的效果,就想着换个组件库吧,然后就去找了 面向 Vue3 的 Material 风格移动端组件库 (gitee.io) 组件库,试了下,虽然效果好多了,但是图片会闪烁,emmmmm,被迫给varlet
提了一个issue。emmmm,最后放弃了组件库,选择自己借助 Swiper中文网-轮播图幻灯片js插件,H5页面前端开发 实现图片预览,demo代码地址是 chouchouji/image_preview: image preview demo written by vue3 & ts (github.com) ,效果如下(也不知道啥时候能支持视频导入):
vant
很好奇为什么vant的滑动体验很流畅,就去看了下源码,我们先打开浏览器看看滑动的时候发生了什么
滑动的时候,transition-duration: 0ms
,滑动结束之后,transition-duration: 300ms
(这里贴一段源码截图,这个其实用的是 swipe
组件)
varlet组件库也是如此
根据偏移的距离、速度和方向判断是停留在当前页,还是去上一页或者下一页
实现思路
页码
产品想要的效果是页码在上面,但是直接添加的话,页码是在下面的
所以我现在需要把这个移动到上面去。翻了一下文档,发现有一个 el
参数,好像可以自主调整页码的样式
代码修改如下
效果不错,成功调整了位置和颜色
传参
一些控制初始位置和间距的参数就不介绍了,感兴趣的掘友可以去看文档。对于开启或者关闭预览,我是想外层使用的组件只需要传入一个参数就行,根据这个参数的值来控制,这里我参考了 vant
组件库对变量的控制,选择使用 v-model
,不太了解的掘友可以点击这里 组件 v-model | Vue.js (vuejs.org)
代码如下:
ImageList.vue
<ImagePreview
v-model:show-image="showImagePreview"
:image-list="IMAGE_LIST"
:start-index="curIndex"
></ImagePreview>
ImagePreview.vue
const props = defineProps({
showImage: Boolean,
})
const showImage = toRef(props, 'showImage')
const close = () => {
emits('update:showImage', false)
}
事件定义
这里我实现了图片预览滑动结束,原来的图片位置会根据滑动之后的位置重新调整,比如,当前没开启之前,我看的是第一张图片,开启之后,滑到了第三张,点击关闭,图片位子也跟着调整到第三张比较友好一些。
这里我选添加一个 close
事件,使用的时候可以自定义一个函数来处理关闭事件
代码如下:
ImagePreview.vue
const close = () => {
emits('update:showImage', false)
emits('close', activeIndex.value)
}
ImageList.vue
function close(activeIndex: number) {
const element = document.querySelector('.images') as HTMLElement
element.scrollLeft = activeIndex * IMG_WIDTH
}
禁止页面滚动
有时候我们的页面比较长,是能滚动的,如果我们在预览的时候滚动了页面的话,关闭预览页面的话,返回的就不是原来的位置了,所以我们开启预览时禁止滚动,关闭时开启(这里看个人需求,如果你的页面本来就不滚动,那就没必要了)
这里我选择的是监听传入的控制开启关闭预览的变量,代码如下:
// 预览开启禁止滚动,关闭允许滚动
watch(showImage, (cur: boolean, _pre: boolean) => {
if (cur) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'scroll'
}
})
代码
index.ts
// 引入本地图片
export function getImageUrl(url: string) {
return new URL(url, import.meta.url).href
}
ImageList.vue
<script setup lang="ts">
import { ref } from 'vue'
import ImagePreview from '@/common/ImagePreview.vue'
import { getImageUrl } from '@/utils'
const IMG_WIDTH = document.documentElement.clientWidth * 0.6
const IMAGE_LIST = Array.from(Array(3), (_item, index: number) => {
return getImageUrl(`../assets/cats/${index + 1}.jpg`)
})
const curIndex = ref(0)
const showImagePreview = ref(false)
const selectImage = (index: number) => {
curIndex.value = index
showImagePreview.value = true
}
function close(activeIndex: number) {
const element = document.querySelector('.images') as HTMLElement
element.scrollLeft = activeIndex * IMG_WIDTH
}
</script>
<template>
<div class="container">
<div class="images">
<div class="image_box" v-for="(imgUrl, index) in IMAGE_LIST">
<img class="image" :src="imgUrl" alt="image" @click="selectImage(index)" />
</div>
</div>
</div>
<ImagePreview
v-model:show-image="showImagePreview"
:image-list="IMAGE_LIST"
:start-index="curIndex"
@close="close"
></ImagePreview>
</template>
<style scoped>
.container {
width: 100%;
height: 100vh;
overflow-y: hidden;
display: flex;
align-items: center;
}
.images {
width: 100%;
height: 60vh;
overflow-x: scroll;
overflow-y: hidden;
display: flex;
flex-direction: row;
}
.image_box {
margin: 0 10px;
min-width: 60%;
}
.image {
width: 100%;
height: 100%;
object-fit: cover;
border: 1px solid #efefef;
}
</style>
ImageList.vue
<script setup lang="ts">
import { ref, toRef, defineEmits, watch } from 'vue'
import { Pagination } from 'swiper'
import { Swiper, SwiperSlide } from 'swiper/vue'
import 'swiper/swiper-bundle.css'
const modules = [Pagination]
const props = defineProps({
showImage: Boolean,
imageList: {
type: Array<string>,
required: true
},
startIndex: {
type: Number
}
})
const activeIndex = ref(0)
const showImage = toRef(props, 'showImage')
const emits = defineEmits(['update:showImage', 'close'])
// 预览开启禁止滚动,关闭允许滚动
watch(showImage, (cur: boolean, _pre: boolean) => {
if (cur) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'scroll'
}
})
const close = () => {
emits('update:showImage', false)
emits('close', activeIndex.value)
}
const slideChange = (swiper: any) => {
activeIndex.value = swiper?.activeIndex
}
</script>
<template>
<div class="swiper" v-if="showImage" @click="close">
<swiper
:spaceBetween="10"
:pagination="{
el: '.custom_pagination',
type: 'fraction',
clickable: false
}"
:initialSlide="startIndex"
:modules="modules"
@slideChange="slideChange"
>
<!-- 页码 -->
<div class="custom_pagination"></div>
<swiper-slide v-for="imageUrl in imageList">
<img :src="imageUrl" alt="image" />
</swiper-slide>
</swiper>
</div>
</template>
<style scoped>
.swiper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
z-index: 2000;
background-color: black;
}
.swiper-slide {
display: flex;
justify-content: center;
}
.swiper-slide img {
width: 100%;
max-height: 70vh;
object-fit: contain;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.custom_pagination {
position: absolute;
width: 60px;
height: 20px;
top: 10px;
left: 50%;
text-align: center;
line-height: 20px;
transform: translateX(-50%);
color: white;
}
</style>
版本
结尾
如有错误,欢迎指正