536 lines
18 KiB
Vue
536 lines
18 KiB
Vue
|
<template>
|
|||
|
<view
|
|||
|
class="tn-c-swiper-class tn-c-swiper"
|
|||
|
>
|
|||
|
<!-- 轮播item容器-->
|
|||
|
<view class="tn-swiper__container" :style="[swiperContainerStyle]" :animation="containerAnimation">
|
|||
|
<slot></slot>
|
|||
|
</view>
|
|||
|
|
|||
|
<!-- 轮播指示器-->
|
|||
|
<view v-if="indicator" class="tn-swiper__indicator" :class="[`tn-swiper__indicator--${vertical ? 'vertical' : 'horizontal'}`]" :style="[indicatorStyle]">
|
|||
|
<!-- 方形 -->
|
|||
|
<block v-if="indicatorType === 'rect'">
|
|||
|
<view
|
|||
|
v-for="(item, index) in children.length"
|
|||
|
:key="index"
|
|||
|
class="tn-swiper__indicator__rect"
|
|||
|
:class="[
|
|||
|
`tn-swiper__indicator__rect--${vertical ? 'vertical' : 'horizontal'}`,
|
|||
|
currentIndex === index ? `tn-swiper__indicator__rect--active tn-swiper__indicator__rect--active--${vertical ? 'vertical' : 'horizontal'}` : ''
|
|||
|
]"
|
|||
|
:style="[indicatorPointStyle(index)]"
|
|||
|
></view>
|
|||
|
</block>
|
|||
|
<!-- 点 -->
|
|||
|
<block v-if="indicatorType === 'dot'">
|
|||
|
<view
|
|||
|
v-for="(item, index) in children.length"
|
|||
|
:key="index"
|
|||
|
class="tn-swiper__indicator__dot"
|
|||
|
:class="[
|
|||
|
`tn-swiper__indicator__dot--${vertical ? 'vertical' : 'horizontal'}`,
|
|||
|
currentIndex === index ? `tn-swiper__indicator__dot--active tn-swiper__indicator__dot--active--${vertical ? 'vertical' : 'horizontal'}` : ''
|
|||
|
]"
|
|||
|
:style="[indicatorPointStyle(index)]"
|
|||
|
></view>
|
|||
|
</block>
|
|||
|
<!-- 圆角方形 -->
|
|||
|
<block v-if="indicatorType === 'round'">
|
|||
|
<view
|
|||
|
v-for="(item, index) in children.length"
|
|||
|
:key="index"
|
|||
|
class="tn-swiper__indicator__round"
|
|||
|
:class="[
|
|||
|
`tn-swiper__indicator__round--${vertical ? 'vertical' : 'horizontal'}`,
|
|||
|
currentIndex === index ? `tn-swiper__indicator__round--active tn-swiper__indicator__round--active--${vertical ? 'vertical' : 'horizontal'}` : ''
|
|||
|
]"
|
|||
|
:style="[indicatorPointStyle(index)]"
|
|||
|
></view>
|
|||
|
</block>
|
|||
|
<!-- 序号 -->
|
|||
|
<block v-if="indicatorType === 'number' && !vertical">
|
|||
|
<view class="tn-swiper__indicator__number">{{ currentIndex + 1 }}/{{ children.length }}</view>
|
|||
|
</block>
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
</template>
|
|||
|
|
|||
|
<script>
|
|||
|
export default {
|
|||
|
name: 'tn-custom-swiper',
|
|||
|
props: {
|
|||
|
// 当前所在的轮播位置
|
|||
|
current: {
|
|||
|
type: Number,
|
|||
|
default: 0
|
|||
|
},
|
|||
|
// 自动切换
|
|||
|
autoplay: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
// 自动切换时间间隔
|
|||
|
interval: {
|
|||
|
type: Number,
|
|||
|
default: 5000
|
|||
|
},
|
|||
|
// 滑动动画时长
|
|||
|
duration: {
|
|||
|
type: Number,
|
|||
|
default: 500
|
|||
|
},
|
|||
|
// 是否采用衔接滑动
|
|||
|
circular: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
// 滑动方向为纵向
|
|||
|
vertical: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
// 显示指示点
|
|||
|
indicator: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
// 指示点类型
|
|||
|
// rect -> 方形 round -> 圆角方形 dot -> 点 number -> 轮播图下标
|
|||
|
indicatorType: {
|
|||
|
type: String,
|
|||
|
default: 'dot'
|
|||
|
},
|
|||
|
// 指示点的位置
|
|||
|
// topLeft \ topCenter \ topRight \ bottomLeft \ bottomCenter \ bottomRight
|
|||
|
indicatorPosition: {
|
|||
|
type: String,
|
|||
|
default: 'bottomCenter'
|
|||
|
},
|
|||
|
// 指示点激活时颜色
|
|||
|
indicatorActiveColor: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
// 指示点未激活时颜色
|
|||
|
indicatorInactiveColor: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
// 前一个轮播的自定义样式
|
|||
|
prevSwiperStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
},
|
|||
|
// 当前轮播的自定义样式
|
|||
|
customSwiperStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
},
|
|||
|
// 后一个轮播的自定义样式
|
|||
|
nextSwiperStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
computed: {
|
|||
|
parentData() {
|
|||
|
return [
|
|||
|
this.duration,
|
|||
|
this.currentIndex,
|
|||
|
this.swiperContainerAnimationFinish,
|
|||
|
this.circular,
|
|||
|
this.vertical,
|
|||
|
this.prevSwiperStyle,
|
|||
|
this.customSwiperStyle,
|
|||
|
this.nextSwiperStyle
|
|||
|
]
|
|||
|
},
|
|||
|
indicatorStyle() {
|
|||
|
let style = {}
|
|||
|
if (this.vertical) {
|
|||
|
if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
|
|||
|
if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
|
|||
|
if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
|
|||
|
if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
|
|||
|
if (this.vertical) {
|
|||
|
style.right = '12rpx'
|
|||
|
style.left = 'auto'
|
|||
|
} else {
|
|||
|
style.top = '12rpx'
|
|||
|
style.bottom = 'auto'
|
|||
|
}
|
|||
|
} else {
|
|||
|
if (this.vertical) {
|
|||
|
style.right = 'auto'
|
|||
|
style.left = '12rpx'
|
|||
|
} else {
|
|||
|
style.top = 'auto'
|
|||
|
style.bottom = '12rpx'
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
|
|||
|
if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
|
|||
|
if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
|
|||
|
if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
|
|||
|
style.top = '12rpx'
|
|||
|
style.bottom = 'auto'
|
|||
|
} else {
|
|||
|
style.top = 'auto'
|
|||
|
style.bottom = '12rpx'
|
|||
|
}
|
|||
|
}
|
|||
|
return style
|
|||
|
},
|
|||
|
indicatorPointStyle() {
|
|||
|
return (index) => {
|
|||
|
let style = {}
|
|||
|
if (index === this.currentIndex && this.indicatorActiveColor !== '') {
|
|||
|
style.backgroundColor = this.indicatorActiveColor
|
|||
|
} else if (this.indicatorInactiveColor !== '') {
|
|||
|
style.backgroundColor = this.indicatorInactiveColor
|
|||
|
}
|
|||
|
return style
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
watch: {
|
|||
|
parentData() {
|
|||
|
if (this.children.length) {
|
|||
|
this.children.forEach((item) => {
|
|||
|
// 判断子组件如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
|
|||
|
typeof(item.updateParentData) === 'function' && item.updateParentData()
|
|||
|
})
|
|||
|
}
|
|||
|
},
|
|||
|
current(nVal, oVal) {
|
|||
|
if (this.currentIndex === nVal) return
|
|||
|
this.currentIndex = nVal > this.children.length ? this.children.length - 1 : nVal
|
|||
|
this.swiperContainerAnimationFinish = false
|
|||
|
// 设置动画过渡时间
|
|||
|
this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
|
|||
|
this.updateSwiperContainerItem(oVal)
|
|||
|
}
|
|||
|
},
|
|||
|
data() {
|
|||
|
return {
|
|||
|
// 清除动画定时器
|
|||
|
clearAnimationTimer: null,
|
|||
|
// 前后衔接执行定时器
|
|||
|
convergeTimer: null,
|
|||
|
// 自动轮播Timer
|
|||
|
autoPlayTimer: null,
|
|||
|
// 当前选中的轮播
|
|||
|
currentIndex: this.current,
|
|||
|
// swiperContainer样式
|
|||
|
swiperContainerStyle: {
|
|||
|
transform: 'translate3d(0px, 0px, 0px)',
|
|||
|
transitionDuration: '0ms'
|
|||
|
},
|
|||
|
// swiperContainer动画
|
|||
|
containerAnimation: {},
|
|||
|
// 滑动动画结束标记
|
|||
|
swiperContainerAnimationFinish: false
|
|||
|
}
|
|||
|
},
|
|||
|
created() {
|
|||
|
this.children = []
|
|||
|
},
|
|||
|
mounted() {
|
|||
|
this.$nextTick(() => {
|
|||
|
const index = this.currentIndex > this.children.length ? this.children.length - 1 : this.currentIndex
|
|||
|
this.updateSwiperContainerStyle(index)
|
|||
|
this.startAutoPlay()
|
|||
|
})
|
|||
|
},
|
|||
|
methods: {
|
|||
|
// 更新全部swiperItem的样式
|
|||
|
updateAllSwiperItemStyle() {
|
|||
|
this.children.forEach((item, index) => {
|
|||
|
typeof(item.updateSwiperItemStyle) === 'function' && item.updateSwiperItemStyle(this.children.length)
|
|||
|
})
|
|||
|
|
|||
|
},
|
|||
|
// 根据swiperIndex更新swiperItemContainer的样式
|
|||
|
updateSwiperContainerStyle(index) {
|
|||
|
if (this.vertical) {
|
|||
|
this.swiperContainerStyle.transform = `translate3d(0px, ${-index * 100}%, 0px)`
|
|||
|
} else {
|
|||
|
this.swiperContainerStyle.transform = `translate3d(${-index * 100}%, 0px, 0px)`
|
|||
|
}
|
|||
|
},
|
|||
|
// 根据传递的值更新swiperItemContainer的位置
|
|||
|
updateSwiperContainerStyleWithValue(value) {
|
|||
|
if (this.vertical) {
|
|||
|
this.swiperContainerStyle.transform = `translate3d(0px, ${(-this.currentIndex * 100) + value * 100}%, 0px)`
|
|||
|
} else {
|
|||
|
this.swiperContainerStyle.transform = `translate3d(${(-this.currentIndex * 100) + value * 100}%, 0px, 0px)`
|
|||
|
}
|
|||
|
},
|
|||
|
// 根据传递的方向更新swiperItemContainer的位置
|
|||
|
updateSwiperContainerStyleWithDirection(direction) {
|
|||
|
const oldCurrent = this.currentIndex
|
|||
|
const childrenLength = this.children.length
|
|||
|
const lastSwiperItemIndex = childrenLength - 1
|
|||
|
this.swiperContainerAnimationFinish = false
|
|||
|
|
|||
|
|
|||
|
// 向后切换一个SwiperItem
|
|||
|
if (direction === 'reset') {
|
|||
|
// 设置动画过渡时间
|
|||
|
this.swiperContainerStyle.transitionDuration = `${this.duration}ms`
|
|||
|
this.updateSwiperContainerStyle(this.currentIndex)
|
|||
|
this.clearAnimationTimer = setTimeout(() => {
|
|||
|
this.clearSwiperContainerAnimation()
|
|||
|
}, this.duration)
|
|||
|
} else if (direction === 'reload') {
|
|||
|
this.clearConvergeSwiperItemTimer()
|
|||
|
this.clearSwiperContainerAnimation()
|
|||
|
this.updateSwiperItemStyle(0)
|
|||
|
this.updateSwiperItemStyle(lastSwiperItemIndex)
|
|||
|
} else {
|
|||
|
if (direction === 'left' || direction === 'up') {
|
|||
|
if (oldCurrent === childrenLength - 1 && !this.circular) {
|
|||
|
this.clearSwiperContainerAnimation()
|
|||
|
this.clearConvergeSwiperItemTimer()
|
|||
|
return
|
|||
|
}
|
|||
|
this.currentIndex = oldCurrent + 1 >= childrenLength ? 0 : oldCurrent + 1
|
|||
|
} else if (direction === 'right' || direction === 'down') {
|
|||
|
if (oldCurrent === 0 && !this.circular) {
|
|||
|
this.clearSwiperContainerAnimation()
|
|||
|
this.clearConvergeSwiperItemTimer()
|
|||
|
return
|
|||
|
}
|
|||
|
this.currentIndex = oldCurrent - 1 < 0 ? childrenLength - 1 : oldCurrent - 1
|
|||
|
}
|
|||
|
// 设置动画过渡时间
|
|||
|
this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
|
|||
|
// this.updateSwiperItemContainerRect(this.currentIndex)
|
|||
|
}
|
|||
|
|
|||
|
// console.log(direction, oldCurrent, this.currentIndex);
|
|||
|
this.updateSwiperContainerItem(oldCurrent)
|
|||
|
|
|||
|
// 切换轮播时触发事件
|
|||
|
this.$emit('change', {
|
|||
|
current: this.currentIndex
|
|||
|
})
|
|||
|
},
|
|||
|
// 设置自动轮播
|
|||
|
startAutoPlay() {
|
|||
|
if (this.autoplay && !this.autoPlayTimer && this.circular) {
|
|||
|
this.autoPlayTimer = setInterval(() => {
|
|||
|
this.updateSwiperContainerStyleWithDirection('left')
|
|||
|
}, this.interval)
|
|||
|
}
|
|||
|
},
|
|||
|
// 停止自动轮播
|
|||
|
stopAutoPlay() {
|
|||
|
if (this.autoPlayTimer) {
|
|||
|
clearInterval(this.autoPlayTimer)
|
|||
|
this.autoPlayTimer = null
|
|||
|
}
|
|||
|
},
|
|||
|
// 更新swiperContainer和swiperItem相关联信息
|
|||
|
updateSwiperContainerItem(oldCurrent) {
|
|||
|
const childrenLength = this.children.length
|
|||
|
const lastSwiperItemIndex = childrenLength - 1
|
|||
|
// 判断当前是否为头尾,如果是更新对应的头尾SwiperItem样式
|
|||
|
// 更新swiperItemContainer的样式
|
|||
|
if (oldCurrent === 0 && this.currentIndex === lastSwiperItemIndex) {
|
|||
|
// 先移动到最左边然后再去除动画偏移到正常的位置
|
|||
|
// this.swiperContainerStyle.transform = `translate3d(100%, 0px, 0px)`
|
|||
|
this.updateSwiperContainerStyle(-1)
|
|||
|
this.clearSwiperContainerAnimationTimer()
|
|||
|
this.clearAnimationTimer = setTimeout(() => {
|
|||
|
this.convergeSwiperItem()
|
|||
|
}, this.duration)
|
|||
|
} else if (oldCurrent === lastSwiperItemIndex && this.currentIndex === 0) {
|
|||
|
// 先移动到最右边然后再去除动画偏移到正常的位置
|
|||
|
// this.swiperContainerStyle.transform = `translate3d(${-childrenLength * 100}%, 0px, 0px)`
|
|||
|
this.updateSwiperContainerStyle(childrenLength)
|
|||
|
this.clearSwiperContainerAnimationTimer()
|
|||
|
this.clearAnimationTimer = setTimeout(() => {
|
|||
|
this.convergeSwiperItem()
|
|||
|
}, this.duration)
|
|||
|
} else {
|
|||
|
this.updateSwiperContainerStyle(this.currentIndex)
|
|||
|
this.updateSwiperItemStyle(0)
|
|||
|
this.updateSwiperItemStyle(lastSwiperItemIndex)
|
|||
|
this.clearAnimationTimer = setTimeout(() => {
|
|||
|
this.clearSwiperContainerAnimation()
|
|||
|
}, this.duration)
|
|||
|
}
|
|||
|
},
|
|||
|
// 更新对应swiperItem的信息
|
|||
|
updateSwiperItemStyle(index) {
|
|||
|
const childrenLength = this.children.length
|
|||
|
if (index < 0) index = 0
|
|||
|
if (index > childrenLength - 1) index = childrenLength - 1
|
|||
|
|
|||
|
typeof(this.children[index].updateSwiperItemStyle) === 'function' && this.children[index].updateSwiperItemStyle(childrenLength, this.currentIndex)
|
|||
|
},
|
|||
|
// 更新对应swiperItem的容器信息
|
|||
|
updateSwiperItemContainerRect(index) {
|
|||
|
const childrenLength = this.children.length
|
|||
|
if (index < 0) index = 0
|
|||
|
if (index > childrenLength - 1) index = childrenLength - 1
|
|||
|
|
|||
|
typeof(this.children[index].getSwiperItemRect) === 'function' && this.children[index].getSwiperItemRect()
|
|||
|
},
|
|||
|
// 执行前后衔接
|
|||
|
convergeSwiperItem() {
|
|||
|
const lastSwiperItemIndex = this.children.length - 1
|
|||
|
this.clearSwiperContainerAnimation()
|
|||
|
this.clearConvergeSwiperItemTimer()
|
|||
|
this.convergeTimer = setTimeout(() => {
|
|||
|
this.updateSwiperItemStyle(0)
|
|||
|
this.updateSwiperItemStyle(lastSwiperItemIndex)
|
|||
|
this.updateSwiperContainerStyle(this.currentIndex)
|
|||
|
this.clearConvergeSwiperItemTimer()
|
|||
|
}, 30)
|
|||
|
},
|
|||
|
// 停止/清除切换动画
|
|||
|
clearSwiperContainerAnimation() {
|
|||
|
this.swiperContainerStyle.transitionDuration = `0ms`
|
|||
|
this.swiperContainerAnimationFinish = true
|
|||
|
this.clearSwiperContainerAnimationTimer()
|
|||
|
},
|
|||
|
// 停止/清除执行前后衔接定时器
|
|||
|
clearConvergeSwiperItemTimer() {
|
|||
|
if (this.convergeTimer) {
|
|||
|
clearTimeout(this.convergeTimer)
|
|||
|
this.convergeTimer = null
|
|||
|
}
|
|||
|
},
|
|||
|
// 停止/清除切换动画定时器
|
|||
|
clearSwiperContainerAnimationTimer() {
|
|||
|
if (this.clearAnimationTimer) {
|
|||
|
clearTimeout(this.clearAnimationTimer)
|
|||
|
this.clearAnimationTimer = null
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
<style lang="scss" scoped>
|
|||
|
.tn-c-swiper {
|
|||
|
position: relative;
|
|||
|
overflow: hidden;
|
|||
|
width: 100%;
|
|||
|
height: 100%;
|
|||
|
|
|||
|
.tn-swiper {
|
|||
|
&__container {
|
|||
|
width: 100%;
|
|||
|
height: 100%;
|
|||
|
position: absolute;
|
|||
|
top: 0;
|
|||
|
left: 0;
|
|||
|
|
|||
|
will-change: transform;
|
|||
|
transition-property: all;
|
|||
|
transition-timing-function: ease-out;
|
|||
|
}
|
|||
|
|
|||
|
&__indicator {
|
|||
|
position: absolute;
|
|||
|
display: flex;
|
|||
|
z-index: 1;
|
|||
|
|
|||
|
&--horizontal {
|
|||
|
padding: 0 24rpx;
|
|||
|
flex-direction: row;
|
|||
|
width: 100%;
|
|||
|
}
|
|||
|
&--vertical {
|
|||
|
padding: 24rpx 0;
|
|||
|
flex-direction: column;
|
|||
|
height: 100%;
|
|||
|
}
|
|||
|
|
|||
|
&__rect {
|
|||
|
background-color: rgba(0, 0, 0, 0.3);
|
|||
|
transition: all 0.5s;
|
|||
|
|
|||
|
&--horizontal {
|
|||
|
width: 26rpx;
|
|||
|
height: 8rpx;
|
|||
|
}
|
|||
|
&--vertical {
|
|||
|
width: 8rpx;
|
|||
|
height: 26rpx;
|
|||
|
}
|
|||
|
|
|||
|
&--active {
|
|||
|
background-color: rgba(255, 255, 255, 0.8);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
&__dot {
|
|||
|
width: 14rpx;
|
|||
|
height: 14rpx;
|
|||
|
border-radius: 20rpx;
|
|||
|
background-color: rgba(0, 0, 0, 0.3);
|
|||
|
transition: all 0.5s;
|
|||
|
|
|||
|
&--horizontal {
|
|||
|
margin: 0 6rpx;
|
|||
|
}
|
|||
|
&--vertical {
|
|||
|
margin: 6rpx 0;
|
|||
|
}
|
|||
|
|
|||
|
&--active {
|
|||
|
background-color: rgba(255, 255, 255, 0.8);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
&__round {
|
|||
|
width: 14rpx;
|
|||
|
height: 14rpx;
|
|||
|
border-radius: 20rpx;
|
|||
|
background-color: rgba(0, 0, 0, 0.3);
|
|||
|
transition: all 0.5s;
|
|||
|
|
|||
|
&--horizontal {
|
|||
|
margin: 0 6rpx;
|
|||
|
}
|
|||
|
&--vertical {
|
|||
|
margin: 6rpx 0;
|
|||
|
}
|
|||
|
|
|||
|
&--active {
|
|||
|
background-color: rgba(255, 255, 255, 0.8);
|
|||
|
|
|||
|
&--horizontal {
|
|||
|
width: 34rpx;
|
|||
|
}
|
|||
|
&--vertical {
|
|||
|
height: 34rpx;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
&__number {
|
|||
|
padding: 6rpx 16rpx;
|
|||
|
line-height: 1;
|
|||
|
background-color: rgba(0, 0, 0, 0.3);
|
|||
|
color: rgba(255, 255, 255, 0.8);
|
|||
|
border-radius: 100rpx;
|
|||
|
font-size: 26rpx;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
</style>
|