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>
|