响应式轮播图 高级轮播

最近看到了一个轮播效果图,来自于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>
相关推荐
Ashore_2 小时前
从简单封装到数据响应:Vue如何引领开发新模式❓❗️
前端·vue.js
顽疲2 小时前
从零用java实现 小红书 springboot vue uniapp (6)用户登录鉴权及发布笔记
java·vue.js·spring boot·uni-app
&活在当下&2 小时前
ref 和 reactive 的用法和区别
前端·javascript·vue.js
云白冰3 小时前
hiprint结合vue2项目实现静默打印详细使用步骤
前端·javascript·vue.js
m0_748251723 小时前
前端入门之VUE--ajax、vuex、router,最后的前端总结
前端·vue.js·ajax
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS安康旅游网站(JAVA毕业设计)
java·vue.js·spring boot·后端·kafka·开源·旅游
跨境商城搭建开发6 小时前
一个服务器可以搭建几个网站?搭建一个网站的流程介绍
运维·服务器·前端·vue.js·mysql·npm·php
hhzz6 小时前
vue前端项目中实现电子签名功能(附完整源码)
前端·javascript·vue.js
冰红茶-Tea7 小时前
typescript数据类型(二)
前端·typescript
slongzhang_7 小时前
elementPlus消息组件多按钮案例
前端·javascript·vue.js