import { onMounted, onUnmounted, ref } from 'vue' import { throttle } from 'lodash' import { storeToRefs } from 'pinia' import { useSlidesStore } from '@/store' import { KEYS } from '@/configs/hotkey' import { ANIMATION_CLASS_PREFIX } from '@/configs/animation' import { message } from 'ant-design-vue' export default () => { const slidesStore = useSlidesStore() const { slides, slideIndex, formatedAnimations } = storeToRefs(slidesStore) // 当前页的元素动画执行到的位置 const animationIndex = ref(0) // 动画执行状态 const inAnimation = ref(false) // 最小已播放页面索引 const playedSlidesMinIndex = ref(slideIndex.value) // 执行元素动画 const runAnimation = () => { // 正在执行动画时,禁止其他新的动画开始 if (inAnimation.value) return const { animations, autoNext } = formatedAnimations.value[animationIndex.value] animationIndex.value += 1 // 标记开始执行动画 inAnimation.value = true let endAnimationCount = 0 // 依次执行该位置中的全部动画 for (const animation of animations) { const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`) if (!elRef) { endAnimationCount += 1 continue } const animationName = `${ANIMATION_CLASS_PREFIX}${animation.effect}` // 执行动画前先清除原有的动画状态(如果有) elRef.style.removeProperty('--animate-duration') for (const classname of elRef.classList) { if (classname.indexOf(ANIMATION_CLASS_PREFIX) !== -1) elRef.classList.remove(classname, `${ANIMATION_CLASS_PREFIX}animated`) } // 执行动画 elRef.style.setProperty('--animate-duration', `${animation.duration}ms`) elRef.classList.add(animationName, `${ANIMATION_CLASS_PREFIX}animated`) // 执行动画结束,将“退场”以外的动画状态清除 const handleAnimationEnd = () => { if (animation.type !== 'out') { elRef.style.removeProperty('--animate-duration') elRef.classList.remove(animationName, `${ANIMATION_CLASS_PREFIX}animated`) } // 判断该位置上的全部动画都已经结束后,标记动画执行完成,并尝试继续向下执行(如果有需要) endAnimationCount += 1 if (endAnimationCount === animations.length) { inAnimation.value = false if (autoNext) runAnimation() } } elRef.addEventListener('animationend', handleAnimationEnd, { once: true }) } } // 撤销元素动画,除了将索引前移外,还需要清除动画状态 const revokeAnimation = () => { animationIndex.value -= 1 const { animations } = formatedAnimations.value[animationIndex.value] for (const animation of animations) { const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`) if (!elRef) continue elRef.style.removeProperty('--animate-duration') for (const classname of elRef.classList) { if (classname.indexOf(ANIMATION_CLASS_PREFIX) !== -1) elRef.classList.remove(classname, `${ANIMATION_CLASS_PREFIX}animated`) } } // 如果撤销时该位置有且仅有强调动画,则继续执行一次撤销 if (animations.every(item => item.type === 'attention')) execPrev() } // 关闭自动播放 const autoPlayTimer = ref(0) const closeAutoPlay = () => { if (autoPlayTimer.value) { clearInterval(autoPlayTimer.value) autoPlayTimer.value = 0 } } onUnmounted(closeAutoPlay) const throttleMassage = throttle(function(msg) { message.success(msg) }, 1000, { leading: true, trailing: false }) // 向上/向下播放 // 遇到元素动画时,优先执行动画播放,无动画则执行翻页 // 向上播放遇到动画时,仅撤销到动画执行前的状态,不需要反向播放动画 // 撤回到上一页时,若该页从未播放过(意味着不存在动画状态),需要将动画索引置为最小值(初始状态),否则置为最大值(最终状态) const execPrev = () => { if (formatedAnimations.value.length && animationIndex.value > 0) { revokeAnimation() } else if (slideIndex.value > 0) { slidesStore.updateSlideIndex(slideIndex.value - 1) if (slideIndex.value < playedSlidesMinIndex.value) { animationIndex.value = 0 playedSlidesMinIndex.value = slideIndex.value } else animationIndex.value = formatedAnimations.value.length inAnimation.value = false } else { throttleMassage('已经是第一页了') inAnimation.value = false } } const execNext = () => { if (formatedAnimations.value.length && animationIndex.value < formatedAnimations.value.length) { runAnimation() } else if (slideIndex.value < slides.value.length - 1) { slidesStore.updateSlideIndex(slideIndex.value + 1) animationIndex.value = 0 inAnimation.value = false } else { throttleMassage('已经是最后一页了') closeAutoPlay() inAnimation.value = false } } // 自动播放 const autoPlay = () => { closeAutoPlay() message.success('开始自动放映') autoPlayTimer.value = setInterval(execNext, 2500) } // 鼠标滚动翻页 const mousewheelListener = throttle(function(e: WheelEvent) { if (e.deltaY < 0) execPrev() else if (e.deltaY > 0) execNext() }, 500, { leading: true, trailing: false }) // 触摸屏上下滑动翻页 const touchInfo = ref<{ x: number; y: number; } | null>(null) const touchStartListener = (e: TouchEvent) => { touchInfo.value = { x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY, } } const touchEndListener = (e: TouchEvent) => { if (!touchInfo.value) return const offsetX = Math.abs(touchInfo.value.x - e.changedTouches[0].pageX) const offsetY = e.changedTouches[0].pageY - touchInfo.value.y if ( Math.abs(offsetY) > offsetX && Math.abs(offsetY) > 50 ) { touchInfo.value = null if (offsetY > 0) execPrev() else execNext() } } // 快捷键翻页 const keydownListener = (e: KeyboardEvent) => { const key = e.key.toUpperCase() if (key === KEYS.UP || key === KEYS.LEFT) execPrev() else if ( key === KEYS.DOWN || key === KEYS.RIGHT || key === KEYS.SPACE || key === KEYS.ENTER ) execNext() } onMounted(() => document.addEventListener('keydown', keydownListener)) onUnmounted(() => document.removeEventListener('keydown', keydownListener)) // 切换到上一张/上一张幻灯片(无视元素的入场动画) const turnPrevSlide = () => { slidesStore.updateSlideIndex(slideIndex.value - 1) animationIndex.value = 0 } const turnNextSlide = () => { slidesStore.updateSlideIndex(slideIndex.value + 1) animationIndex.value = 0 } // 切换幻灯片到指定的页面 const turnSlideToIndex = (index: number) => { slidesStore.updateSlideIndex(index) animationIndex.value = 0 } const turnSlideToId = (id: string) => { const index = slides.value.findIndex(slide => slide.id === id) if (index !== -1) { slidesStore.updateSlideIndex(index) animationIndex.value = 0 } } return { autoPlayTimer, autoPlay, closeAutoPlay, mousewheelListener, touchStartListener, touchEndListener, turnPrevSlide, turnNextSlide, turnSlideToIndex, turnSlideToId, execPrev, execNext, animationIndex, } }