mySwiper.vue
<template> <div class="swiper-box" ref="swiperBox"> <!-- 滑块主体 --> <div class="swiper-wrapper" :style="{width: width + 'rem', height: oHeight}" @touchstart="startMove" @touchend="endMove" @touchmove="move" ref="sliderBox"> <slot class="swiper-sliber" /> </div> <!-- 按钮 --> <div class="btn-box" v-if="nextBtn" @touchstart="startMove" @touchend="endMove" @touchmove="move"> <p @click.stop="handleLast" :class="{'click': isClickL}"><i class="iconfont icon-arrow-left-s-fill"></i></p> <p @click.stop="handleNext" :class="{'click': isClickR}"><i class="iconfont icon-arrow-right-s-fill"></i> </p> </div> <!-- 分页文字 --> <div v-if="pagingText !== 'none'" class="paging-text-box" :class="{left: pagingPosition === 'left', right: pagingPosition === 'right'}"> <span class="paging">{{pagingText !== 'none'? pagingText : ''}}{{loop ? length-2 + '/' + index : length + '/' + (index + 1)}}</span> </div> <!-- 分页器 --> <div class="hint-list-box" v-if="paging !== 'none'"> <ul class="hint-list" :class="{center: paging === 'center', right: paging === 'right' }"> <li class="item" v-for="item in hintLen" :key="item" :class="{active: (item === index && loop === true) || (!loop && item === index + 1)}"> </li> </ul> </div> </div></template><script> export default { name: 'my-swiper', data() { return { startX: 0, endX: 0, nowLeft: 0, isClickL: false, isClickR: false, length: 0, index: 0, width: 0, resistance: 1, itemWidth: 0, timer: null, autoTimer: null, zoomTimer: null } }, props: { oHeight: { //设置组件高度 type: String, default: 'auto' }, nextBtn: { //是否显示切换按钮默认false type: Boolean, default: false }, loop: { //是否循环播放默认false type: Boolean, default: false }, paging: { // 是否显示分页器默认none不显示 可选 left, center, right type: String, default: 'center' }, pagingText: { // 是否显示页数 默认none不显示 传入字符串为页数文字前缀 type: String, default: 'none' }, pagingPosition: { //页数定位默认center居中 可选left right type: String, default: 'center' }, defaultIndex: { //默认开始轮播索引 默认0 请填写你传入元素长度内的 数字 type: Number, default: 0 }, automation: { // 是否主动轮播 为0时不主动轮播 默认3000 每3000ms轮播一次 type: Number, default: 0 }, zoom: { // 是否启动zoom模式 要配合子组件宽度一起应用 必须在loop模式下开启 type: Boolean, default: false }, retract: { // 是否缩进显示 要配合子组件宽度一起应用 必须在loop模式下开启 type: Boolean, default: false } }, methods: { // 手指点击时触发办法, 次要记录手指要开始滑动时x坐标用于计算滑动完结与开始的差值, 触控时革除 主动轮播与轮播动画的定时器 startMove(e) { clearInterval(this.timer) // 革除轮播动画定时器 clearInterval(this.autoTimer) // 革除主动轮播定时器 this.startX = e.targetTouches[0].clientX // 获取触控时x坐标 this.nowLeft = this.$refs.sliderBox.offsetLeft // 记录以后滑块left值 }, // 手指拖动时触发的,滑动动画 slider(nowX) { if (this.$refs.sliderBox.offsetLeft > 0 && !this.loop) { // 不是循环模式下 在第一个元素向右拖拽时速度衰减 this.resistance += 0.03 window.console.log(55555) this.$refs.sliderBox.style.left = (nowX - this.startX) / this.resistance + 'px' return } else if (this.$refs.sliderBox.offsetLeft < -this.maxLeft && !this.loop) { // 不是循环模式下 在最初一个元素向左拖拽时速度衰减 this.resistance += 0.03 this.$refs.sliderBox.style.left = this.nowLeft + (nowX - this.startX) / this.resistance + 'px' return } this.$refs.sliderBox.style.left = this.nowLeft + (nowX - this.startX) + 'px' }, // 依据子元素计算 滑块宽度 getItemDom() { let itemArr = Array.prototype.slice.call(this.$refs.sliderBox.children, 0) this.length = this.$refs.sliderBox.children.length itemArr.forEach((ele) => { this.width += parseFloat(ele.style.width) }) }, // zoom模式下滑动动画 zoomMove(nowX) { if (nowX - this.startX < 0) { let scale = (-(nowX - this.startX) * 0.001) + 0.8 > 1 ? 1 : (-(nowX - this.startX) * 0.001) + 0.8 let opacity = (-(nowX - this.startX) * 0.001) + 0.5 > 1 ? 1 : (-(nowX - this.startX) * 0.0015) + 0.5 this.$refs.sliderBox.children[this.index + 1].style.transform = 'scale(' + scale + ')' this.$refs.sliderBox.children[this.index + 1].style.opacity = opacity } else { let scale = ((nowX - this.startX) * 0.001) + 0.8 > 1 ? 1 : ((nowX - this.startX) * 0.001) + 0.8 let opacity = ((nowX - this.startX) * 0.001) + 0.5 > 1 ? 1 : ((nowX - this.startX) * 0.0015) + 0.5 this.$refs.sliderBox.children[this.index - 1].style.transform = 'scale(' + scale + ')' this.$refs.sliderBox.children[this.index - 1].style.opacity = opacity } }, // 手指拖动时触发该办法 move(e) { let nowX = e.targetTouches[0].clientX this.slider(nowX) // 调用滑动动画函数 this.zoom && this.zoomMove(nowX) // 调用zoom动画函数 }, // 拖动完结时触发 该办法 endMove(e) { this.endX = e.changedTouches[0].clientX if (this.resistance !== 1) this.resistance = 1 // 判断拖动间隔 是否扭转index if (this.distance > 50) { this.handleLast('touch') } else if (this.distance < -50) { this.handleNext('touch') } else { this.animation(this.$refs.sliderBox, this.target) } // 在主动轮播时 拖动完结重新启动定时器 this.autoTimer = this.automation ? this.handleAuto() : null }, // 切换下一个函数, 次要以扭转index形式切换 handleLast(isTouch) { this.isClickL = true if (!this.loop) { if (!this.index && isTouch !== 'touch') { this.index = this.length - 1 } else if (this.index) { this.index-- } else { this.animation(this.$refs.sliderBox, this.target) } } else { if (this.index === 1) { this.transposition() this.index = this.length - 2 } else { this.index-- } } this.zoom && this.$refs.sliderBox && this.zoomAnimation(this.$refs.sliderBox.children[this.index]) setTimeout(() => { this.isClickL = false }, 300) }, // 切换上一个函数, 次要以扭转index形式切换 handleNext(isTouch) { this.isClickR = true if (!this.loop) { if (this.index === this.length - 1 && isTouch !== 'touch') { this.index = 0 } else if (this.index !== this.length - 1) { this.index++ } else { this.animation(this.$refs.sliderBox, this.target) } } else { if (this.index === this.length - 2) { this.transposition('isLast') this.index = 1 } else { this.index++ } } this.zoom && this.$refs.sliderBox && this.zoomAnimation(this.$refs.sliderBox.children[this.index]) setTimeout(() => { this.isClickR = false }, 300) }, // 切换动画 animation(dom, target) { clearInterval(this.timer) let origin = null let speed = null this.timer = setInterval(() => { origin = dom.offsetLeft speed = (target - origin) / 9 speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed) if (Math.abs(target - dom.offsetLeft) < 2) { dom.style.left = target + 'px' clearInterval(this.timer) return } dom.style.left = origin + speed + 'px' }, 16) this.endX = 0 this.startX = 0 if (this.zoom) { this.isZoomActive() } }, // zoom动画 zoomAnimation(dom) { clearInterval(this.zoomTimer) let originScale = Number(this.getDomTransform(dom, 'scale')) let originOpacity = Number(dom.style.opacity) if (originScale === 1 && originOpacity === 1) { clearInterval(this.zoomTimer) return } let speed = 0.02 this.zoomTimer = setInterval(() => { if (originScale >= 1 && originOpacity >= 1) { clearInterval(this.zoomTimer) return } originScale += speed originOpacity += speed dom.style.transform = originScale > 1 ? `scale(1)` : `scale(${originScale})` dom.style.opacity = originOpacity }, 16) }, // 获取元素transform函数 getDomTransform(dom, type) { let transformStr = dom.style.transform let tarnArr = transformStr.split(' ') let obj = {} tarnArr.forEach(item => { let tempArr = item.split('(') obj[tempArr[0]] = tempArr[1].slice(0, -1) }) return type ? obj[type] : obj }, // loop模式的后期渲染函数, 感觉这里写的不是很漂亮要害是想把,滑块中的每一项以标签模式传进来,没想到更好的办法,如果是数组的模式就要好些 renderLoop() { if (this.loop) { let divArr = Array.prototype.slice.call(this.$refs.sliderBox.children, 0) let len = divArr.length let deviceWidth = document.documentElement.clientWidth let first = divArr[0].cloneNode(true) let last = divArr[len - 1].cloneNode(true) this.$refs.sliderBox.appendChild(first) this.$refs.sliderBox.insertBefore(last, this.$refs.sliderBox.children[0]) setTimeout(() => { this.$refs.sliderBox.style.left = -(this.defaultIndex + 1) * this.$refs.sliderBox.children[0].offsetWidth + (deviceWidth - this.$refs.sliderBox.children[0].offsetWidth)/2 + 'px' window.console.log('-----', this.$refs.sliderBox.style.left) if (this.defaultIndex <= this.length - 1) { this.index = this.defaultIndex + 1 } }) } else { setTimeout(() => { this.$refs.sliderBox.style.left = -this.defaultIndex * this.$refs.sliderBox.children[0].offsetWidth + 'px' this.index = this.defaultIndex }) } this.getItemDom() }, // zoom模式中款式调整 isZoomActive() { if (this.$refs.sliderBox) { for (let i = 0; i < this.$refs.sliderBox.children.length; i++) { if (i !== this.index) { this.$refs.sliderBox.children[i].style.transform = 'scale(0.8)' this.$refs.sliderBox.children[i].style.opacity = '0.5' } else { this.$refs.sliderBox.children[i].style.transform = 'scale(1)' this.$refs.sliderBox.children[i].style.opacity = '1' } } } }, // 循环模式中的换位函数 transposition(isLast) { if (isLast) { if (this.$refs.sliderBox) this.$refs.sliderBox.style.left = this.distance + 'px' return } this.$refs.sliderBox.style.left = -(this.length - 1) * this.$refs.sliderBox.children[0].offsetWidth + this.distance + 'px' }, // 启动主动模式 handleAuto() { if (this.automation) { return (setInterval(() => { this.handleNext() }, this.automation)) } } }, computed: { // 手指触碰与来到的差值 distance() { return this.endX - this.startX }, // 计算滑块目的地 target() { if (this.swiperWidth - this.$refs.sliderBox.children[0].offsetWidth !== 0 && this.retract) { return -this.index * this.$refs.sliderBox.children[0].offsetWidth + (this.swiperWidth - this.$refs.sliderBox.children[0].offsetWidth) / 2 } return -this.index * this.$refs.sliderBox.children[0].offsetWidth }, // 分页个数 hintLen() { return this.loop && this.length ? this.length - 2 : this.length }, // box宽度 swiperWidth() { return this.$refs.swiperBox && this.$refs.swiperBox.offsetWidth }, // 滑块极限值 maxLeft() { return this.$refs.sliderBox && this.$refs.sliderBox.offsetWidth - this.$refs.sliderBox.children[0].offsetWidth } }, watch: { // 监控index变动 index() { this.animation(this.$refs.sliderBox, this.target) } }, mounted() { this.renderLoop() this.autoTimer = this.automation ? this.handleAuto() : null }, beforeDestroy() { clearInterval(this.timer) clearInterval(this.autoTimer) clearInterval(this.zoomTimer) } }</script><style lang="scss" scoped> .swiper-box { width: 100%; overflow: hidden; position: relative; .swiper-wrapper { position: relative; overflow: hidden; display: flex; } .btn-box { position: absolute; width: 100%; top: 50%; display: flex; justify-content: space-between; transform: translateY(-50%); overflow: hidden; p { overflow: hidden; color: rgba(4, 4, 4, .4); background-color: rgba(8, 8, 8, .1); &.click { background-color: rgba(8, 8, 8, .6); .iconfont { color: rgba(4, 4, 4, .6); } } .iconfont { font-size: .8rem; } } } .hint-list-box { position: absolute; width: 100%; bottom: .2rem; .hint-list { display: flex; &.center { justify-content: center; } &.right { justify-content: flex-end; } } .item { width: .2rem; height: .2rem; border-radius: 50%; background-color: #eeeeec; margin-left: .2rem; &.active { background: linear-gradient(#6a3, #4e6); } } } .paging-text-box { position: absolute; top: .1rem; text-align: center; width: 100%; &.right { text-align: right; span { margin-right: 1rem; } } &.left { text-align: left; span { margin-left: 1rem; } } } }</style>
mySwiperItem.vue
<!-- * @Author: yang * @Date: 2020-11-09 07:04:39 * @LastEditors: yang * @LastEditTime: 2020-11-09 10:45:21 * @FilePath: \gloud-h5\src\component\index\mySwiperItem.vue--><template> <div :style="{height: height + 'rem', width: width + 'rem'}" class="item-wrapper" > <slot :ref="'item' + name"></slot> </div></template><script> export default { name: "my-swiper-item", props: { width: { type: Number, required: true }, height: { type: String, default: '4' } } }</script><style scoped></style>
应用的是css预编译less
npm install --save-dev less less-loader css-loader style-loader
应用
index.vue
<template> <div class="show-swiper-wrapper"> <p class="show-toast-title">轮播图展现</p> <div class="item"> <divider>根底调用</divider> <my-swiper> <my-swiper-item :width="7.5" v-for="item in list" :key="item.id" > <img :src="item.img" alt=""> </my-swiper-item> </my-swiper> </div> <div class="item"> <divider>主动轮播</divider> <my-swiper :automation="2000" paging="right"> <my-swiper-item :width="7.5" v-for="item in list" :key="item.id"> <img :src="item.img" alt=""> </my-swiper-item> </my-swiper> </div> <div class="item"> <divider>循环有按钮</divider> <my-swiper :nextBtn="true" :automation="2000" :loop="true" paging="left"> <my-swiper-item :width="7.5" v-for="item in list" :key="item.id"> <img :src="item.img" alt=""> </my-swiper-item> </my-swiper> </div> <div class="item"> <divider>缩进模式</divider> <my-swiper :zoom="true" :retract="true" :automation="3000" :oHeight="'3rem'" eight="3" :loop="true" paging="none"> <my-swiper-item :width="5" height="3" v-for="item in list" :key="item.id"> <img :src="item.img" alt=""> </my-swiper-item> </my-swiper> </div> <divider>查看源码</divider> <my-button class="button-item" @button-click="pushLink" text="查看demo源码" size="lang" /> </div></template><script> import MySwiper from './components/slider/mySwiper' import MySwiperItem from './components/slider/mySwiperItem' import Divider from './components/divider' import MyButton from './components/myButton' export default { name: 'showSwiper', components: { MySwiper, MySwiperItem, Divider, MyButton }, data() { return { // 拿到本地试验要换门路偶 list: [ { img: require('./static/1.jpg'), id: 1 }, { img: require('./static/2.jpg'), id: 2 }, { img: require('./static/3.jpg'), id: 3 }, { img: require('./static/4.jpg'), id: 4 } ] } }, methods: { pushLink() { this.$router.push(`/soundCodeShow${this.$route.fullPath}`) } } }</script><style scoped lang="less"> .show-swiper-wrapper { display: flex; flex-direction: column; align-items: center; position: relative; padding-bottom: 1rem; } .show-swiper-wrapper .item { width: 100%; } .show-swiper-wrapper .item img { width: 100%; height: 100%; }</style>
成果: