tn-custom-swiper.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. <template>
  2. <view
  3. class="tn-c-swiper-class tn-c-swiper"
  4. >
  5. <!-- 轮播item容器-->
  6. <view class="tn-swiper__container" :style="[swiperContainerStyle]" :animation="containerAnimation">
  7. <slot></slot>
  8. </view>
  9. <!-- 轮播指示器-->
  10. <view v-if="indicator" class="tn-swiper__indicator" :class="[`tn-swiper__indicator--${vertical ? 'vertical' : 'horizontal'}`]" :style="[indicatorStyle]">
  11. <!-- 方形 -->
  12. <block v-if="indicatorType === 'rect'">
  13. <view
  14. v-for="(item, index) in children.length"
  15. :key="index"
  16. class="tn-swiper__indicator__rect"
  17. :class="[
  18. `tn-swiper__indicator__rect--${vertical ? 'vertical' : 'horizontal'}`,
  19. currentIndex === index ? `tn-swiper__indicator__rect--active tn-swiper__indicator__rect--active--${vertical ? 'vertical' : 'horizontal'}` : ''
  20. ]"
  21. :style="[indicatorPointStyle(index)]"
  22. ></view>
  23. </block>
  24. <!-- 点 -->
  25. <block v-if="indicatorType === 'dot'">
  26. <view
  27. v-for="(item, index) in children.length"
  28. :key="index"
  29. class="tn-swiper__indicator__dot"
  30. :class="[
  31. `tn-swiper__indicator__dot--${vertical ? 'vertical' : 'horizontal'}`,
  32. currentIndex === index ? `tn-swiper__indicator__dot--active tn-swiper__indicator__dot--active--${vertical ? 'vertical' : 'horizontal'}` : ''
  33. ]"
  34. :style="[indicatorPointStyle(index)]"
  35. ></view>
  36. </block>
  37. <!-- 圆角方形 -->
  38. <block v-if="indicatorType === 'round'">
  39. <view
  40. v-for="(item, index) in children.length"
  41. :key="index"
  42. class="tn-swiper__indicator__round"
  43. :class="[
  44. `tn-swiper__indicator__round--${vertical ? 'vertical' : 'horizontal'}`,
  45. currentIndex === index ? `tn-swiper__indicator__round--active tn-swiper__indicator__round--active--${vertical ? 'vertical' : 'horizontal'}` : ''
  46. ]"
  47. :style="[indicatorPointStyle(index)]"
  48. ></view>
  49. </block>
  50. <!-- 序号 -->
  51. <block v-if="indicatorType === 'number' && !vertical">
  52. <view class="tn-swiper__indicator__number">{{ currentIndex + 1 }}/{{ children.length }}</view>
  53. </block>
  54. </view>
  55. </view>
  56. </template>
  57. <script>
  58. export default {
  59. name: 'tn-custom-swiper',
  60. props: {
  61. // 当前所在的轮播位置
  62. current: {
  63. type: Number,
  64. default: 0
  65. },
  66. // 自动切换
  67. autoplay: {
  68. type: Boolean,
  69. default: false
  70. },
  71. // 自动切换时间间隔
  72. interval: {
  73. type: Number,
  74. default: 5000
  75. },
  76. // 滑动动画时长
  77. duration: {
  78. type: Number,
  79. default: 500
  80. },
  81. // 是否采用衔接滑动
  82. circular: {
  83. type: Boolean,
  84. default: false
  85. },
  86. // 滑动方向为纵向
  87. vertical: {
  88. type: Boolean,
  89. default: false
  90. },
  91. // 显示指示点
  92. indicator: {
  93. type: Boolean,
  94. default: false
  95. },
  96. // 指示点类型
  97. // rect -> 方形 round -> 圆角方形 dot -> 点 number -> 轮播图下标
  98. indicatorType: {
  99. type: String,
  100. default: 'dot'
  101. },
  102. // 指示点的位置
  103. // topLeft \ topCenter \ topRight \ bottomLeft \ bottomCenter \ bottomRight
  104. indicatorPosition: {
  105. type: String,
  106. default: 'bottomCenter'
  107. },
  108. // 指示点激活时颜色
  109. indicatorActiveColor: {
  110. type: String,
  111. default: ''
  112. },
  113. // 指示点未激活时颜色
  114. indicatorInactiveColor: {
  115. type: String,
  116. default: ''
  117. },
  118. // 前一个轮播的自定义样式
  119. prevSwiperStyle: {
  120. type: Object,
  121. default() {
  122. return {}
  123. }
  124. },
  125. // 当前轮播的自定义样式
  126. customSwiperStyle: {
  127. type: Object,
  128. default() {
  129. return {}
  130. }
  131. },
  132. // 后一个轮播的自定义样式
  133. nextSwiperStyle: {
  134. type: Object,
  135. default() {
  136. return {}
  137. }
  138. }
  139. },
  140. computed: {
  141. parentData() {
  142. return [
  143. this.duration,
  144. this.currentIndex,
  145. this.swiperContainerAnimationFinish,
  146. this.circular,
  147. this.vertical,
  148. this.prevSwiperStyle,
  149. this.customSwiperStyle,
  150. this.nextSwiperStyle
  151. ]
  152. },
  153. indicatorStyle() {
  154. let style = {}
  155. if (this.vertical) {
  156. if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
  157. if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
  158. if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
  159. if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
  160. if (this.vertical) {
  161. style.right = '12rpx'
  162. style.left = 'auto'
  163. } else {
  164. style.top = '12rpx'
  165. style.bottom = 'auto'
  166. }
  167. } else {
  168. if (this.vertical) {
  169. style.right = 'auto'
  170. style.left = '12rpx'
  171. } else {
  172. style.top = 'auto'
  173. style.bottom = '12rpx'
  174. }
  175. }
  176. } else {
  177. if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
  178. if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
  179. if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
  180. if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
  181. style.top = '12rpx'
  182. style.bottom = 'auto'
  183. } else {
  184. style.top = 'auto'
  185. style.bottom = '12rpx'
  186. }
  187. }
  188. return style
  189. },
  190. indicatorPointStyle() {
  191. return (index) => {
  192. let style = {}
  193. if (index === this.currentIndex && this.indicatorActiveColor !== '') {
  194. style.backgroundColor = this.indicatorActiveColor
  195. } else if (this.indicatorInactiveColor !== '') {
  196. style.backgroundColor = this.indicatorInactiveColor
  197. }
  198. return style
  199. }
  200. }
  201. },
  202. watch: {
  203. parentData() {
  204. if (this.children.length) {
  205. this.children.forEach((item) => {
  206. // 判断子组件如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
  207. typeof(item.updateParentData) === 'function' && item.updateParentData()
  208. })
  209. }
  210. },
  211. current(nVal, oVal) {
  212. if (this.currentIndex === nVal) return
  213. this.currentIndex = nVal > this.children.length ? this.children.length - 1 : nVal
  214. this.swiperContainerAnimationFinish = false
  215. // 设置动画过渡时间
  216. this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
  217. this.updateSwiperContainerItem(oVal)
  218. }
  219. },
  220. data() {
  221. return {
  222. // 清除动画定时器
  223. clearAnimationTimer: null,
  224. // 前后衔接执行定时器
  225. convergeTimer: null,
  226. // 自动轮播Timer
  227. autoPlayTimer: null,
  228. // 当前选中的轮播
  229. currentIndex: this.current,
  230. // swiperContainer样式
  231. swiperContainerStyle: {
  232. transform: 'translate3d(0px, 0px, 0px)',
  233. transitionDuration: '0ms'
  234. },
  235. // swiperContainer动画
  236. containerAnimation: {},
  237. // 滑动动画结束标记
  238. swiperContainerAnimationFinish: false
  239. }
  240. },
  241. created() {
  242. this.children = []
  243. },
  244. mounted() {
  245. this.$nextTick(() => {
  246. const index = this.currentIndex > this.children.length ? this.children.length - 1 : this.currentIndex
  247. this.updateSwiperContainerStyle(index)
  248. this.startAutoPlay()
  249. })
  250. },
  251. methods: {
  252. // 更新全部swiperItem的样式
  253. updateAllSwiperItemStyle() {
  254. this.children.forEach((item, index) => {
  255. typeof(item.updateSwiperItemStyle) === 'function' && item.updateSwiperItemStyle(this.children.length)
  256. })
  257. },
  258. // 根据swiperIndex更新swiperItemContainer的样式
  259. updateSwiperContainerStyle(index) {
  260. if (this.vertical) {
  261. this.swiperContainerStyle.transform = `translate3d(0px, ${-index * 100}%, 0px)`
  262. } else {
  263. this.swiperContainerStyle.transform = `translate3d(${-index * 100}%, 0px, 0px)`
  264. }
  265. },
  266. // 根据传递的值更新swiperItemContainer的位置
  267. updateSwiperContainerStyleWithValue(value) {
  268. if (this.vertical) {
  269. this.swiperContainerStyle.transform = `translate3d(0px, ${(-this.currentIndex * 100) + value * 100}%, 0px)`
  270. } else {
  271. this.swiperContainerStyle.transform = `translate3d(${(-this.currentIndex * 100) + value * 100}%, 0px, 0px)`
  272. }
  273. },
  274. // 根据传递的方向更新swiperItemContainer的位置
  275. updateSwiperContainerStyleWithDirection(direction) {
  276. const oldCurrent = this.currentIndex
  277. const childrenLength = this.children.length
  278. const lastSwiperItemIndex = childrenLength - 1
  279. this.swiperContainerAnimationFinish = false
  280. // 向后切换一个SwiperItem
  281. if (direction === 'reset') {
  282. // 设置动画过渡时间
  283. this.swiperContainerStyle.transitionDuration = `${this.duration}ms`
  284. this.updateSwiperContainerStyle(this.currentIndex)
  285. this.clearAnimationTimer = setTimeout(() => {
  286. this.clearSwiperContainerAnimation()
  287. }, this.duration)
  288. } else if (direction === 'reload') {
  289. this.clearConvergeSwiperItemTimer()
  290. this.clearSwiperContainerAnimation()
  291. this.updateSwiperItemStyle(0)
  292. this.updateSwiperItemStyle(lastSwiperItemIndex)
  293. } else {
  294. if (direction === 'left' || direction === 'up') {
  295. if (oldCurrent === childrenLength - 1 && !this.circular) {
  296. this.clearSwiperContainerAnimation()
  297. this.clearConvergeSwiperItemTimer()
  298. return
  299. }
  300. this.currentIndex = oldCurrent + 1 >= childrenLength ? 0 : oldCurrent + 1
  301. } else if (direction === 'right' || direction === 'down') {
  302. if (oldCurrent === 0 && !this.circular) {
  303. this.clearSwiperContainerAnimation()
  304. this.clearConvergeSwiperItemTimer()
  305. return
  306. }
  307. this.currentIndex = oldCurrent - 1 < 0 ? childrenLength - 1 : oldCurrent - 1
  308. }
  309. // 设置动画过渡时间
  310. this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
  311. // this.updateSwiperItemContainerRect(this.currentIndex)
  312. }
  313. // console.log(direction, oldCurrent, this.currentIndex);
  314. this.updateSwiperContainerItem(oldCurrent)
  315. // 切换轮播时触发事件
  316. this.$emit('change', {
  317. current: this.currentIndex
  318. })
  319. },
  320. // 设置自动轮播
  321. startAutoPlay() {
  322. if (this.autoplay && !this.autoPlayTimer && this.circular) {
  323. this.autoPlayTimer = setInterval(() => {
  324. this.updateSwiperContainerStyleWithDirection('left')
  325. }, this.interval)
  326. }
  327. },
  328. // 停止自动轮播
  329. stopAutoPlay() {
  330. if (this.autoPlayTimer) {
  331. clearInterval(this.autoPlayTimer)
  332. this.autoPlayTimer = null
  333. }
  334. },
  335. // 更新swiperContainer和swiperItem相关联信息
  336. updateSwiperContainerItem(oldCurrent) {
  337. const childrenLength = this.children.length
  338. const lastSwiperItemIndex = childrenLength - 1
  339. // 判断当前是否为头尾,如果是更新对应的头尾SwiperItem样式
  340. // 更新swiperItemContainer的样式
  341. if (oldCurrent === 0 && this.currentIndex === lastSwiperItemIndex) {
  342. // 先移动到最左边然后再去除动画偏移到正常的位置
  343. // this.swiperContainerStyle.transform = `translate3d(100%, 0px, 0px)`
  344. this.updateSwiperContainerStyle(-1)
  345. this.clearSwiperContainerAnimationTimer()
  346. this.clearAnimationTimer = setTimeout(() => {
  347. this.convergeSwiperItem()
  348. }, this.duration)
  349. } else if (oldCurrent === lastSwiperItemIndex && this.currentIndex === 0) {
  350. // 先移动到最右边然后再去除动画偏移到正常的位置
  351. // this.swiperContainerStyle.transform = `translate3d(${-childrenLength * 100}%, 0px, 0px)`
  352. this.updateSwiperContainerStyle(childrenLength)
  353. this.clearSwiperContainerAnimationTimer()
  354. this.clearAnimationTimer = setTimeout(() => {
  355. this.convergeSwiperItem()
  356. }, this.duration)
  357. } else {
  358. this.updateSwiperContainerStyle(this.currentIndex)
  359. this.updateSwiperItemStyle(0)
  360. this.updateSwiperItemStyle(lastSwiperItemIndex)
  361. this.clearAnimationTimer = setTimeout(() => {
  362. this.clearSwiperContainerAnimation()
  363. }, this.duration)
  364. }
  365. },
  366. // 更新对应swiperItem的信息
  367. updateSwiperItemStyle(index) {
  368. const childrenLength = this.children.length
  369. if (index < 0) index = 0
  370. if (index > childrenLength - 1) index = childrenLength - 1
  371. typeof(this.children[index].updateSwiperItemStyle) === 'function' && this.children[index].updateSwiperItemStyle(childrenLength, this.currentIndex)
  372. },
  373. // 更新对应swiperItem的容器信息
  374. updateSwiperItemContainerRect(index) {
  375. const childrenLength = this.children.length
  376. if (index < 0) index = 0
  377. if (index > childrenLength - 1) index = childrenLength - 1
  378. typeof(this.children[index].getSwiperItemRect) === 'function' && this.children[index].getSwiperItemRect()
  379. },
  380. // 执行前后衔接
  381. convergeSwiperItem() {
  382. const lastSwiperItemIndex = this.children.length - 1
  383. this.clearSwiperContainerAnimation()
  384. this.clearConvergeSwiperItemTimer()
  385. this.convergeTimer = setTimeout(() => {
  386. this.updateSwiperItemStyle(0)
  387. this.updateSwiperItemStyle(lastSwiperItemIndex)
  388. this.updateSwiperContainerStyle(this.currentIndex)
  389. this.clearConvergeSwiperItemTimer()
  390. }, 30)
  391. },
  392. // 停止/清除切换动画
  393. clearSwiperContainerAnimation() {
  394. this.swiperContainerStyle.transitionDuration = `0ms`
  395. this.swiperContainerAnimationFinish = true
  396. this.clearSwiperContainerAnimationTimer()
  397. },
  398. // 停止/清除执行前后衔接定时器
  399. clearConvergeSwiperItemTimer() {
  400. if (this.convergeTimer) {
  401. clearTimeout(this.convergeTimer)
  402. this.convergeTimer = null
  403. }
  404. },
  405. // 停止/清除切换动画定时器
  406. clearSwiperContainerAnimationTimer() {
  407. if (this.clearAnimationTimer) {
  408. clearTimeout(this.clearAnimationTimer)
  409. this.clearAnimationTimer = null
  410. }
  411. }
  412. }
  413. }
  414. </script>
  415. <style lang="scss" scoped>
  416. .tn-c-swiper {
  417. position: relative;
  418. overflow: hidden;
  419. width: 100%;
  420. height: 100%;
  421. .tn-swiper {
  422. &__container {
  423. width: 100%;
  424. height: 100%;
  425. position: absolute;
  426. top: 0;
  427. left: 0;
  428. will-change: transform;
  429. transition-property: all;
  430. transition-timing-function: ease-out;
  431. }
  432. &__indicator {
  433. position: absolute;
  434. display: flex;
  435. z-index: 1;
  436. &--horizontal {
  437. padding: 0 24rpx;
  438. flex-direction: row;
  439. width: 100%;
  440. }
  441. &--vertical {
  442. padding: 24rpx 0;
  443. flex-direction: column;
  444. height: 100%;
  445. }
  446. &__rect {
  447. background-color: rgba(0, 0, 0, 0.3);
  448. transition: all 0.5s;
  449. &--horizontal {
  450. width: 26rpx;
  451. height: 8rpx;
  452. }
  453. &--vertical {
  454. width: 8rpx;
  455. height: 26rpx;
  456. }
  457. &--active {
  458. background-color: rgba(255, 255, 255, 0.8);
  459. }
  460. }
  461. &__dot {
  462. width: 14rpx;
  463. height: 14rpx;
  464. border-radius: 20rpx;
  465. background-color: rgba(0, 0, 0, 0.3);
  466. transition: all 0.5s;
  467. &--horizontal {
  468. margin: 0 6rpx;
  469. }
  470. &--vertical {
  471. margin: 6rpx 0;
  472. }
  473. &--active {
  474. background-color: rgba(255, 255, 255, 0.8);
  475. }
  476. }
  477. &__round {
  478. width: 14rpx;
  479. height: 14rpx;
  480. border-radius: 20rpx;
  481. background-color: rgba(0, 0, 0, 0.3);
  482. transition: all 0.5s;
  483. &--horizontal {
  484. margin: 0 6rpx;
  485. }
  486. &--vertical {
  487. margin: 6rpx 0;
  488. }
  489. &--active {
  490. background-color: rgba(255, 255, 255, 0.8);
  491. &--horizontal {
  492. width: 34rpx;
  493. }
  494. &--vertical {
  495. height: 34rpx;
  496. }
  497. }
  498. }
  499. &__number {
  500. padding: 6rpx 16rpx;
  501. line-height: 1;
  502. background-color: rgba(0, 0, 0, 0.3);
  503. color: rgba(255, 255, 255, 0.8);
  504. border-radius: 100rpx;
  505. font-size: 26rpx;
  506. }
  507. }
  508. }
  509. }
  510. </style>