import { ref, computed } from 'vue' // RAF 封装 const rafFn = (cb) => uni.requestAnimationFrame ? uni.requestAnimationFrame(cb) : setTimeout(cb, 16) const cafFn = (id) => uni.cancelAnimationFrame ? uni.cancelAnimationFrame(id) : clearTimeout(id) export function useSwipe() { const bgOffsetX = ref(0) const screenWidth = ref(375) const tileWidth = ref(375) let rawOffsetX = 0 let touchStartX = 0 let lastMoveX = 0 let lastMoveTime = 0 let velocity = 0 let inertiaRaf = null let isInertiaPhase = false let touchInBanner = false let onTileChange = null const cabinLayerStyle = computed(() => ({ transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)` })) const backgroundStripStyle = computed(() => ({ width: `${tileWidth.value * 3}px`, transform: `translateX(${-tileWidth.value + bgOffsetX.value}px)` })) const clampOffset = (offset) => { const w = tileWidth.value return (((offset % w) + w) % w) - w } const normalizeOffset = (offset) => { const w = tileWidth.value const normalized = clampOffset(offset) const prevTileN = Math.floor(-rawOffsetX / w) rawOffsetX += offset - bgOffsetX.value const nextTileN = Math.floor(-rawOffsetX / w) const delta = nextTileN - prevTileN if (delta !== 0 && onTileChange) { onTileChange(delta, isInertiaPhase) } return normalized } const stopInertia = () => { if (inertiaRaf) { cafFn(inertiaRaf) inertiaRaf = null } isInertiaPhase = false } const scrollPage = (direction) => { stopInertia() if (onTileChange) { onTileChange(direction, false) } const DURATION = 300 const FRAME = 16 const totalFrames = Math.round(DURATION / FRAME) const totalDelta = tileWidth.value * direction * -1 let frame = 0 const step = () => { frame++ const progress = frame / totalFrames const eased = 1 - Math.pow(1 - progress, 3) const prevEased = frame === 1 ? 0 : 1 - Math.pow(1 - (frame - 1) / totalFrames, 3) const delta = totalDelta * (eased - prevEased) bgOffsetX.value = clampOffset(bgOffsetX.value + delta) rawOffsetX += delta if (frame < totalFrames) { inertiaRaf = rafFn(step) } } inertiaRaf = rafFn(step) } const getBannerBottom = () => (screenWidth.value / 750) * 496 const onBgTouchStart = (e) => { const touchY = e.touches[0].clientY touchInBanner = touchY < getBannerBottom() if (touchInBanner) return stopInertia() touchStartX = e.touches[0].clientX lastMoveX = touchStartX lastMoveTime = Date.now() velocity = 0 } const onBgTouchMove = (e) => { if (touchInBanner) return e.preventDefault() const currentX = e.touches[0].clientX const now = Date.now() const dt = now - lastMoveTime || 1 velocity = (currentX - lastMoveX) / dt lastMoveX = currentX lastMoveTime = now bgOffsetX.value = normalizeOffset(bgOffsetX.value + (currentX - touchStartX)) touchStartX = currentX } const onBgTouchEnd = () => { if (touchInBanner) { touchInBanner = false return } touchInBanner = false isInertiaPhase = true const FRICTION = 0.8 const MIN_VELOCITY = 0.2 const step = () => { velocity *= FRICTION if (Math.abs(velocity) < MIN_VELOCITY) { isInertiaPhase = false return } bgOffsetX.value = normalizeOffset(bgOffsetX.value + velocity * 16) inertiaRaf = rafFn(step) } inertiaRaf = rafFn(step) } const onBgTouchCancel = () => { touchInBanner = false stopInertia() velocity = 0 } const initSwipe = ({ screenW, tileW, onTileChangeCallback }) => { screenWidth.value = screenW tileWidth.value = tileW onTileChange = onTileChangeCallback bgOffsetX.value = 0 rawOffsetX = 0 velocity = 0 } const reset = () => { stopInertia() bgOffsetX.value = 0 rawOffsetX = 0 velocity = 0 } return { bgOffsetX, rawOffsetX, velocity, cabinLayerStyle, backgroundStripStyle, scrollPage, stopInertia, initSwipe, reset, onBgTouchStart, onBgTouchMove, onBgTouchEnd, onBgTouchCancel, } }