使用vue3+vite+typescript编写一个网易云首页轮播插件

vue3+vite已经成熟。就想着哪来练手项目,然后写到页面轮播部分时突然奇想,不如使用vue3编写一个轮播组件练练手,于是网上找到了轮播图的编写思路,同时参考element-plus库源码的编写思路,最后用render函数的形式完成了这个组件,今天分享给大家

效果图如下

具体实现过程

创建项目的过程忽略了,想要了解的稍后我放出git地址

编写Swiper组件

<!--
 * @Description: 轮播组件
 * @Autor: ZmSama
 * @Date: 2021-05-28 15:00:37
-->
<script lang="ts">
import {
      defineComponent, h, getCurrentInstance, onMounted, ref, VNode } from 'vue';
import Slider from './Slider.vue';
import throttle from '../utils/throttle';
export interface ISlider {
     
  sty?: Object;
  className?: string;
}
export default defineComponent({
     
  name: 'Swiper',
  components: {
     
    Slider,
  },
  props: {
     
    initial: {
     
      type: Number,
      default: 0,
    },
    interval: {
     
      type: Number,
      default: 5000,
    },
    auto: {
     
      type: Boolean,
      default: true,
    },
  },
  setup(props, ctx) {
     
    // 因为setup中没有this,使用方法得到实例
    const instance = getCurrentInstance();

    // 轮播子内容
    const childrenNode = ref([]);
    // 初始index
    const initial = ref(props.initial);
    // 间隔时间
    const interval = ref(props.interval);
    // 定时器对象
    let timer = null;

    // 自动轮播
    const autoPlay = () => {
     
      timer = setInterval(() => {
     
        initial.value++;
        if (initial.value >= childrenNode.value.length) {
     
          initial.value = 0;
        }
      }, interval.value);
    };

    // 上一页(节流一下)
    const preSlider = throttle(() => {
     
      initial.value--;
      if (initial.value < 0) {
     
        initial.value = childrenNode.value.length - 1;
      }
    });

    // 下一页(节流一下)
    const nextSlider = throttle(() => {
     
      initial.value++;
      if (initial.value >= childrenNode.value.length) {
     
        initial.value = 0;
      }
    });

    // 点击指示器前往某一页
    const gotoSlider = (i: number) => {
     
      initial.value = i;
    };

    // 鼠标进入
    const enterSlider = () => {
     
      clearInterval(timer);
    };

    // 鼠标离开
    const leaveSlider = () => {
     
      props.auto && autoPlay();
    };

    // 处理每一项的样式
    const computedSty = (index: number, arr: Array<ISlider>) => {
     
      // 确保索引合法
      let len = arr.length;
      // 判断初始值
      index < 0 ? 0 : index >= len ? len - 1 : initial;
      // 第一项
      let temp1 = index - 1;
      // 第二项(中间项)
      let temp2 = index;
      // 第三项
      let temp3 = index + 1;

      // 判断起始值处于哪里
      temp1 < 0 && (temp1 = len + temp1);
      temp3 >= len && (temp3 = temp3 - len);

      // 修改每一项的样式

      return arr.map((item: ISlider, index) => {
     
        // 初始样式
        let transform = 'translate(-50%, -50%) scale(0.55)',
          zIndex = 0,
          className = '';

        // 根据索引确定项目的样式
        switch (index) {
     
          case temp2:
            zIndex = 1;
            transform = 'translate(-50%, -50%) scale(1)';
            className = ' is-active';
            break;
          case temp1:
            zIndex = 0;
            transform = 'translate(-100%, -50%) scale(0.85)';
            className = '';
            break;
          case temp3:
            zIndex = 0;
            transform = 'translate(-0%, -50%) scale(0.85)';
            className = '';
            break;
        }
        // 给每一项绑定样式
        item.sty = {
     
          transform,
          zIndex,
        };
        //  加类
        item.className = className;

        return item;
      });
    };

    onMounted(() => {
     
      // 这里就是要插入外部传入内容的地方,instance.slots.default()返回的是一个数组,
      // 数组内就是所有未定义名字的插槽内容,
      // 如果想在render中获得相同的内容则调用this.$solts. default()
      const arr = instance.slots.default();
      // 从上面取出所有的实际子节点,同时处理v-for循环和直接写组件的形式,
      // 还要防止用户使用非slider组件(使用v-for得到的将是数组、直接写就是对象)
      const Collection = Array.from(arr).map((item: VNode) => {
     
        // 说明是v-for指令的
        if (typeof item.type == 'symbol') {
     
          return item.children;
        } else if (item.type['name'] == 'Slider') {
     
          return item;
        } else {
     
          throw new Error('swiper组件内部只允许使用组件');
        }
      });
      // 将上面得到的可能是二维数组打散成一维数组既是所有的实际子节点
      childrenNode.value = Collection.flat().map((item: VNode) => item.children);

      props.auto && autoPlay();
    });
    return {
     
      initial,
      computedSty,
      childrenNode,
      preSlider,
      nextSlider,
      gotoSlider,
      enterSlider,
      leaveSlider,
    };
  },
  render() {
     
    const {
     
      initial,
      computedSty,
      childrenNode,
      preSlider,
      nextSlider,
      gotoSlider,
      enterSlider,
      leaveSlider,
    } = this;
    // 轮播主体内容

    const sliderList = computedSty(initial, childrenNode);
    //  渲染轮播内容
    const slider = h(
      'div',
      {
     
        class: 'slider-content',
      },
      [
        sliderList.map((item, index) => {
     
          return h(
            'div',
            {
     
              class: {
     
                slider: true,
                'is-active': index === initial,
              },
              style: item.sty,
              onClick: () => gotoSlider(index),
              onMouseenter: () => enterSlider(),
              onMouseleave: () => leaveSlider(),
            },
            item.default()
          );
        }),
      ]
    );

    // // 底部点点
    let dotItem = childrenNode.map((item, index) => {
     
      return h('div', {
     
        class: {
     
          dot: true,
          'is-active': index == initial,
        },
        onClick: () => throttle(gotoSlider(index)),
      });
    });
    const dot = h(
      'div',
      {
     
        class: 'dot-wrap',
      },
      [dotItem]
    );

    // // 左右按钮
    const arrowLeft = h('div', {
     
      class: 'arrow-left',
      onClick: () => preSlider(),
      onMouseenter: () => enterSlider(),
      onMouseleave: () => leaveSlider(),
    });

    const arrowRight = h('div', {
     
      class: 'arrow-right',
      onClick: () => nextSlider(),
      onMouseenter: () => enterSlider(),
      onMouseleave: () => leaveSlider(),
    });

    //组合
    return h(
      'div',
      {
     
        class: 'slider-container',
      },
      [slider, dot, arrowLeft, arrowRight]
    );
  },
});
</script>
<style lang="scss" scoped></style>

接着写Slider组件

<!--
 * @Description: 
 * @Autor: ZmSama
 * @Date: 2021-05-28 15:00:52
-->
<template>
  <div>
    <slot>Slider</slot>
  </div>
</template>

<script lang="ts">
import {
      defineComponent } from 'vue';

export default defineComponent({
     
  name: 'Slider',
});
</script>
<style lang="scss" scoped></style>

接下来是样式

// 系统颜色
$red: rgba(255, 0, 0, 0.816);

// 垂直和水平居中
@mixin jcc-aic {
     
  display: flex;
  justify-content: center;
  align-items: center;
}
// 水平居中
@mixin jcc {
     
  display: flex;
  justify-content: center;
}
// 垂直居中
@mixin aic {
     
  display: flex;
  align-items: center;
}

// 横向排列,从头开始
@mixin jcs-row {
     
  display: flex;
  justify-content: flex-start;
  flex-direction: row;
}
// 横向排列,从头开始,垂直居中
@mixin jcc-aic-row {
     
  display: flex;
  justify-content: flex-start;
  align-items: center;
  flex-direction: row;
}
// 轮播样式
.slider-container {
     
  height: 100%;
  width: 100%;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  position: absolute;
  .slider-content {
     
    position: relative;
    box-sizing: border-box;
    height: 100%;
    width: 100%;
    .slider {
     
      position: absolute;
      width: 55%;
      height: 82%;
      background-color: aqua;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      transition: all 0.3s ease-in-out;
      cursor: pointer;
      border-radius: 14px;
      overflow: hidden;
      box-shadow: 3px 3px 3px 1px rgba(0, 0, 0, 0.2);
      @include jcc-aic;
      img {
     
        width: 100%;
        height: 100%;
      }
    }
  }
  .dot-wrap {
     
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 2;
    @include jcc-aic-row;
    .dot {
     
      width: 10px;
      height: 10px;
      border-radius: 50%;
      background-color: #ccc;
      margin-left: 10px;
      transition: all 0.3s ease-in-out;
      cursor: pointer;
    }
    .is-active {
     
      background-color: $red;
    }
  }

  .arrow-left {
     
    position: absolute;
    left: 10px;
    top: 50%;
    margin-top: -15px;
    cursor: pointer;
    border: solid #ffff;
    border-width: 0 3px 3px 0;
    display: inline-block;
    transform: rotate(135deg);
    padding: 10px;
    opacity: 0;
  }
  .arrow-right {
     
    position: absolute;
    right: 10px;
    top: 50%;
    margin-top: -15px;
    cursor: pointer;
    border: solid #ffff;
    border-width: 0 3px 3px 0;
    display: inline-block;
    transform: rotate(-45deg);
    padding: 10px;
    opacity: 0;
  }
  &:hover .arrow-left {
     
    opacity: 1;
  }
  &:hover .arrow-right {
     
    opacity: 1;
  }
}

最后是具体使用(这些图片是从网易云的官网复制下来的,可能有天就失效了,自己找图片换上去

<!--
 * @Description: 
 * @Autor: ZmSama
 * @Date: 2021-05-28 14:40:41
-->
<template>
  <div class="app">
    <div class="swiper-container">
      <swiper :initial="2" :interval="3000" :auto="true">
        <slider name="slider2" v-for="item in source" :key="item.id">
          <img :src="item.pic" alt="" srcset="" />
        </slider>
      </swiper>
    </div>
  </div>
</template>

<script lang="ts">
import {
      defineComponent } from 'vue';
export default defineComponent({
     
  name: 'App',
  setup() {
     
    const source = [
      {
     
        id: 1,
        label: 'Slider1',
        color: '#ff3399',
        pic: 'http://p1.music.126.net/O5hmcHHdJpABcArdHOXXZw==/109951166006223055.jpg?imageView&quality=89',
      },
      {
     
        id: 2,
        label: 'Slider2',
        color: '#99ffff',
        pic: 'http://p1.music.126.net/uhxClMS2xY2b48a-PsoG9g==/109951166006290269.jpg?imageView&quality=89',
      },
      {
     
        id: 3,
        label: 'Slider3',
        color: '#66cc00',
        pic: 'http://p1.music.126.net/Rgqg8zqkKiewTUMjh0GMCg==/109951166005126541.jpg?imageView&quality=89',
      },
      {
     
        id: 4,
        label: 'Slider4',
        color: '#0066ff',
        pic: 'http://p1.music.126.net/oMXHYkRHy5XbE0905ljqZg==/109951166005813356.jpg?imageView&quality=89',
      },
      {
     
        id: 5,
        label: 'Slider5',
        color: '#00ff33',
        pic: 'http://p1.music.126.net/IXXt2TECvZIeEVNUr1E_gA==/109951166004785770.jpg?imageView&quality=89',
      },
    ];
    return {
     
      source,
    };
  },
});
</script>

<style lang="scss" scoped>
.app {
     
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  .swiper-container {
     
    width: 80%;
    height: 300px;
    position: relative;
  }
}
</style>

以上就是全部的源代码了

懒得自己写的朋友可以访问我的git地址。如果能点个start就更好了项目地址

你可能感兴趣的:(Vue专栏)