51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Vue实现移动端轮播swiper组件

# 前言 {#前言}

轮播组件是在移动端很常用的一种组件,而swiper (opens new window)vue-awesome-swiper (opens new window)(123KB)组件基本是大家的首选。鉴于现在对移动端性能的要求越来越高,而这两个组件功能较全、体积较大,很多功能可能根本用不到,故封装了 只适用于移动端、只支持水平方向滑动(可扩展支持垂直方向)OcSwiper 组件,本组件源码大小15KB,打包后大小约10KB,gzip后大小约4KB。

# Swiper轮播实现功能 {#swiper轮播实现功能}

轮播组件,支持循环、分页、自动轮播等(目前仅支持水平方向轮播)。

# 演示 {#演示}

提示

触摸滑动只支持在移动端设备进行演示,如需在PC端使用,可自行修改组件源码,将移动端touch事件替换为mouse事件。
1
2
3
4
更新数据 增加一条数据 滚动到第3个 停止自动播放 3S自动播放 关闭循环 最小滑动距离50%切换 开启自定义分页 隐藏分页

# 安装 {#安装}

# 全局引入 {#全局引入}

import { OcSwiper  } from 'OcSwiper'
Vue.use(OcSwiper)

# 按需引入 {#按需引入}

import { OcSwiper } from 'OcSwiper'
export default {
  components: {
    OcSwiper,
  }
}

# 基础用法 {#基础用法}

<oc-swiper ref="swiper"
  :pagination="true"
  :autoplayTime="3000"
  :loop="true"
  minMoveDistance="20%"
  @slideChangeTransitionStart="slideChangeTransitionStart"
  @slideChangeTransitionEnd="slideChangeTransitionEnd"
  @onTouchStart="onTouchStart"
  @onTouchStart="onTouchStart"
  @onTouchEnd="onTouchEnd"
  v-model="index">
    <div class="oc-swiper-item" v-for="(item, idx) in list" :key="idx" @click="slideClick(item, idx)">
      <div class="slide-item">{{ item }}</div>
    </div>

    <!-- 自定义分页展示:slot="pagination" -->
    <!-- 样式一:1 2 3 4 -->
    <div slot="pagination">
      <ul>
        <li v-for="page in list.length" :key="page" :class="{'cur-page': index === (page - 1)}">
          {{ page }}
        </li>
      </ul>
    </div>
    <!-- 样式二:1/4 -->
    <div slot="pagination">
      <div>{{ index + 1 }} / {{ list.length }}</div>
    </div>
</oc-swiper>

# 方法 {#方法}

# mySwiper.reset() 重置swiper组件。常用于window.onresize和swiper数据异步更新,如: {#myswiper-reset-重置swiper组件。常用于window-onresize和swiper数据异步更新-如}

// 窗口大小改变
window.onresize = () => {
  this.$refs.swiper.reset()
}

// 列表数据更新
this.list = ['a', 'b', 'c', 'd']
this.$nextTick(() => {
  // 接口异步获取到列表数据后,重置swiper组件
  this.$refs.swiper.reset()
})

# 配置参数 {#配置参数}

| 参数 | 说明 | 类型 | 默认值 | 说明 | |------------------|-----------------|---------|------|---------------------------------------------| | v-model | 绑定值 | Number | 0 | 当前slide索引,从0开始 | | pagination | 是否显示分页器 | Boolean | true | 默认显示分页器 | | autoplayTime | 自动轮播时间间隔 | Number | 2500 | 0-不开启自动轮播,>0开启自动轮播,数值表示时间间隔 | | loop | 是否循环 | Boolean | true | 默认开启循环 | | minMoveDistance | 最小滑动距离 | String | 20% | 成功触发切换 item 的最小滑动距离。支持格式: 20(20px)/20px/20% | | quickTouchTime | 快速滑动 | Number | 150 | 单位(ms)。快速滑动时,只要距离大于 10px 便可以触发滑动 | | speed | 切换动画时间 | Number | 300 | 单位(ms) | | isPreventDefault | 是否在滑动时禁用浏览器默认行为 | Boolean | true | 默认禁用。 |

# 模版调用的回调方法 {#模版调用的回调方法}

| 方法名 | 说明 | 回调参数 | 备注 | |----------------------------|--------------------------------|-----------------------------------------------------------------------------------------|----| | slideChangeTransitionStart | swiper从当前slide开始过渡到另一个slide时执行 | realIndex,切换后slide索引 | | | slideChangeTransitionEnd | swiper从一个slide过渡到另一个slide结束时执行 | realIndex,切换后slide索引 | | | onTouchStart | touchStart,碰触到组件时执行 | - | | | onTouchMove | touchMove | (event, direction) event:可用于禁用浏览器默认操作。 direction:方向(horizontal:水平方向 vertical:垂直方向)。 | | | onTouchEnd | touchEnd,触摸释放时执行 | - | |

# 模版插槽 {#模版插槽}

| 名字 | 说明 | |------------|------------------------------------------------------------------------------| | 默认插槽 | swiper组件内容展示。格式: <div class="oc-swiper-item"><div>swiper slide</div></div> | | pagination | 自定义分页样式展示 |

# 源码 {#源码}

点击查看 OcSwiper.vue 组件源码

<template>
  <div
    class="oc-swiper"
    @touchstart="handleTouchstart"
    @touchmove="handleTouchmove"
    @touchend="handleTouchend"
    @touchcancel="handleTouchend"
  >
    <div ref="wrapper" class="oc-swiper-wrapper">
      <slot></slot>
    </div>
    <div v-if="pagination" class="oc-swiper-pagination">
      <!-- 自定义分页器展示 -->
      <slot name="pagination">
        <div class="oc-swiper-pagination-bar">
          <i
            v-for="item in length"
            :key="item"
            :class="[
              'oc-swiper-pagination-item',
              item - 1 === realIndex ? 'active' : '',
            ]"
          ></i>
        </div>
      </slot>
    </div>
  </div>
</template>
<script>
export default {
  name: "OcSwiper",
  props: {
    value: {
      // 默认展示第N个,N从0开始
      type: Number,
      default: 0,
    },
    pagination: {
      // 默认分页导航器
      type: Boolean,
      default: true,
    },
    autoplayTime: {
      // 自动轮播时间间隔
      type: Number,
      default: 2500,
    },
    loop: {
      // 循环滑动
      type: Boolean,
      default: true,
    },
    minMoveDistance: {
      // 成功触发切换 item 的最小滑动距离。支持格式: 20(20px)/20px/20%
      type: String,
      default: "20%",
    },
    quickTouchTime: {
      // 快速滑动时只要距离大于 10px 便可以触发滑动
      type: Number,
      default: 150,
    },
    speed: {
      // 切换速度,即slider自动滑动开始到结束的时间(单位ms),也是触摸滑动时释放至贴合的时间。
      type: Number,
      default: 300,
    },
    isPreventDefault: {
      // 是否在滑动时禁用浏览器默认行为。默认为true表示禁用
      type: Boolean,
      default: true,
    },
  },
  data() {
    return {
      width: 0, // 组件宽度
      hasMounted: false,
      realIndex: this.value, // 当前活动块的索引,从0开始
      isRealIndexChange: false, // 滑动后当前活动块的索引是否改变
      pages: [], // swiperSlide dom list
      length: 0,
      touchStartTime: 0, // 滑动开始时间
      startx: 0, // 滑动开始位置X
      starty: 0, // 滑动开始位置Y
      moveDistance: 0, // 滑动距离
      animating: false, // 组件正在过渡(自由滑动)
      isTouching: true, // 用户正在操作(touch事件未结束)
      horizontalMove: true,
      copyNum: 1, // 复制数量
      autoplayTimer: null,
    };
  },
  computed: {
    ocMinMoveDistance() {
      let value = this.minMoveDistance;
      let mode = "";
      if (/px$/.test(value)) {
        mode = "pixel";
      } else if (/^\d+$/.test(value)) {
        mode = "pixel";
      } else if (/%$/.test(value)) {
        mode = "percent";
      } else {
        value = "20%";
        mode = "percent";
      }
      const stgy = {
        pixel() {
          const parsedValue = parseInt(value, 10);
          return `${value}px`;
        },
        percent() {
          const parsedValue = parseInt(value, 10) / 100;
          return this.width * parsedValue;
        },
      };
      return stgy[mode].apply(this);
    },
    // 滑动结束后 translatex 的值
    ocTranslatex() {
      return -this.width * this.realIndex;
    },
    ocIsEnd() {
      return this.realIndex === this.length - 1;
    },
    ocIsBegin() {
      return this.realIndex === 0;
    },
  },
  watch: {
    realIndex(val) {
      if (val !== this.value) {
        this.$emit("input", val);
      }
      this.valueChangeHandler(val);
    },
    value(val) {
      const realIndex = this.realIndex;
      const length = this.pages.length;
      if (val >= length) {
        val = realIndex;
        this.$emit("input", val);
      }
      this.realIndex = val;
    },
    autoplayTime() {
      this.autoChange();
    },
  },
  mounted() {
    this.hasMounted = true;
    this.init();
    this.initOnce();
  },
  destroyed() {
    this.autoplayTimer && clearTimeout(this.autoplayTimer);
  },
  methods: {
    reset() {
      this.init();
      this.initOnce();
    },
    init() {
      // 如果组件 mounted 前 init 方法被调用,则会引起报错。
      // 因此使用 hasMounted 变量来保证不会报错。
      if (!this.hasMounted) return;
      // 设置部分 datas 的值
      const success = this.initDatas();
      if (!success) {
        // Failed to init datas
        return;
      }
      // 为 wrapper 定宽
      this.$refs.wrapper.style.width = `${this.width}px`;
      // 复制首尾 dom
      this.clearCopies();
      this.addCopies();
      // 自动轮播
      if (this.autoplayTime > 0) {
        this.autoChange();
      }
    },
    initOnce() {
      this.setTranslate(this.ocTranslatex);
    },
    initDatas() {
      const style = getComputedStyle(this.$el, false).width;
      this.width = parseInt(style, 10);
      if (!this.$slots.default) {
        // console.warn('No child nodes in swipe component', this.$el);
        return false;
      }
      this.pages = this.$slots.default
        .filter(
          (vnode) => vnode.tag && vnode.elm.classList.contains("oc-swiper-item")
        )
        .map((vnode) => vnode.elm);
      if (!this.pages.length) {
        // console.warn('The swipe component not contained swipe-item component', this.$el);
        return false;
      }
      this.length = this.pages.length;
      return true;
    },
    // 清除复制的首尾dom
    clearCopies() {
      const children = this.$refs.wrapper.querySelectorAll(
        ".oc-swiper-item-copy"
      );
      [...children].forEach((el) => {
        this.$refs.wrapper.removeChild(el);
      }, this);
      this.$refs.wrapper.style.marginLeft = "0";
    },
    // 复制首尾dom
    addCopies() {
      // if(!this.loop) return
      const fronts = [];
      const ends = [];
      const pages = this.pages;
      // 只有一个或不需要循环时时不复制
      if (!this.loop || pages.length < 2) return;
      // copy 第一个和最后一个元素
      pages.forEach((item, index) => {
        if (index === 0) {
          const copy = item.cloneNode(true);
          copy.classList.remove("oc-swiper-item");
          copy.classList.add("oc-swiper-item-copy");
          fronts.push(copy);
        }
        if (index === pages.length - 1) {
          const copy = item.cloneNode(true);
          copy.classList.remove("oc-swiper-item");
          copy.classList.add("oc-swiper-item-copy");
          ends.push(copy);
        }
      }, this);
      this.copyNum = ends.length;
      // insert node
      while (ends.length) {
        const item = ends.shift();
        const firstNode = this.$refs.wrapper.querySelector(".oc-swiper-item");
        this.$refs.wrapper.insertBefore(item, firstNode);
      }
      while (fronts.length) {
        const item = fronts.shift();
        this.$refs.wrapper.appendChild(item);
      }
      this.$refs.wrapper.style.marginLeft = `-${this.width * this.copyNum}px`;
    },
    handleTouchstart(e) {
      this.$emit("onTouchStart");
      if (this.length <= 1 || this.animating) return;
      this.startx = e.touches[0].pageX;
      this.starty = e.touches[0].pageY;
      this.touchStartTime = new Date().getTime();
      // 滑动开始时,清除自动轮播的计时器
      this.autoplayTimer && clearTimeout(this.autoplayTimer);
      // if (this.autoChange) {
      // 	this.autoChange();  // 重置自动轮播的计时器
      // }
      this.isTouching = true;
    },
    handleTouchmove(e) {
      // if (this.length <= 1 || this.animating) return;
      if (this.length <= 1) return;
      if (this.animating) {
        e.preventDefault();
        return;
      }
      if (this.isPreventDefault) {
        e.preventDefault();
      }
      let moveDistance = e.touches[0].pageX - this.startx;
      this.moveDistance = moveDistance;
      // 判断用户是横向滑动还是纵向滑动,以此来避免误滑
      if (this.isTouching) {
        this.isTouching = false;
        const moveY = e.touches[0].pageY - this.starty;
        this.horizontalMove = Math.abs(moveDistance) >= Math.abs(moveY);
      }
      // 用户非水平滑动屏幕
      if (!this.horizontalMove) {
        this.$emit("onTouchMove", e, "vertical");
        return;
      }
      this.$emit("onTouchMove", e, "horizontal");
      const translate = this.ocTranslatex + moveDistance;
      // 正常触摸应该移动的距离
      let finalTranslate = translate;
      // 考虑 loop 的取值时
      if (!this.loop) {
        const leftBoundary = 0;
        const rightBoundary = -this.width * (this.length - 1);
        if (translate > leftBoundary) {
          // 左边界
          finalTranslate = leftBoundary;
        } else if (translate < rightBoundary) {
          // 右边界
          finalTranslate = rightBoundary;
        }
      }
      this.setTranslate(finalTranslate);
    },
    handleTouchend(e) {
      this.$emit("onTouchEnd");
      if (this.length <= 1 || this.animating) return;
      if (!this.horizontalMove) return;
      const isQuick =
        new Date().getTime() - this.touchStartTime < this.quickTouchTime;
      if (this.loop) {
        // 如果是 loop 的话,有很多地方需要特殊处理
        this.handleTouchend_loop(this.cartChange(this.moveDistance, isQuick));
      } else {
        // 根据轮播图滑动的方向来改变 realIndex
        this.updateInsideValue(this.cartChange(this.moveDistance, isQuick));
      }
      // reset some data
      this.moveDistance = 0;
      // 滑动结束,开启自动轮播
      this.autoChange();
    },
    // 考虑 this.loop 的取值对 translate 的影响
    handleTouchend_loop(deviation) {
      if (!this.loop) return;
      const newValue = this.realIndex + deviation;
      // left boundary
      if (this.realIndex === 0 && newValue < this.realIndex) {
        this.setTranslate(-this.width * this.length + this.moveDistance);
        setTimeout(() => {
          this.updateInsideValue(deviation);
        }, 40);
        return;
      }
      // right boundary
      if (this.realIndex === this.length - 1 && newValue > this.realIndex) {
        this.setTranslate(this.width + this.moveDistance);
        setTimeout(() => {
          this.updateInsideValue(deviation);
        }, 40);
        return;
      }
      this.updateInsideValue(deviation);
    },
    /**
     *  根据传入的差值来正确的更新 realIndex
     *  @param  {number} deviation value 改变的差值
     */
    updateInsideValue(deviation) {
      // 因为滑动后如果没有翻页成功,是无法改变 realIndex 的值的,所以需要手动触发 handler
      if (deviation === 0) {
        this.isRealIndexChange = false;
        this.valueChangeHandler(deviation);
        return;
      }
      // 新的 insidevalue 值应该是现在 realIndex 的值 和 改变的差值的和
      const newValue = this.realIndex + deviation;
      this.isRealIndexChange = true;
      // 按顺序查看是否满足处理数据的要求,如果不满足则交给下一个函数处理
      const chain = this.chain(
        // 是否是 loop
        this.updateInsideValue_loop,
        // 什么特殊的设置都没有
        this.updateInsideValue_normal,
        // 通过更新 realIndex 来触发 valueChangeHandler
        (newValue) => {
          this.realIndex = newValue;
        }
      );
      chain(newValue);
    },
    // 当考虑到 loop 的情况时
    updateInsideValue_loop(newValue) {
      if (!this.loop) return false;
      this.isRealIndexChange = true;
      if (newValue < 0) {
        this.realIndex = this.length - 1;
        return "done";
      }
      if (newValue > this.length - 1) {
        this.realIndex = 0;
        return "done";
      }
      return false;
    },
    // 普通状态下, loop === false
    updateInsideValue_normal(newValue) {
      if (newValue < 0) {
        this.realIndex = 0;
        this.isRealIndexChange = false;
        // 因为这时候 realIndex 的值其实没变,所以需要手动触发 valueChangeHandler
        this.valueChangeHandler(0);
        return "done";
      }
      if (newValue > this.length - 1) {
        this.realIndex = this.length - 1;
        this.isRealIndexChange = false;
        // 因为这时候 realIndex 的值其实没变,所以需要手动触发 valueChangeHandler
        this.valueChangeHandler(this.length - 1);
        return "done";
      }
      this.isRealIndexChange = true;
      return false;
    },
    cartChange(moveDistance, quick) {
      const absMove = Math.abs(moveDistance);
      const absMin = Math.abs(this.ocMinMoveDistance);
      // 策略组
      const strategies = {
        // 普通滑动
        normal() {
          if (absMove < absMin) return 0;
          if (moveDistance > 0) return -1;
          if (moveDistance < 0) return 1;
          return 0;
        },
        // 快速滑动
        quick() {
          if (absMove < 10) return 0;
          if (moveDistance > 0) return -1;
          if (moveDistance < 0) return 1;
          return 0;
        },
      };
      let stgy = "normal";
      // 当前策略
      switch (true) {
        case quick === true:
          stgy = "quick";
          break;
        default:
          stgy = "normal";
          break;
      }
      return strategies[stgy].apply(this);
    },
    valueChangeHandler(value) {
      // 添加过渡效果
      this.duration();
      this.setTranslate(this.ocTranslatex);
    },
    // 自动轮播
    autoChange() {
      this.autoplayTimer && clearTimeout(this.autoplayTimer);
      const timer = () => {
        if (
          typeof this.autoplayTime !== "number" ||
          this.autoplayTime <= 0 ||
          this.length <= 1
        )
          return;
        this.autoplayTimer = setTimeout(() => {
          this.autoChangeHandler();
          timer();
        }, this.autoplayTime);
      };
      timer();
    },
    autoChangeHandler() {
      this.isRealIndexChange = true;
      // 如果是右边界,则先移动到左边被 copy 的相同的元素
      if (this.ocIsEnd) {
        this.setTranslate(this.width);
      }
      // 如果不延迟 40 ms 的话,在 setTranslate 的时候,就会触发 transition 效果,这是不想要的。
      setTimeout(() => {
        this.realIndex =
          this.realIndex < this.length - 1 ? this.realIndex + 1 : 0;
      }, 40);
    },
    /**
     *  惰性函数,设置 dom 的 translate 值
     *  @param  {dom}            el       进行变换的元素
     *  @param  {number|string}  trans    进行变换的值
     */
    setTranslate(d) {
      let transform = `translate3d(${d}px, 0, 0)`;
      this.$refs.wrapper.style.webkitTransform = transform;
      this.$refs.wrapper.style.transform = transform;
    },
    /**
     *  添加和删除过渡效果
     *  @param  {Array} args 需要添加过渡动画的元素数组
     */
    duration() {
      this.animating = true;
      // 回调函数,swiper从当前slide开始过渡到另一个slide时执行。
      if (this.isRealIndexChange) {
        this.$emit("slideChangeTransitionStart", this.realIndex);
      }
      const el = this.$refs.wrapper;
      const speed = this.speed;
      el.style.webkitTransitionDuration = `${speed}ms`;
      el.style.transitionDuration = `${speed}ms`;
      el.style.webkitTransitionTimingFunction = "ease-out";
      el.style.transitionTimingFunction = "ease-out";
      // 添加过渡效果
      this.durationTimer && clearTimeout(this.durationTimer);
      this.durationTimer = setTimeout(() => {
        el.style.transitionDuration = "";
        el.style.webkitTransitionDuration = "";
        this.animating = false;
        // 回调函数,swiper从一个slide过渡到另一个slide结束时执行。
        if (this.isRealIndexChange) {
          this.$emit("slideChangeTransitionEnd", this.realIndex);
        }
      }, speed);
    },
    // 职责链,函数 return false 则终止传递
    chain(...fns) {
      return (...args) => {
        for (let index = 0; index < fns.length; index += 1) {
          const result = fns[index](...args);
          if (result === "done") break;
        }
      };
    },
  },
};
</script>

<style lang="scss" scoped>
.oc-swiper {
  overflow: hidden;
}
.oc-swiper-wrapper {
  height: 100%;
  display: flex;
  flex-direction: row;
}
.oc-swiper-item,
.oc-swiper-item-copy {
  width: 100%;
  height: 100%;
  flex: none;
}
.oc-swiper-pagination {
  position: relative;
  height: 0;
}
.oc-swiper-pagination-bar {
  position: absolute;
  left: 0;
  right: 0;
  top: -16px;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}
.oc-swiper-pagination-item {
  display: block;
  width: 4px;
  height: 4px;
  border-radius: 2px;
  background-color: rgba(#fff, 0.6);
  margin: 0 4px;
  transition: all 0.1s;
  &.active {
    width: 8px;
    background-color: #fff;
  }
}
</style>

点击查看 DEMO 源码

<template>
  <div class="demo-swiper-page">
    <section class="demo">
      <oc-swiper
        ref="swiper"
        :autoplayTime="autoplayTime"
        :loop="loop"
        :minMoveDistance="minMoveDistance"
        :pagination="pagination"
        :isPreventDefault="false"
        @slideChangeTransitionStart="slideChangeTransitionStart"
        @slideChangeTransitionEnd="slideChangeTransitionEnd"
        @onTouchStart="onTouchStart"
        @onTouchMove="onTouchMove"
        @onTouchEnd="onTouchEnd"
        v-model="index"
      >
        <div
          class="oc-swiper-item"
          v-for="(item, idx) in list"
          :key="idx"
          @click="slideClick(item, idx)"
        >
          <div class="slide-item">{{ item }}</div>
        </div>
        <div v-if="userPagination" class="user-pagination" slot="pagination">
          <ul v-if="isPageStyle1">
            <li
              v-for="page in list.length"
              :key="page"
              :class="{ 'cur-page': index === page - 1 }"
              @click="index = page - 1"
            >
              {{ page }}
            </li>
          </ul>
          <div v-else class="page-2">{{ index + 1 }} / {{ list.length }}</div>
        </div>
      </oc-swiper>
    </section>
    <button class="g-btn" @click="update">更新数据</button>
    <button class="g-btn" @click="add">增加一条数据</button>
    <button class="g-btn" @click="index = 2">滚动到第3个</button>
    <button class="g-btn" @click="autoplayTime = 0">停止自动播放</button>
    <button class="g-btn" @click="autoplayTime = 3000">3S自动播放</button>
    <button class="g-btn" @click="loop = !loop">
      {{ loop ? "关闭" : "开启" }}循环
    </button>
    <button class="g-btn" @click="minMoveDistance = '50%'">
      最小滑动距离50%切换
    </button>
    <button class="g-btn" @click="userPagination = !userPagination">
      {{ userPagination ? "关闭" : "开启" }}自定义分页
    </button>
    <button
      class="g-btn"
      v-if="userPagination"
      @click="isPageStyle1 = !isPageStyle1"
    >
      自定义分页样式切换
    </button>
    <button class="g-btn" @click="pagination = !pagination">
      {{ pagination ? "隐藏" : "显示" }}分页
    </button>
  </div>
</template>

<script>
export default {
  name: "DemoSwiper",
  data() {
    return {
      index: 0,
      minMoveDistance: "20%",
      autoplayTime: 3000,
      loop: true,
      list: [1, 2, 3, 4],
      pagination: true,
      userPagination: false,
      isPageStyle1: true,
    };
  },
  mounted() {
    window.onresize = () => {
      this.$refs.swiper.reset();
    };
  },
  methods: {
    add() {
      this.list.push(this.list.length + 1);
      this.$nextTick(() => {
        this.$refs.swiper.reset();
      });
    },
    update() {
      this.list = ["a", "b", "c", "d"];
      this.$nextTick(() => {
        this.$refs.swiper.reset();
      });
    },
    onTouchStart() {
      console.log("onTouchStart:");
    },
    onTouchMove(e, direction) {
      // 禁用水平方向滑动
      if (direction === "horizontal") {
        e.preventDefault();
      }
      console.log(`onTouchMove:${direction}`);
    },
    onTouchEnd() {
      console.log("onTouchEnd:");
    },
    slideClick(item, idx) {
      console.log(`slideClick: index-${idx}, item-${item}`);
    },
    slideChangeTransitionStart(idx) {
      console.log(`slideChangeTransitionStart:${idx}`);
    },
    slideChangeTransitionEnd(idx) {
      console.log(`slideChangeTransitionEnd:${idx}`);
    },
  },
};
</script>
<style lang="scss">
.oc-swiper {
  height: 200px;
  .slide-item {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #99a9bf;
    font-size: 30px;
    font-weight: bold;
  }
}
.user-pagination {
  position: relative;
  ul {
    margin-top: -24px;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    li {
      color: #fff;
      background: rgba(#000, 0.4);
      width: 20px;
      height: 20px;
      text-align: center;
      line-height: 20px;
      font-size: 12px;
      border-radius: 50%;
      display: inline-block;
      margin: 0 4px;
      &.cur-page {
        background: #007aff;
      }
    }
  }
  .page-2 {
    margin-top: -24px;
    text-align: center;
    color: #000;
  }
}
</style>
<style lang="scss" scoped>
.demo-swiper-page {
  margin: 10px;
  .demo {
    margin-bottom: 10px;
  }
  .g-btn {
    margin-bottom: 10px;
  }
}
</style>
赞(3)
未经允许不得转载:工具盒子 » Vue实现移动端轮播swiper组件