likes
comments
collection
share

前端实现图片预览

作者站长头像
站长
· 阅读数 7

最近有个移动端图片预览的需求,本来是不想自己再去手错的,就去翻了下 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>

版本

前端实现图片预览

结尾

如有错误,欢迎指正