TS自动轮播图

App开发中经常用到这种轮播图组件。

 

 

最近在做Vue商城类的应用时正好用到,整理记录一下一遍后续使用。主要逻辑就是通过定时器间隔一定时间移动显示内容到对应位置改变offset,需要特殊处理的地方是滚动到最后一页时,把首页拼接到后边,下一次滚动时滚到第一页然后重置,形成循环往复自动播放。本组件还添加了处理手动滑动以及添加页码

主要逻辑代码 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import { useChildren } from '@/use/useChildren'
import { doubleRaf } from '@/utils/raf'
import { clamp, createNamespace } from 'vant/lib/utils'
import { ref, defineComponent, computed, reactive, onMounted, onBeforeUnmount } from 'vue'
import './OpSwipe.scss'
//import OpSwipeItem from './OpSwipeItem'
import { useTouch } from '@/use/useTouch'
 
const [name, bem] = createNamespace('swipe')
 
export const SWIPE_KEY = Symbol('swipe')
 
export type SwipeState = {
  rect: { width: number; height: number } | null
  width: number
  height: number
  offset: number
  active: number
  swiping: boolean
}
 
export default defineComponent({
  name,
  props: {
    //是否自动播放
    autoplay: {
      type: Number,
      default: 0,
    },
    //时间间隔
    duration: {
      type: Number,
      default: 1000,
    },
    //是否循环播放
    loop: {
      type: Boolean,
      default: true,
    },
    //是否展示页码
    showIndicators: {
      type: Boolean,
      default: true,
    },
    //方向 水平还是数值方向
    vertical: {
      type: Boolean,
      defalut: false,
    },     //滚动方向是否正方向(下/右为正)
     forward: {
        type: Boolean,
        defalut: false,
     },
  },
  setup(props, { slots }) {
    const root = ref()
    const track = ref()
    const state = reactive<SwipeState>({
      rect: null,
      offset: 0,
      width: 0,
      height: 0,
      active: 0,
      swiping: false,
    })
 
    const { children, linkChildren } = useChildren(SWIPE_KEY)
    const count = computed(() => children.length)
    const size = computed(() => state[props.vertical ? 'height' : 'width'])
    const trackSize = computed(() => count.value * size.value)
    const firstChild = computed(() => track.value.children[0])
    const lastChild = computed(() => track.value.children[count.value - 1])    const pStyle = computed(() => {
      const x = props.vertical ? 'bottom' : 'left'
      const y = props.vertical ? 'right' : 'bottom'
      const style = {
        Position: 'absolute',
        [x]: '50%',
        [y]: '10px',
        transform: `translate${props.vertical ? 'Y' : 'X'}(-50%)`,
        display: 'flex',
        FlexDirection: `${props.vertical ? 'column' : 'row'}`,
      }
      return style
 
    })
const trackStyle = computed(() => {
      const mainAxis = props.vertical ? 'height' : 'width'
      const style = {
        transform: `translate${props.vertical ? 'Y' : 'X'}(${state.offset}px)`,
        transitionDuration: `${state.swiping ? 0 : props.duration}ms`,
        [mainAxis]: `${trackSize.value}px`,        display: 'flex',
        FlexDirection: `${props.vertical ? 'column' : 'row'}`,<br><br>      }return style
    })
 
    //获取下一页对应页码,pace移动几页
    const getTargetActive = (pace: number) => {
      const active = state.active
      if (pace) {
        if (props.loop) {
          return clamp(active + pace, -1, count.value)
        } else {
          return clamp(active + pace, 0, count.value - 1)
        }
      }
      return active
    }
    //获取下一页对应的偏移距离
    const getTargetOffset = (active: number, offset: number) => {
      const position = active * size.value
      const targetOffset = offset - position
      return targetOffset
    }
    //最小偏移距离
    const minOffset = computed(() => {
      if (state.rect) {
        const base = props.vertical ? state.rect.height : state.rect.width
        return base - trackSize.value
      }
      return 0
    })
    //移动到下一页
    const move = ({ pace = 0, offset = 0 }) => {
      if (count.value > 1) {
        const targetActive = getTargetActive(pace)
        const targetOffset = getTargetOffset(targetActive, offset)
 
        if (props.loop) {         // 正向滚动,从右向左
          if (children[0] && targetOffset !== minOffset.value) {
            const outRightBound = targetOffset < minOffset.value
            //把第一个元素复原offset 从-size*count 变成0
            children[0].setOffset(outRightBound ? trackSize.value : 0)
          }
          // 反向滚动,从左向右
          const last = children[count.value - 1]
          if(last && targetOffset !== 0) {
            const onLeftBound = targetOffset > 0
            last.setOffset(onLeftBound ? -trackSize.value : 0)
          }
        }<em id="__mceDel">        state.active = targetActive
        state.offset = targetOffset //改变offset触发滚动
      }
    }
    const correctPositon = () => {
      state.swiping = true
      //如果超出页码范围返回首页初始位置,形成循环播放
      if (state.active < 0) {
        move({ pace: count.value })
      } else if (state.active >= count.value) {
        move({ pace: -count.value })
      }
    }
 
    const next = () => {
      correctPositon()
      doubleRaf(() => {
        state.swiping = false
        move({ pace: props.forward ? -1 : 1 })</em>      })
    }
 
    let timer: number
    const stopAutoplay = () => {
      clearTimeout(timer)
    }
    const autoplay = () => {
      stopAutoplay()
      if (props.autoplay > 0 && count.value > 1) {
        timer = setTimeout(() => {
          next()
          autoplay()
        }, props.autoplay)
      }
    }
    const init = () => {
      if (!root.value) {
        return
      }
      const rect = {
        width: root.value?.offsetWidth,
        height: root.value?.offsetHeight,
      }
      state.rect = rect
      state.width = rect.width
      state.height = rect.height
      autoplay()
    }
 
 
    linkChildren({
      size,
      props,
    })
    onMounted(init)
    onBeforeUnmount(stopAutoplay)<br><br>watch(() => props.autoplay, autoplay)
    <br>    return () => (
      <div ref={root} class={bem()}>
        <div
          ref={track}
          style={trackStyle.value}
          class={bem('track')}
          onTouchstart={onTouchStart}
          onTouchmove={onTouchMove}
          onTouchend={onTouchEnd}
        >
          {slots.default?.()}
        </div>
        {renderIndicator()}
      </div>
    )
  },
})
          

 

 

 

 添加手势滑动处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//对滑动手势的一些处理,主要是获取滑动距离位置等的封装
    const touch = useTouch()
    const delta = computed(() => (props.vertical ? touch.deltaY.value : touch.deltaX.value))
    let touchStartTime: number
    const onTouchStart = (evevt: TouchEvent) => {
      touch.start(evevt)
      touchStartTime = Date.now()
      //停止制动播放
      stopAutoplay()
      correctPositon()
    }
    //触发手势滑动
    const onTouchMove = (event: TouchEvent) => {
      touch.move(event)
 
      event.preventDefault()
      move({ offset: delta.value })
    }
    //手势滑动结束时决定是否滚到下一下
    const onTouchEnd = () => {
      const duration = Date.now() - touchStartTime
      const speed = delta.value / duration
      const shouldSwipe = Math.abs(speed) > 0.25 || Math.abs(delta.value) > size.value / 2
      if (shouldSwipe) {
        const offset = props.vertical ? touch.offsetY.value : touch.offsetX.value
        let pace = 0
        if (props.loop) {
          pace = offset > 0 ? (delta.value > 0 ? -1 : 1) : 0
        } else {
          pace = -Math[delta.value > 0 ? 'ceil' : 'floor'](delta.value / size.value)
        }
        move({ pace: pace })
      } else {
        move({ pace: 0 })
      }
 
      state.swiping = false
      autoplay()
    }

 添加页码

1
2
3
4
5
6
7
8
9
//页码    const renderDot = (_: string, index: number) => {
      const active = index === activeIndicator.value
      return <i class={bem('indicator', { active })}> </i>
    }
    const renderIndicator = () => {
      if (props.showIndicators) {
        return <div class={bem('indicators') style={pStyle.value}<em id="__mceDel">}>{Array(count.value).fill('').map(renderDot)}</div></em><em id="__mceDel">      }
    }
</em>

const activeIndicator = computed(() => {

      const num = state.active % count.value

      return num >= 0 ? num : count.value - 1

    })

 封装手势移动距离工具useTouch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import { ref } from 'vue'
//垂直和水平,哪个方向移动距离大算哪个
const getDirection = (x: number, y: number) => {
  if (x > y) {
    return 'horizontal'
  }
  if (y > x) {
    return 'vertical'
  }
  return ''
}
 
export function useTouch() {
  const startX = ref(0)
  const startY = ref(0)
  //移动的水平距离(有正负)
  const deltaX = ref(0)
  const deltaY = ref(0)
  //距离绝对值
  const offsetX = ref(0)
  const offsetY = ref(0)
  const direction = ref('')
  const isVertical = () => direction.value === 'vertical'
  const isHorizontal = () => direction.value === 'horizontal'
 
  const reset = () => {
    deltaX.value = 0
    deltaY.value = 0
    offsetX.value = 0
    offsetY.value = 0
  }
 
  const start = (event: TouchEvent) => {
    reset()
    startX.value = event.touches[0].clientX
    startY.value = event.touches[0].clientY
  }
  const move = (event: TouchEvent) => {
    const touch = event.touches[0]
 
    deltaX.value = (touch.clientX < 0 ? 0 : touch.clientX) - startX.value
    deltaY.value = touch.clientY - startY.value
    offsetX.value = Math.abs(deltaX.value)
    offsetY.value = Math.abs(deltaY.value)
 
    const LOCK_DIRECTION_DISTANCE = 10
    if (
      !direction.value ||
      (offsetX.value < LOCK_DIRECTION_DISTANCE && offsetY.value < LOCK_DIRECTION_DISTANCE)
    ) {
      direction.value = getDirection(offsetX.value, offsetY.value)
    }
  }
 
  return {
    move,
    start,
    reset,
    startX,
    startY,
    deltaX,
    deltaY,
    offsetX,
    offsetY,
    direction,
    isVertical,
    isHorizontal,
  }
}

 通过父子组件自动添加,父组件获取子组件数组确定轮播图数量,子组件获取轮播图size大小

useParent代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import type { InjectionKey } from 'vue'
import type { Child } from './useChildren'
import { inject, getCurrentInstance, onUnmounted } from 'vue'
 
export type ParentProvide = {
  link(instance: Child): void
  unlink(instance: Child): void
  [key: string]: any
}
 
export function useParent(key: InjectionKey<ParentProvide>) {
  //为子组件注入父组件提供的属性
  const parent = inject(key, null)
 
  if (!parent) {
    return {
      parent: null,
    }
  }
  //当前的子组件 加入到数组中
  const instance = getCurrentInstance()
  const { link, unlink } = parent
  link(instance)
  //生命周期结束时 从数组中移除,防止内存泄漏
  onUnmounted(() => unlink(instance))
 
  return {
    parent,
  }
}

useChildren代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import type { ComponentInternalInstance, InjectionKey, Ref } from 'vue'
import type { ParentProvide } from './useParent'
import { reactive, provide } from 'vue'
 
export type NotNullChild = ComponentInternalInstance & Record<string, any>
export type Child = NotNullChild | null
 
export function useChildren(key: InjectionKey<ParentProvide>) {
  const children = reactive<Child[]>([])
 
  const linkChildren = (value?: any) => {
    const link = (child: Child) => {
      children.push(child)
    }
 
    const unlink = (child: Child) => {
      const index = children.indexOf(child)
      children.splice(index, 1)
    }
    //提供注入
    provide(key, {
      link,
      unlink,
      ...value, //把value对象所有属性添加进去
    })
  }
 
  return {
    children,
    linkChildren,
  }
}

 子组件逻辑代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { useParent } from '@/use/useParent'
import { createNamespace } from '@/utils/create'
 
import { computed, defineComponent, type CSSProperties } from 'vue'
import { SWIPE_KEY } from './OpSwipe'
import { useExpose } from '@/use/useExpose'
 
const [name, bem] = createNamespace('swipe-item')
 
export default defineComponent({
  name,
  //props: {},
  setup(props, { slots }) {
 
    const { parent } = useParent(SWIPE_KEY)
 
    const style = computed(() => {
      const style: CSSProperties = {}
      style['width'] = '100px'
 
      if (parent) {
        if (parent.size.value) {
          style[parent.vertical ? 'height' : 'width'] = `${parent.size.value}px`
        }
      }
      return style
    })
 
    return () => (
      <div class={bem()} style={style.value}>
        {slots.default?.()}
      </div>
    )
  },
})

 

完整代码地址:http://github.com/duzhaoquan/ele-h5.git

posted @   不停奔跑的蜗牛  阅读(91)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
点击右上角即可分享
微信分享提示