最近看到了一个轮播效果图,来自于B站UP主山羊の前端小窝,于是照着效果封装了一个 vue组件。
顺便做了一点改进,让整个轮播前后连贯起来。
效果图
轮播效果图
应用
index.vue
html
<script setup lang="ts">
import Carousel from "@/components/CarouselView.vue";
// 最少需要6张图片素材 webImgs.length ≥ 6
const webImgs = Object.keys(import.meta.glob("@/assets/images/web/*.jpg"), { eager: true }));
</script>
<template>
<Carousel :list="webImgs" />
</template>
CarouselView.vue
html
<script lang="ts" setup>
const props = defineProps<{
readonly list: string[];
}>();
// 窗口大小为 7,前后补充6个素材就可以连贯起来
const supplyNum = 7 - 1;
const imglist = [...props.list];
const len = props.list.length;
imglist.unshift(...props.list.slice(len - supplyNum, len));
imglist.push(...props.list.slice(0, supplyNum));
const rotate = (i: number) => {
// 中间的素材没有角度偏移 左右两边的偏移35度
const val = i - options.mmiddleCur;
return val < 0 ? 30 : val > 0 ? -30 : 0;
};
const options = reactive({
// 是否启用动画过渡 这是首尾连接效果的关键
isTrans: true,
// 最中间素材的下标 初始值是第一张图片,但相对整个素材列表来说是第7个
mmiddleCur: supplyNum,
// 这里我们使用左外边距来实现平移的效果
// 前面补了6个素材,隐掉了3个,所以初始左外边距是 负的 3个素材宽度
// 240 是一个素材(.front)的宽度
marginLeftCur: -3 * 240,
});
const timer = ref<number>();
/** 这一块逻辑比较绕,需要结合实操才能更好的理解 */
const toLeft = () => {
// 当左滑到第四个素材时,下一个就进入末尾了
if (options.mmiddleCur == 3) {
// 我们在进入末尾之前,把动画过渡停掉
options.isTrans = false;
// 然后把素材(窗口最右侧)换成末尾之前的那个素材(其实素材是一样的,但是位置不一样),也就是
// 中间素材对应的是:素材列表减掉后补的6个素材,再往前推3个
options.mmiddleCur = imglist.length - 6 - 3;
// 左边距是:中间素材,再往前推4个,所有素材的宽度总和
options.marginLeftCur = -(options.mmiddleCur + 1 - 4) * 240;
// OK,到了这一步,虽然页面上没什么变化,但其实素材位置已经变了,已经连续上了
// 接下来,我们正常走上一页的逻辑就好
if (timer.value) clearTimeout(timer.value);
// 这里 nextTick() 不好使,我们用一个定时器来延迟一下
timer.value = setTimeout(() => last(), 0);
} else last();
function last() {
options.isTrans = true;
options.mmiddleCur--;
options.marginLeftCur += 240;
}
};
// 下一页的逻辑是差不多的
const toRight = () => {
// 当右滑到倒数第四个素材时,下一个就进入开头了
if (options.mmiddleCur == imglist.length - 1 - 3) {
// 我们在进入开头之前,把动画过渡停掉
options.isTrans = false;
// 然后把素材(窗口最左侧)换成开头之前的那个素材,也就是
// 中间素材对应的是:前补的6个素材,再往后推3个,也就是第9个素材
options.mmiddleCur = 6 + 3 - 1; // 下标计算 -1
// 左边距是:中间(第9个)素材,再往前推4个,总共5个素材的宽度总和
options.marginLeftCur = -(options.mmiddleCur + 1 - 4) * 240;
if (timer.value) clearTimeout(timer.value);
timer.value = setTimeout(() => next(), 0);
} else next();
function next() {
options.isTrans = true;
options.mmiddleCur++;
options.marginLeftCur -= 240;
}
};
const inter = setInterval(toRight, 1500);
onUnmounted(() => {
clearTimeout(timer.value);
clearInterval(inter);
});
</script>
<template>
<div class="carousel">
<div class="background" :style="{ backgroundImage: `url(${imglist[options.mmiddleCur]})` }"></div>
<div class="carousel-scroll">
<div :class="['carousel-body', options.isTrans && 'trans']">
<div class="carousel-item" v-for="(img, inx) in imglist" :key="inx">
<div class="carousel-per" :style="{ transform: `rotateY(${rotate(inx)}deg)` }">
<div class="box front" :style="{ backgroundImage: `url(${img})` }"></div>
<div class="box left" :style="{ backgroundImage: `url(${img})` }"></div>
<div class="box right" :style="{ backgroundImage: `url(${img})` }"></div>
</div>
</div>
</div>
</div>
<div class="btns">
<div class="btn last" @click="toLeft"></div>
<div class="btn next" @click="toRight"></div>
</div>
</div>
</template>
<style lang="scss" scoped>
.carousel {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
background-position: center;
background-size: 100%;
transition: 1s;
.background {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
// 高斯模糊
filter: blur(3px);
z-index: -1;
}
.carousel-scroll {
width: 1720px; // 240 * 7 + 40
margin: 100px auto 100px;
padding: 100px 20px;
box-shadow: 0 0 20px rgba($color: skyblue, $alpha: 0.5);
overflow: hidden;
.trans {
transition: 0.5s ease-in-out;
.carousel-per {
transition: transform 0.5s ease-in-out;
}
}
.carousel-body {
display: flex;
height: 100%;
margin-left: v-bind("options.marginLeftCur + 'px'");
.carousel-item {
perspective: 1200px;
.carousel-per {
position: relative;
transform-style: preserve-3d;
&:hover {
.box {
box-shadow: 0 0 50px rgba($color: #fff, $alpha: 0.7);
}
}
.box {
height: 100%;
background-position: center;
background-size: cover;
border: 4px solid #fff;
box-shadow: 0 0 50px rgba($color: pink, $alpha: 0.7);
}
.front {
position: relative;
width: 200px;
height: 300px;
margin: 0 20px;
transition: transform 1s ease-in-out;
transform-style: preserve-3d;
&:after {
content: "";
position: absolute;
bottom: -20%;
width: 100%;
height: 60px;
background: #ffffff1c;
box-shadow: 0px 0px 15px 5px #ffffff1c;
transform: rotateX(-90deg) translate3d(0, 20px, 0px);
}
}
.left,
.right {
position: absolute;
top: 0;
width: 40px;
}
.left {
left: 0px;
transform: translate3d(1px, 0, -20px) rotateY(-90deg);
}
.right {
right: 0px;
transform: translate3d(-1px, 0, -20px) rotateY(90deg);
}
}
}
}
}
.btns {
display: flex;
justify-content: center;
.btn {
width: 40px;
height: 60px;
margin: 0 100px;
background-color: orangered;
transition: 0.5s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
.last {
clip-path: polygon(100% 0, 0 50%, 100% 100%, 60% 50%);
}
.next {
clip-path: polygon(0 0, 100% 50%, 0 100%, 40% 50%);
}
}
}
</style>