当前位置:   article > 正文

Vue3走马灯(Carousel)_vue3 走马灯

vue3 走马灯

Vue2走马灯(Carousel)

可自定义设置以下属性: 

  • 走马灯图片数组(images),类型:Array<{title: string, src: string , link?: string}>,必传,默认 []

  • 自动滑动轮播间隔(interval),类型:number,单位ms,默认 3000ms

  • 走马灯宽度(width),类型:number|string,默认 '100vw'

  • 走马灯高度(height),类型:number|string,默认 '100vh'

  • 是否显示导航(navigation),类型:boolean,默认 true

  • 导航颜色(navColor),类型:string,默认 '#FFF'

  • 导航大小(navSize),类型:number,单位 px,默认 36

  • 是否显示分页(pagination),类型:boolean,默认 true

  • 分页选中颜色(pageActiveColor),类型:string,默认 '#1677FF'

  • 分页大小(pageSize),单位px,类型:number,默认 10

  • 分页样式(pageStyle),优先级高于 pageSize,类型:CSSProperties,默认 {}

  • 用户操作导航或分页之后,是否禁止自动切换(disableOnInteraction),类型:boolean,默认 true

  • 鼠标悬浮时暂停自动切换,鼠标离开时恢复自动切换(pauseOnMouseEnter),类型:boolean,默认 true

效果如下图:在线预览

注:组件引用方法 import { rafTimeout, cancelRaf } from '../index' 请参考以下博客:使用requestAnimationFrame模拟实现setTimeout和setInterval_theMuseCatcher的博客-CSDN博客使用requestAnimationFrame模拟实现setTimeout和setInterval!https://blog.csdn.net/Dandrose/article/details/130167061

 其中引入组件:Vue3加载(Spin)   

①创建走马灯组件Carousel.vue:

  1. <script setup lang="ts">
  2. import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
  3. import type { CSSProperties } from 'vue'
  4. import { rafTimeout, cancelRaf, requestAnimationFrame, cancelAnimationFrame } from '../index'
  5. import Spin from '../spin'
  6. interface Image {
  7. title?: string // 图片名称
  8. src: string // 图片地址
  9. link?: string // 图片跳转链接
  10. }
  11. interface Props {
  12. images: Image[] // 走马灯图片数组
  13. interval?: number // 自动滑动轮播间隔,单位ms
  14. width?: number|string // 走马灯宽度
  15. height?: number|string // 走马灯高度
  16. navigation?: boolean // 是否显示导航
  17. navColor?: string // 导航颜色
  18. navSize?: number // 导航大小
  19. pagination?: boolean // 是否显示分页
  20. pageActiveColor?: string // 分页选中颜色
  21. pageSize?: number // 分页大小
  22. pageStyle?: CSSProperties // 分页样式,优先级高于pageSize
  23. disableOnInteraction?: boolean // 用户操作导航或分页之后,是否禁止自动切换,默认为true:停止
  24. pauseOnMouseEnter?: boolean // 鼠标悬浮时暂停自动切换,鼠标离开时恢复自动切换,默认true
  25. }
  26. const props = withDefaults(defineProps<Props>(), {
  27. images: () => [],
  28. interval: 3000,
  29. width: '100%',
  30. height: '100vh',
  31. navigation: true,
  32. navColor: '#FFF',
  33. navSize: 36,
  34. pagination: true,
  35. pageActiveColor: '#1677FF',
  36. pageSize: 10,
  37. pageStyle: () => ({}),
  38. disableOnInteraction: true,
  39. pauseOnMouseEnter: true
  40. })
  41. const toLeft = ref(true) // 左滑标志,默认左滑
  42. const left = ref(0) // 滑动偏移值
  43. const transition = ref(false) // 暂停时为完成滑动的过渡标志
  44. const slideTimer = ref() // 轮播切换定时器
  45. const moveRaf = ref() // 滑动效果回调标识
  46. const targetMove = ref() // 目标移动位置
  47. const switched = ref(false) // 是否在进行跳转切换,用于区别箭头或自动切换(false)和跳转切换(true)
  48. const carousel = ref() // DOM引用
  49. const activeSwitcher = ref(1) // 当前展示图片标识
  50. const carouselWidth = computed(() => { // 走马灯区域宽度
  51. if (typeof props.width === 'number') {
  52. return props.width + 'px'
  53. } else {
  54. return props.width
  55. }
  56. })
  57. const carouselHeight = computed(() => { // 走马灯区域高度
  58. if (typeof props.height === 'number') {
  59. return props.height + 'px'
  60. } else {
  61. return props.height
  62. }
  63. })
  64. const totalWidth = computed(() => { // 容器宽度:(图片数组长度+1) * 图片宽度
  65. return (props.images.length + 1) * imageWidth.value
  66. })
  67. const imageCount = computed(() => { // 图片数量
  68. return props.images.length
  69. })
  70. const complete = ref(Array(imageCount.value).fill(false)) // 图片是否加载完成
  71. const fpsRaf = ref() // fps回调标识
  72. const fps = ref(60)
  73. const step = computed(() => { // 移动参数(120fps: 24, 60fps: 12)
  74. if (fps.value === 60) {
  75. return 12
  76. } else {
  77. return 12 * (fps.value / 60)
  78. }
  79. })
  80. function onComplete (index: number) { // 图片加载完成
  81. complete.value[index] = true
  82. }
  83. watch(
  84. () => complete.value[0],
  85. (to) => {
  86. if (to) {
  87. onStart()
  88. }
  89. }
  90. )
  91. function getFPS () { // 获取屏幕刷新率
  92. var start: any = null
  93. function timeElapse (timestamp: number) {
  94. /*
  95. timestamp参数:与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻
  96. */
  97. // console.log('timestamp:', timestamp)
  98. if (!start) {
  99. if (fpsRaf.value > 10) {
  100. start = timestamp
  101. }
  102. fpsRaf.value = requestAnimationFrame(timeElapse)
  103. } else {
  104. fps.value = Math.floor(1000 / (timestamp - start))
  105. console.log('fps', fps.value)
  106. }
  107. }
  108. fpsRaf.value = requestAnimationFrame(timeElapse)
  109. }
  110. const imageWidth = ref() // 图片宽度
  111. const imageHeight = ref() // 图片高度
  112. function getImageSize () {
  113. imageWidth.value = carousel.value.offsetWidth
  114. imageHeight.value = carousel.value.offsetHeight
  115. }
  116. function keyboardSwitch (e: KeyboardEvent) {
  117. e.preventDefault()
  118. if (imageCount.value > 1) {
  119. if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
  120. onLeftArrow()
  121. }
  122. if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
  123. onRightArrow()
  124. }
  125. }
  126. }
  127. onMounted(() => {
  128. getFPS() // 获取浏览器的刷新率
  129. getImageSize() // 获取每张图片大小
  130. // 监听键盘切换事件
  131. document.addEventListener('keydown', keyboardSwitch)
  132. })
  133. onUnmounted(() => {
  134. // 移除键盘切换事件
  135. document.removeEventListener('keydown', keyboardSwitch)
  136. })
  137. function onStart () {
  138. if (imageCount.value > 1 && complete.value[0]) { // 超过一条时滑动
  139. toLeft.value = true // 重置左滑标志
  140. transition.value = false
  141. onAutoSlide() // 自动滑动轮播
  142. console.log('imageSlider start')
  143. }
  144. }
  145. function onStop () {
  146. cancelRaf(slideTimer.value)
  147. slideTimer.value = null
  148. if (toLeft.value) { // 左滑箭头移出时
  149. onStopLeft()
  150. } else {
  151. onStopRight()
  152. }
  153. console.log('imageSlider stop')
  154. }
  155. function onStopLeft () { // 停止往左滑动
  156. cancelAnimationFrame(moveRaf.value)
  157. transition.value = true
  158. left.value = Math.ceil(left.value / imageWidth.value) * imageWidth.value // ceil:向上取整,floor:向下取整
  159. }
  160. function onStopRight () { // 停止往右滑动
  161. cancelAnimationFrame(moveRaf.value)
  162. transition.value = true
  163. left.value = Math.floor(left.value / imageWidth.value) * imageWidth.value // ceil:向上取整,floor:向下取整
  164. }
  165. function onAutoSlide () {
  166. slideTimer.value = rafTimeout(() => {
  167. const target = left.value % (imageCount.value * imageWidth.value) + imageWidth.value
  168. activeSwitcher.value = activeSwitcher.value % imageCount.value + 1
  169. autoMoveLeft(target)
  170. }, props.interval)
  171. }
  172. function goLeft (target: number) { // 点击右箭头,往左滑动
  173. if (toLeft.value) {
  174. onStopLeft()
  175. } else {
  176. onStopRight()
  177. toLeft.value = true // 向左滑动
  178. }
  179. transition.value = false
  180. moveLeft(target)
  181. }
  182. function goRight (target: number) { // 点击左箭头,往右滑动
  183. if (toLeft.value) {
  184. onStopLeft()
  185. toLeft.value = false // 非向左滑动
  186. } else {
  187. onStopRight()
  188. }
  189. transition.value = false
  190. moveRight(target)
  191. }
  192. function onLeftArrow () {
  193. const target = (activeSwitcher.value + imageCount.value - 2) % imageCount.value * imageWidth.value
  194. activeSwitcher.value = (activeSwitcher.value - 1 > 0) ? activeSwitcher.value - 1 : imageCount.value
  195. goRight(target)
  196. }
  197. function onRightArrow () {
  198. const target = activeSwitcher.value * imageWidth.value
  199. activeSwitcher.value = activeSwitcher.value % imageCount.value + 1
  200. goLeft(target)
  201. }
  202. function autoMoveLeftEffect () {
  203. if (left.value >= targetMove.value) {
  204. onAutoSlide() // 自动间隔切换下一张
  205. } else {
  206. var move = Math.ceil((targetMove.value - left.value) / step.value) // 越来越慢的滑动过程
  207. left.value += move
  208. moveRaf.value = requestAnimationFrame(autoMoveLeftEffect)
  209. }
  210. }
  211. function autoMoveLeft (target: number) { // 自动切换,向左滑动效果
  212. if (left.value === imageCount.value * imageWidth.value) { // 最后一张时,重置left
  213. left.value = 0
  214. }
  215. targetMove.value = target
  216. moveRaf.value = requestAnimationFrame(autoMoveLeftEffect)
  217. }
  218. function moveLeftEffect () {
  219. if (left.value >= targetMove.value) {
  220. if (switched.value) { // 跳转切换,完成后自动滑动
  221. switched.value = false
  222. if (!props.disableOnInteraction && !props.pauseOnMouseEnter) {
  223. onStart()
  224. }
  225. }
  226. } else {
  227. var move = Math.ceil((targetMove.value - left.value) / step.value) // 越来越慢的滑动过程
  228. left.value += move
  229. moveRaf.value = requestAnimationFrame(moveLeftEffect)
  230. }
  231. }
  232. function moveLeft (target: number) { // 箭头切换或跳转切换,向左滑动效果
  233. if (left.value === imageCount.value * imageWidth.value) { // 最后一张时,重置left
  234. left.value = 0
  235. }
  236. targetMove.value = target
  237. moveRaf.value = requestAnimationFrame(moveLeftEffect)
  238. }
  239. function moveRightEffect () {
  240. if (left.value <= targetMove.value) {
  241. if (switched.value) { // 跳转切换,完成后自动滑动
  242. switched.value = false
  243. if (!props.disableOnInteraction && !props.pauseOnMouseEnter) {
  244. onStart()
  245. }
  246. }
  247. } else {
  248. var move = Math.floor((targetMove.value - left.value) / step.value) // 越来越慢的滑动过程
  249. left.value += move
  250. moveRaf.value = requestAnimationFrame(moveRightEffect)
  251. }
  252. }
  253. function moveRight (target: number) { // 箭头切换或跳转切换,向右滑动效果
  254. if (left.value === 0) { // 第一张时,重置left
  255. left.value = imageCount.value * imageWidth.value
  256. }
  257. targetMove.value = target
  258. moveRaf.value = requestAnimationFrame(moveRightEffect)
  259. }
  260. function onSwitch (n: number) { // 分页切换图片
  261. if (activeSwitcher.value !== n) {
  262. switched.value = true // 跳转切换标志
  263. const target = (n - 1) * imageWidth.value
  264. if (n < activeSwitcher.value) { // 往右滑动
  265. activeSwitcher.value = n
  266. goRight(target)
  267. }
  268. if (n > activeSwitcher.value) { // 往左滑动
  269. activeSwitcher.value = n
  270. goLeft(target)
  271. }
  272. }
  273. }
  274. </script>
  275. <template>
  276. <div
  277. class="m-slider"
  278. ref="carousel"
  279. :style="`--navColor: ${navColor}; --pageActiveColor: ${pageActiveColor}; width: ${carouselWidth}; height: ${carouselHeight};`"
  280. @mouseenter="pauseOnMouseEnter ? onStop() : () => false"
  281. @mouseleave="pauseOnMouseEnter ? onStart() : () => false">
  282. <div :class="{'transition': transition}" :style="`width: ${totalWidth}px; height: 100%; will-change: transform; transform: translateX(${-left}px);`">
  283. <div class="m-image" v-for="(image, index) in images" :key="index">
  284. <Spin :spinning="!complete[index]" indicator="dynamic-circle">
  285. <a :href="image.link ? image.link:'javascript:;'" :target="image.link ? '_blank':'_self'" class="m-link">
  286. <img @load="onComplete(index)" :src="image.src" :key="image.src" :alt="image.title" class="u-img" :style="`width: ${imageWidth}px; height: ${imageHeight}px;`"/>
  287. </a>
  288. </Spin>
  289. </div>
  290. <div class="m-image" v-if="imageCount">
  291. <Spin :spinning="!complete[0]" indicator="dynamic-circle">
  292. <a :href="images[0].link ? images[0].link:'javascript:;'" :target="images[0].link ? '_blank':'_self'" class="m-link">
  293. <img @load="onComplete(0)" :src="images[0].src" :key="images[0].src" :alt="images[0].title" class="u-img" :style="`width: ${imageWidth}px; height: ${imageHeight}px;`"/>
  294. </a>
  295. </Spin>
  296. </div>
  297. </div>
  298. <template v-if="navigation">
  299. <svg class="arrow-left" :style="`width: ${navSize}px; height: ${navSize}px;`" @click="onLeftArrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10.26 3.2a.75.75 0 0 1 .04 1.06L6.773 8l3.527 3.74a.75.75 0 1 1-1.1 1.02l-4-4.25a.75.75 0 0 1 0-1.02l4-4.25a.75.75 0 0 1 1.06-.04z"></path></svg>
  300. <svg class="arrow-right" :style="`width: ${navSize}px; height: ${navSize}px;`" @click="onRightArrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M5.74 3.2a.75.75 0 0 0-.04 1.06L9.227 8L5.7 11.74a.75.75 0 1 0 1.1 1.02l4-4.25a.75.75 0 0 0 0-1.02l-4-4.25a.75.75 0 0 0-1.06-.04z"></path></svg>
  301. </template>
  302. <div class="m-switch" v-if="pagination">
  303. <div
  304. @click="onSwitch(n)"
  305. :class="['u-circle', {'active': activeSwitcher === n }]"
  306. :style="[{width: `${pageSize}px`, height: `${pageSize}px`}, pageStyle]"
  307. v-for="n in imageCount" :key="n">
  308. </div>
  309. </div>
  310. </div>
  311. </template>
  312. <style lang="less" scoped>
  313. .m-slider {
  314. display: inline-block;
  315. margin: 0 auto;
  316. position: relative;
  317. overflow: hidden;
  318. .transition {
  319. transition: transform .3s ease-out;
  320. }
  321. .m-image {
  322. display: inline-block;
  323. .m-link {
  324. position: relative;
  325. display: block;
  326. height: 100%;
  327. .u-img {
  328. object-fit: cover;
  329. vertical-align: bottom; // 消除img标签底部的5px
  330. cursor: pointer;
  331. }
  332. }
  333. }
  334. &:hover {
  335. .arrow-left {
  336. opacity: .7;
  337. pointer-events: auto;
  338. }
  339. .arrow-right {
  340. opacity: .7;
  341. pointer-events: auto;
  342. }
  343. }
  344. .arrow-left {
  345. position: absolute;
  346. left: 10px;
  347. top: 50%;
  348. transform: translateY(-50%);
  349. fill: var(--navColor);
  350. cursor: pointer;
  351. opacity: 0;
  352. pointer-events: none;
  353. transition: all .3s;
  354. &:hover {
  355. opacity: 1;
  356. }
  357. }
  358. .arrow-right {
  359. position: absolute;
  360. right: 10px;
  361. top: 50%;
  362. transform: translateY(-50%);
  363. fill: var(--navColor);
  364. cursor: pointer;
  365. opacity: 0;
  366. pointer-events: none;
  367. transition: all .3s;
  368. &:hover {
  369. opacity: 1;
  370. }
  371. }
  372. .m-switch {
  373. position: absolute;
  374. bottom: 12px;
  375. left: 50%;
  376. transform: translateX(-50%);
  377. display: flex;
  378. flex-wrap: nowrap;
  379. .u-circle {
  380. background-color: rgba(255, 255, 255, .3);
  381. border-radius: 50%;
  382. margin: 0 4px;
  383. cursor: pointer;
  384. transition: background-color .3s cubic-bezier(.4, 0, .2, 1);
  385. }
  386. .active {
  387. background-color: var(--pageActiveColor) !important;
  388. }
  389. }
  390. }
  391. </style>

②在要使用的页面引入:

  1. <script setup lang="ts">
  2. import Carousel from './Carousel.vue'
  3. import { ref } from 'vue'
  4. const images = ref([
  5. {
  6. title: 'image-1',
  7. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/1.jpg',
  8. link: ''
  9. },
  10. {
  11. title: 'image-2',
  12. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/2.jpg',
  13. link: ''
  14. },
  15. {
  16. title: 'image-3',
  17. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/3.jpg',
  18. link: ''
  19. },
  20. {
  21. title: 'image-4',
  22. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/4.jpg',
  23. link: ''
  24. },
  25. {
  26. title: 'image-5',
  27. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/5.jpg',
  28. link: ''
  29. },
  30. {
  31. title: 'image-6',
  32. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/6.jpg',
  33. link: ''
  34. },
  35. {
  36. title: 'image-7',
  37. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/7.jpg',
  38. link: ''
  39. },
  40. {
  41. title: 'image-8',
  42. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/8.jpg',
  43. link: ''
  44. },
  45. {
  46. title: 'image-9',
  47. src: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.0.3/9.jpg',
  48. link: ''
  49. }
  50. ])
  51. </script>
  52. <template>
  53. <div>
  54. <h2 class="mb10">Carousel 走马灯基本使用</h2>
  55. <Carousel
  56. :images="images"
  57. :width="800"
  58. :height="450" />
  59. <h2 class="mt30 mb10">自定义导航、分页样式</h2>
  60. <Carousel
  61. :images="images"
  62. :width="800"
  63. :height="450"
  64. navColor="#13C2C2"
  65. :navSize="48"
  66. pageActiveColor="#13C2C2"
  67. :pageStyle="{ width: '20px', height: '12px', borderRadius: '12px', backgroundColor: '#DDD' }" />
  68. </div>
  69. </template>
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/250913
推荐阅读
相关标签
  

闽ICP备14008679号