File size: 8,578 Bytes
89ce340 |
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 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
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 '@/utils/message'
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 })
}
}
onMounted(() => {
const firstAnimations = formatedAnimations.value[0]
if (firstAnimations && firstAnimations.animations.length) {
const autoExecFirstAnimations = firstAnimations.animations.every(item => item.trigger === 'auto' || item.trigger === 'meantime')
if (autoExecFirstAnimations) runAnimation()
}
})
// 撤销元素动画,除了将索引前移外,还需要清除动画状态
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 loopPlay = ref(false)
const setLoopPlay = (loop: boolean) => {
loopPlay.value = loop
}
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
}
else {
if (loopPlay.value) turnSlideToIndex(slides.value.length - 1)
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 {
if (loopPlay.value) turnSlideToIndex(0)
else {
throttleMassage('已经是最后一页了')
closeAutoPlay()
}
inAnimation.value = false
}
}
// 自动播放
const autoPlayInterval = ref(2500)
const autoPlay = () => {
closeAutoPlay()
message.success('开始自动放映')
autoPlayTimer.value = setInterval(execNext, autoPlayInterval.value)
}
const setAutoPlayInterval = (interval: number) => {
closeAutoPlay()
autoPlayInterval.value = interval
autoPlay()
}
// 鼠标滚动翻页
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 || key === KEYS.PAGEUP) execPrev()
else if (
key === KEYS.DOWN ||
key === KEYS.RIGHT ||
key === KEYS.SPACE ||
key === KEYS.ENTER ||
key === KEYS.PAGEDOWN
) 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,
autoPlayInterval,
setAutoPlayInterval,
autoPlay,
closeAutoPlay,
loopPlay,
setLoopPlay,
mousewheelListener,
touchStartListener,
touchEndListener,
turnPrevSlide,
turnNextSlide,
turnSlideToIndex,
turnSlideToId,
execPrev,
execNext,
animationIndex,
}
}
|