<script lang="ts" setup>
import { defineProps, PropType } from 'vue';
import { Overlay } from 'vant';
import * as utils from '@/utils';
import * as api from '@/api';
import Swiper from './Swiper.vue';
import SwiperItem from './SwiperItem.vue';
const props = defineProps({
medias: {
type: Array as PropType<api.message.Media[]>,
required: true,
},
index: {
type: Number,
required: true,
},
handelClose: {
type: Function,
required: true,
},
});
</script>
<template>
<Overlay class="wrap" :show="true">
<div class="close t-white fs15px" @click="props.handelClose()">返回</div>
<Swiper :initialSwipe="props.index">
<SwiperItem v-for="(media, i) in props.medias" :key="i" class="f-row f-center">
<img v-if="media.type === api.message.MediaType.Photo" :src="utils.file.getResourceUrl(media.url)"
alt="图片" />
<video v-else controls controlslist="nodownload" disablepictureinpicture playsinline="true"
:src="utils.file.getResourceUrl(media.url)"></video>
</SwiperItem>
</Swiper>
</Overlay>
</template>
<style scoped lang="less">
@import "@/variables";
.wrap {
width: 100%;
height: 100%;
overflow: hidden;
}
.close {
position: absolute;
top: 10 * @px;
right: 0;
z-index: 999;
padding: 0 @panelPadding;
height: 32 * @px;
line-height: 32 * @px;
}
img {
max-width: 100vw;
max-height: 100vh;
padding: @panelPadding;
}
video {
object-fit: fill;
width: 400 * @px;
max-height: 100vh;
padding: @panelPadding;
background-color: #000;
}
</style>
<style lang="less">
@import "@/variables";
.image-viewer-swipe .van-swipe__indicators {
top: 23 * @px;
}
</style>
手写一个Swiper
<script setup lang="ts">
import { defineProps, ref, onMounted, reactive } from 'vue';
const props = defineProps({
initialSwipe: {
type: Number,
default: 0,
required: true,
},
});
const slideStyle = reactive({ style: {} });
const scrolling = ref(false);
const startX = ref(0);
const currentX = ref(0);
const distance = ref(0);
const currentIndex = ref(props.initialSwipe + 1);
const slideNum = ref(0);
const totalWidth = ref(0);
const animDuration = ref(300);
const moveRatio = ref(0.25);
function animate(position: number) {
Object.assign(slideStyle.style,
{
transform: `translate3d(${position}px, 0, 0)`,
'-webkit-transform': `translate3d(${position}px, 0, 0)`,
'-ms-transform': `translate3d(${position}px, 0, 0)`,
});
}
function handleDom() {
const swiperEl = document.querySelector('.slide') as HTMLElement;
const swiperitems = swiperEl.getElementsByClassName('swiperitem');
slideNum.value = swiperitems.length;
if (slideNum.value > 1) {
const firstClone = swiperitems[0].cloneNode(true);
const lastClone = swiperitems[slideNum.value - 1].cloneNode(true);
swiperEl.insertBefore(lastClone, swiperitems[0]);
swiperEl.appendChild(firstClone);
totalWidth.value = swiperEl.offsetWidth;
const { style } = swiperEl;
Object.assign(slideStyle, { style });
}
animate(-totalWidth.value * currentIndex.value);
// eslint-disable-next-line no-undef
let firstTimer: NodeJS.Timeout;
window.onresize = () => {
if (firstTimer) {
clearTimeout(firstTimer);
}
firstTimer = setTimeout(() => {
totalWidth.value = swiperEl.offsetWidth;
animate(-totalWidth.value);
currentIndex.value = 1;
}, 100);
};
}
/**
* 调整动画
*/
function checkPosition() {
window.setTimeout(() => {
Object.assign(slideStyle.style, { transition: '0ms' });
if (currentIndex.value >= slideNum.value + 1) {
currentIndex.value = 1;
animate(-currentIndex.value * totalWidth.value);
} else if (currentIndex.value <= 0) {
currentIndex.value = slideNum.value;
animate(-currentIndex.value * totalWidth.value);
}
}, animDuration.value);
}
function scrollContent(position: number) {
scrolling.value = true;
Object.assign(slideStyle.style, { transition: `transform ${animDuration.value}ms` });
animate(position);
checkPosition();
scrolling.value = false;
}
function touchstart(e: TouchEvent) {
const video = e.touches[0].target as HTMLVideoElement;
if (video.localName === 'video') {
video.pause();
}
if (scrolling.value) return;
startX.value = e.touches[0].pageX;
}
function touchmove(e: TouchEvent) {
currentX.value = e.touches[0].pageX;
distance.value = currentX.value - startX.value;
const currentPosition = -currentIndex.value * totalWidth.value;
const moveDistance = distance.value + currentPosition;
animate(moveDistance);
scrolling.value = true;
}
/**
* 手指从一个 DOM 元素上移开时触发
*/
function touchend() {
if (scrolling.value) {
const currentMove = Math.abs(distance.value);
if (distance.value === 0) {
return;
}
if (distance.value > 0 && currentMove > totalWidth.value * moveRatio.value) {
// eslint-disable-next-line no-plusplus
currentIndex.value--;
}
if (distance.value < 0 && currentMove > totalWidth.value * moveRatio.value) {
// eslint-disable-next-line no-plusplus
currentIndex.value++;
}
}
scrollContent(-currentIndex.value * totalWidth.value);
}
onMounted(() => {
handleDom();
});
</script>
<template>
<div class="swiper">
<div class="slide" @touchstart="touchstart" @touchend="touchend" @touchmove="touchmove">
<slot></slot>
</div>
<div>
<ul class="indicator">
<li v-for="i in slideNum" :key="i" :class="{ active: i === currentIndex }" class="indi-item"></li>
</ul>
</div>
</div>
</template>
<style scoped lang="less">
@import "@/variables";
.swiper {
overflow: hidden;
position: relative;
}
.slide {
display: flex;
}
.indicator {
display: flex;
justify-content: center;
position: absolute;
width: 100%;
top: 23 * @px;
}
.indi-item {
box-sizing: border-box;
width: 8px;
height: 8px;
border-radius: 4px;
background-color: #fff;
line-height: 8px;
text-align: center;
font-size: 12px;
margin: 0 5px;
}
.indi-item.active {
background-color: rgb(192, 189, 189);
}
</style>
SwiperItem组件
<script setup lang="ts">
</script>
<template>
<div class="swiperitem">
<slot></slot>
</div>
</template>
<style scoped lang="less">
.swiperitem {
width: 100%;
flex-shrink: 0;
height: 100vh;
max-height: 100vh;
}
</style>