mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
235 lines
7.5 KiB
TypeScript
235 lines
7.5 KiB
TypeScript
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,
|
|
}
|
|
} |