导读: 最近在做前端工作项目中做了一个功能,要求在界面中可以预览显示图片,且支持常规的左右滑动,以及切题,同时在 H5 页面也要支持图片的缩放。
由于之前使用的 Swiper
都是属于低版本,按照官网的使用案例,并无不妥之处;但是当在 Vue3
环境下使用后,尤其是当 Swiper
版本更新到 v11.1.1
之后,会出现一些使用上的改变,故此动动手指,敲敲键盘,记录一下~
效果图
背景
其实如果只是想实现一个图片预览功能,完全可以手撸一个,亦或者找一些现成的三方库,比如 Element Plus
的 Image Viewer API
、以及 Vant UI
的 ImagePreview
组件。
不过由于定制化的效果和我们本期项目需求有些差异,就想着使用 Swiper
了。
Swiper 旧版使用
其中 Swiper
在 Vue2.x
中的写法,亦或者是 Vue3.x
但不是 setup
语法糖样式的写法,都是和 Swiper
官网上的提供的写法大同小异,而且核心的配置也都是通过 options
进行配置的,亦或者是通过创建的时候进行配置。
// Vue 2.x ---------
<template>
<swiper
ref="mySwiper"
class="swiper-wrapper"
:options="swiperOption"
@slide-change="slideChange"
>
<swiper-slide
v-for="(item, index) in imgUrls"
:key="index"
class="swiper-item swiper-zoom-container"
>
<img :src="item" alt="" />
</swiper-slide>
</swiper>
</template>
<script>
import { swiper, swiperSlide } from "vue-awesome-swiper";
export default {
name: "SwiperTest",
components: {
swiper,
swiperSlide,
},
props: {
imgUrls: {
type: Array,
default: () => [],
},
},
data() {
return {
swiperOption: {
pagination: {
el: ".swiper-pagination",
type: "fraction",
},
navigation: {
nextEl: ".icon-jiantouyou",
prevEl: ".icon-jiantouzuo",
disabledClass: "swiper-paperChange-disabled",
},
zoom: true,
},
curIndex: 0,
};
},
computed: {
swiper() {
return this.$refs.mySwiper && this.$refs.mySwiper.swiper;
},
slideIndex() {
return `${this.curIndex + 1} / ${this.imgUrls.length}`;
},
},
methods: {
sizeChange() {
setTimeout(() => {
this.swiper.update();
}, 0);
},
slideChange() {
this.curIndex = this.swiper.activeIndex;
},
},
};
</script>
Swiper 在 Vue3 的 setup 语法糖中的写法
如果是在 TypeScript
语法环境下 Vue3.x
的 setup
语法糖中的写法,如果通过 new Swiper()
创建的时候配置属性,会报 TypeScript class constructor without ‘new’ cannot be invoked
类似的错误,主要还是因为写法的变化引起的。
说明:
- 说明一下,此处示例中的
分页指示器
,并没有使用官方的Pagination Module
模块;- 主要是由于项目需求和官方指示器功能略有不同,而且项目中指示器显示的数字是根据服务端返回的,并非固定顺序,完全有可能是乱序的。
默认情况下,Swiper Vue
使用 Swiper
的核心版本(没有任何附加模块),如果要使用 Navigation、Pagination、Zoom
等模块,必须先安装它们。
以下列举 Swiper 相关扩展模块
:
模块名称 | 模块描述 |
---|---|
Virtual | Virtual Slides module,用于创建虚拟幻灯片。 |
Keyboard | Keyboard Control module,通过键盘控制幻灯片的切换。 |
Mousewheel | Mousewheel Control module,通过鼠标滚轮控制幻灯片的切换。 |
Navigation | Navigation module,提供导航按钮以控制幻灯片的切换。 |
Pagination | Pagination module,提供分页器以控制幻灯片的切换。 |
Scrollbar | Scrollbar module,提供滚动条以控制幻灯片的滚动。 |
Parallax | Parallax module,创建具有透视效果的幻灯片背景。 |
FreeMode | Free Mode module,允许自由拖动和缩放幻灯片。 |
Grid | Grid module,将幻灯片组织成网格布局。 |
Manipulation | Slides manipulation module,提供对幻灯片的各种操作和调整功能。 |
Zoom | Zoom module,用于实现缩放功能 |
Controller | Controller module,用于控制视频播放、暂停等操作 |
A11y | Accessibility module,提高应用程序的可访问性,使残障人士也能方便地使用应用程序 |
History | History Navigation module,用于实现历史记录的导航功能 |
HashNavigation | Hash Navigation module,通过 URL 的哈希部分实现页面的导航功能 |
Autoplay | Autoplay module,用于实现自动播放功能 |
EffectFade | Fade Effect module,实现淡入淡出效果 |
EffectCube | Cube Effect module,实现立方体旋转效果 |
EffectFlip | Flip Effect module,实现翻页效果 |
EffectCoverflow | Cover Effect module,实现覆盖层效果 |
EffectCards | Cards Effect module |
EffectCreative | Creative Effect module |
Thumbs | Thumbs module |
// Vue3.x 的 setup 语法糖中的写法 ---------
<script setup lang="ts">
import { useRoute } from "vue-router";
import { useShareStore } from "@share/stores/modules/share";
import { Ref, computed, onMounted, ref, watch } from "vue";
import { Swiper, SwiperSlide } from "swiper/vue";
import type { Swiper as SwiperObj } from "swiper/types";
import { Zoom } from "swiper/modules";
import "swiper/css";
import "swiper/css/zoom";
import { isWeixinBrowser } from "@share/utils/device";
import switchPrevGrayIcon from "@share/assets/icons/icon_arrow_left_gray.png";
import switchPrevLightGrayIcon from "@share/assets/icons/icon_arrow_left_light_gray.png";
import switchNextGrayIcon from "@share/assets/icons/icon_arrow_right_gray.png";
import switchNextLightGrayIcon from "@share/assets/icons/icon_arrow_right_light_gray.png";
const route = useRoute();
const shareStore = useShareStore();
const isLoading = ref(false);
// eslint-disable-next-line no-undef
const imgList: Ref<ShareTopicQuestionInfo[]> = ref([]);
const pageNum = 5;
const curPage = ref(0);
const curIndex = ref(0);
const swiperRef = ref<SwiperObj>();
const isWeixin = ref(false);
const curPageList = computed(() => {
const list: number[] = [];
let count = 0;
while (
curPage.value * pageNum + count < imgList.value.length &&
count < pageNum
) {
count++;
list.push(curPage.value * pageNum + count);
}
return list;
});
const isLastPage = computed(() => {
return (curPage.value + 1) * pageNum >= imgList.value.length;
});
onMounted(() => {
isWeixin.value = isWeixinBrowser();
isLoading.value = true;
shareStore.getShareDetail(route.query.shareId as string);
});
watch(
() => shareStore.shareTopic,
() => {
document.title = shareStore.shareTopic.title;
imgList.value = shareStore.shareTopic.questionInfos;
isLoading.value = false;
if (swiperRef.value && swiperRef.value.zoom) {
swiperRef.value.zoom.enabled = true;
}
}
);
const getSwitchIcon = (isNext: boolean) => {
if (isNext) {
return isLastPage.value ? switchNextLightGrayIcon : switchNextGrayIcon;
} else {
return curPage.value === 0 ? switchPrevLightGrayIcon : switchPrevGrayIcon;
}
};
const onSwiper = (swiper: SwiperObj) => {
swiperRef.value = swiper;
};
const onSlideChangeEvent = (swiper: SwiperObj) => {
curIndex.value = swiper.activeIndex;
curPage.value = Math.floor(curIndex.value / pageNum);
// 修正由于 swiper-wrapper 的 transform 属性引起的滚动偏移问题
const swiperEl = document.querySelector(".swiper-wrapper") as HTMLElement;
if (swiperEl) {
const rect = swiperEl.getBoundingClientRect();
swiperEl.style.transform = `translate3d(-${
rect.width * curIndex.value
}px, 0px, 0px)`;
}
};
const clickTopicEvent = (index: number) => {
curIndex.value = index - 1;
curPage.value = Math.floor(curIndex.value / pageNum);
swiperRef.value?.slideTo(curIndex.value);
};
const clickPrevEvent = () => {
if (curPage.value === 0) return;
curPage.value--;
};
const clickNextEvent = () => {
if (isLastPage.value) return;
curPage.value++;
};
const clickCloseEvent = () => {
window.close();
};
</script>
<template>
<div class="share-topic-wrapper">
<template v-if="isLoading">
<div
class="loading-wrapper"
v-loading="isLoading"
element-loading-text="精彩内容马上呈现"
element-loading-background="rgba(1, 1, 1, 0.01)"
>
<img
class="loading-background"
src="@share/assets/images/image_placeholder_loading.png"
alt=""
/>
<img
class="loading-logo-zhc"
src="@share/assets/images/image_logo_zhc.png"
alt=""
/>
<img
class="loading-logo-iflyspark"
src="@share/assets/images/image_logo_iflyspark.png"
alt=""
/>
</div>
</template>
<template v-else>
<div v-if="!isWeixin" class="navigation-wrapper">
<div class="navigation-close" @click.stop="clickCloseEvent">
<img src="@share/assets/icons/icon_close_black.png" alt="" />
</div>
<span class="navigation-title"></span>
<div class="navigation-right"></div>
</div>
<div class="pagination-wrapper">
<div
v-show="imgList.length > pageNum"
class="prev-button button switch-button"
@click.stop="clickPrevEvent"
>
<img :src="getSwitchIcon(false)" alt="" />
</div>
<div
class="list-button-wrapper"
:class="{ notFull: curPageList.length < pageNum }"
>
<template v-for="item in curPageList" :key="item">
<div
class="item-button button"
:class="{ active: curIndex + 1 === item }"
@click.stop="clickTopicEvent(item)"
>
</div>
</template>
</div>
<div
v-show="imgList.length > pageNum"
class="next-button button switch-button"
@click.stop="clickNextEvent"
>
<img :src="getSwitchIcon(true)" alt="" />
</div>
</div>
<div class="content-wrapper">
<swiper
class="img-list swiper-container"
:modules="[Zoom]"
:zoom="true"
:maxRatio="5"
@swiper="onSwiper"
@slideChange="onSlideChangeEvent"
>
<swiper-slide
v-for="(item, index) in imgList"
:key="index"
class="swiper-item"
>
<div class="swiper-zoom-container">
<img class="img-item" :src="item.resourceUrl" />
</div>
</swiper-slide>
</swiper>
</div>
</template>
</div>
</template>
<style scoped lang="less">
.share-topic-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
font-size: 16px;
}
.loading-wrapper {
width: 100%;
height: 100%;
.loading-background {
width: 100%;
height: 100%;
}
.loading-logo-zhc {
position: fixed;
top: 110px;
left: 40px;
width: 40%;
}
.loading-logo-iflyspark {
position: fixed;
right: 40px;
bottom: 56px;
width: 30%;
}
:deep(.el-loading-spinner .path) {
stroke: var(--color-blue) !important;
stroke-width: 5px;
}
:deep(.el-loading-text) {
font-size: 24px;
color: var(--text-gray) !important;
}
}
.navigation-wrapper {
display: flex;
align-items: center;
width: 100%;
height: 80px;
.navigation-close {
width: 40px;
height: 40px;
margin-left: 30px;
img {
width: 100%;
height: 100%;
}
}
.navigation-title {
flex: 1;
height: 43px;
margin: 0 10px;
font-size: 36px;
font-weight: bold;
line-height: 43px;
text-align: center;
}
.navigation-right {
width: 40px;
height: 40px;
margin-right: 30px;
}
}
.pagination-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 80px;
margin: 32px 10px;
.list-button-wrapper {
display: flex;
flex: 1;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 1px;
margin: 0 20px;
&.notFull {
justify-content: center;
.item-button {
margin: 0 16px;
}
}
}
.button {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
min-width: 80px;
height: 80px;
min-height: 80px;
font-size: 40px;
font-weight: 600;
cursor: pointer;
border: 1px solid var(--border-light-gray);
border-radius: 12px;
&.switch-button {
border: 1px solid transparent;
}
&.active {
color: var(--color-white);
background-color: var(--bg-blue);
}
img {
width: 60px;
height: 60px;
}
}
}
.content-wrapper {
flex: 1;
margin: 0 30px;
.img-list {
width: 100%;
height: 100%;
.swiper-item {
max-width: 100%;
max-height: 100%;
border: 1px solid transparent;
.swiper-zoom-container {
display: flex;
align-items: center;
max-width: 100%;
max-height: 100%;
.img-item {
max-width: 100%;
height: auto;
}
}
}
}
}
</style>
2024.04.26 更新
说一下最近由于
Swiper
遇到的坑位点
Swiper 滚动偏移导致的两侧出现前一页面和后一页面内容
该现象的实际效果是:
- 最开始的几页滚动是没有异常的;
- 但是当页数越来越多的时候,当前页两侧会出现垂直状黑色细条;
- 经排查,发现该细条是属于前一页或后一页的图片。
原因排查:
- 由于排查发现该黑色细条属于前一页或后一页的图片内容,则得出结论是属于内容发生了偏移;
- 而正常情况下,
Swiper
滚动是不会发生偏移的,然后就查询了一下当前滚动对象swiper-wrapper
类的transform
属性; - 经观察发现,每个单位的
div.swiper-slide
的渲染宽度为358.81px
,而每次swiper-wrapper
滚动是transform
变化单位是359px
,存在了一定的偏差; - 所以当滚动的页数达到一定量级的时候,这个偏差值就会变得很大,也就出现了上面的内容偏移。
问题解决:
- 既然是由于
swiper-wrapper
的transform
滚动没有按照实际值进行滚动,那就在滚动的时候对该值进行调整; - 不能直接使用
clientWidth
取值,该值也是transform
实际滚动单位; - 通过
getBoundingClientRect()
方法获取实际当前swiper-wrapper
渲染的尺寸,然后进行调整; - 同时为了防止出现其它异常场景,可以对每个
swiper-item
添加border: 1px solid transparent;
透明边框属性,防止内容外溢。
<script setup lang="ts">
const onSlideChangeEvent = (swiper: SwiperObj) => {
curIndex.value = swiper.activeIndex;
curPage.value = Math.floor(curIndex.value / pageNum);
// 修正由于 swiper-wrapper 的 transform 属性不允许有小数引起的滚动偏移问题
const swiperEl = document.querySelector(".swiper-wrapper") as HTMLElement;
if (swiperEl) {
const rect = swiperEl.getBoundingClientRect();
swiperEl.style.transform = `translate3d(-${
rect.width * curIndex.value
}px, 0px, 0px)`;
}
};
</script>
参考链接
版权声明
原文作者:苜蓿鬼仙(苜蓿、jijiucheng)
原文链接:GitHub.io - 苜蓿鬼仙 - 【前端】Vue3 中 Swiper 的使用示例
发表日期:2024/04/23 16:30:00
更新日期:2024/04/26 10:00:00
-
GitHub:GitHub - jijiucheng
个人博客:GitHub.io - 苜蓿鬼仙
小专栏:小专栏 - 苜蓿鬼仙
掘金:掘金 - 苜蓿鬼仙
微博:微博 - 苜蓿鬼仙
公众号:微信 - 苜蓿小站
小程序:微信 - 苜蓿小站