useExecPlay.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import { onMounted, onUnmounted, ref } from "vue"
  2. import { throttle } from "lodash"
  3. import { storeToRefs } from "pinia"
  4. import { useSlidesStore, useScreenStore, useMainStore } from "@/store"
  5. import { KEYS } from "@/configs/hotkey"
  6. import { ANIMATION_CLASS_PREFIX } from "@/configs/animation"
  7. import message from "@/utils/message"
  8. import { changePageSlideMes } from "@/messageHooks/pptScreen"
  9. export default () => {
  10. const slidesStore = useSlidesStore()
  11. const mainStore = useMainStore()
  12. const { slides, slideIndex, formatedAnimations } = storeToRefs(slidesStore)
  13. const { isPPTWheelPage } = storeToRefs(mainStore)
  14. // 当前页的元素动画执行到的位置
  15. const animationIndex = ref(0)
  16. // 动画执行状态
  17. const inAnimation = ref(false)
  18. // 最小已播放页面索引
  19. const playedSlidesMinIndex = ref(slideIndex.value)
  20. // 执行元素动画
  21. const runAnimation = () => {
  22. // 正在执行动画时,禁止其他新的动画开始
  23. if (inAnimation.value) return
  24. const { animations, autoNext } = formatedAnimations.value[animationIndex.value]
  25. animationIndex.value += 1
  26. // 标记开始执行动画
  27. inAnimation.value = true
  28. let endAnimationCount = 0
  29. // 依次执行该位置中的全部动画
  30. for (const animation of animations) {
  31. const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
  32. if (!elRef) {
  33. endAnimationCount += 1
  34. continue
  35. }
  36. const animationName = `${ANIMATION_CLASS_PREFIX}${animation.effect}`
  37. // 执行动画前先清除原有的动画状态(如果有)
  38. elRef.style.removeProperty("--animate-duration")
  39. for (const classname of elRef.classList) {
  40. if (classname.indexOf(ANIMATION_CLASS_PREFIX) !== -1) elRef.classList.remove(classname, `${ANIMATION_CLASS_PREFIX}animated`)
  41. }
  42. // 执行动画
  43. elRef.style.setProperty("--animate-duration", `${animation.duration}ms`)
  44. elRef.classList.add(animationName, `${ANIMATION_CLASS_PREFIX}animated`)
  45. // 执行动画结束,将“退场”以外的动画状态清除
  46. const handleAnimationEnd = () => {
  47. if (animation.type !== "out") {
  48. elRef.style.removeProperty("--animate-duration")
  49. elRef.classList.remove(animationName, `${ANIMATION_CLASS_PREFIX}animated`)
  50. }
  51. // 判断该位置上的全部动画都已经结束后,标记动画执行完成,并尝试继续向下执行(如果有需要)
  52. endAnimationCount += 1
  53. if (endAnimationCount === animations.length) {
  54. inAnimation.value = false
  55. if (autoNext) runAnimation()
  56. }
  57. }
  58. elRef.addEventListener("animationend", handleAnimationEnd, { once: true })
  59. }
  60. }
  61. onMounted(() => {
  62. const firstAnimations = formatedAnimations.value[0]
  63. if (firstAnimations && firstAnimations.animations.length) {
  64. const autoExecFirstAnimations = firstAnimations.animations.every(item => item.trigger === "auto" || item.trigger === "meantime")
  65. if (autoExecFirstAnimations) runAnimation()
  66. }
  67. })
  68. // 撤销元素动画,除了将索引前移外,还需要清除动画状态
  69. const revokeAnimation = () => {
  70. animationIndex.value -= 1
  71. const { animations } = formatedAnimations.value[animationIndex.value]
  72. for (const animation of animations) {
  73. const elRef: HTMLElement | null = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
  74. if (!elRef) continue
  75. elRef.style.removeProperty("--animate-duration")
  76. for (const classname of elRef.classList) {
  77. if (classname.indexOf(ANIMATION_CLASS_PREFIX) !== -1) elRef.classList.remove(classname, `${ANIMATION_CLASS_PREFIX}animated`)
  78. }
  79. }
  80. // 如果撤销时该位置有且仅有强调动画,则继续执行一次撤销
  81. if (animations.every(item => item.type === "attention")) execPrev()
  82. }
  83. // 关闭自动播放
  84. const autoPlayTimer = ref(0)
  85. const closeAutoPlay = () => {
  86. if (autoPlayTimer.value) {
  87. clearInterval(autoPlayTimer.value)
  88. autoPlayTimer.value = 0
  89. }
  90. }
  91. onUnmounted(closeAutoPlay)
  92. // 循环放映
  93. const loopPlay = ref(false)
  94. const setLoopPlay = (loop: boolean) => {
  95. loopPlay.value = loop
  96. }
  97. const throttleMassage = throttle(
  98. function (msg) {
  99. message.success(msg)
  100. },
  101. 1000,
  102. { leading: true, trailing: false }
  103. )
  104. // 向上/向下播放
  105. // 遇到元素动画时,优先执行动画播放,无动画则执行翻页
  106. // 向上播放遇到动画时,仅撤销到动画执行前的状态,不需要反向播放动画
  107. // 撤回到上一页时,若该页从未播放过(意味着不存在动画状态),需要将动画索引置为最小值(初始状态),否则置为最大值(最终状态)
  108. const execPrev = () => {
  109. if (formatedAnimations.value.length && animationIndex.value > 0) {
  110. revokeAnimation()
  111. } else if (slideIndex.value > 0) {
  112. slidesStore.updateSlideIndex(slideIndex.value - 1)
  113. if (slideIndex.value < playedSlidesMinIndex.value) {
  114. animationIndex.value = 0
  115. playedSlidesMinIndex.value = slideIndex.value
  116. } else animationIndex.value = formatedAnimations.value.length
  117. } else {
  118. if (loopPlay.value) turnSlideToIndex(slides.value.length - 1)
  119. else throttleMassage("已经是第一页了")
  120. }
  121. inAnimation.value = false
  122. }
  123. const execNext = () => {
  124. if (formatedAnimations.value.length && animationIndex.value < formatedAnimations.value.length) {
  125. runAnimation()
  126. } else if (slideIndex.value < slides.value.length - 1) {
  127. slidesStore.updateSlideIndex(slideIndex.value + 1)
  128. animationIndex.value = 0
  129. inAnimation.value = false
  130. } else {
  131. if (loopPlay.value) turnSlideToIndex(0)
  132. else {
  133. throttleMassage("已经是最后一页了")
  134. closeAutoPlay()
  135. }
  136. inAnimation.value = false
  137. }
  138. }
  139. // 自动播放
  140. const autoPlayInterval = ref(2500)
  141. const autoPlay = () => {
  142. closeAutoPlay()
  143. message.success("开始自动放映")
  144. autoPlayTimer.value = setInterval(execNext, autoPlayInterval.value)
  145. }
  146. const setAutoPlayInterval = (interval: number) => {
  147. closeAutoPlay()
  148. autoPlayInterval.value = interval
  149. autoPlay()
  150. }
  151. // 鼠标滚动翻页
  152. const mousewheelListener = throttle(
  153. function (e: WheelEvent) {
  154. // 控制能不能翻页
  155. if (!isPPTWheelPage.value) return
  156. if (e.deltaY < 0) execPrev()
  157. else if (e.deltaY > 0) execNext()
  158. },
  159. 500,
  160. { leading: true, trailing: false }
  161. )
  162. // 触摸屏上下滑动翻页
  163. const touchInfo = ref<{ x: number; y: number } | null>(null)
  164. const touchStartListener = (e: TouchEvent) => {
  165. // 控制能不能翻页
  166. if (!isPPTWheelPage.value) return
  167. touchInfo.value = {
  168. x: e.changedTouches[0].pageX,
  169. y: e.changedTouches[0].pageY
  170. }
  171. }
  172. const touchEndListener = (e: TouchEvent) => {
  173. // 控制能不能翻页
  174. if (!isPPTWheelPage.value) return
  175. if (!touchInfo.value) return
  176. const offsetX = Math.abs(touchInfo.value.x - e.changedTouches[0].pageX)
  177. const offsetY = e.changedTouches[0].pageY - touchInfo.value.y
  178. if (Math.abs(offsetY) > offsetX && Math.abs(offsetY) > 50) {
  179. touchInfo.value = null
  180. if (offsetY > 0) execPrev()
  181. else execNext()
  182. }
  183. }
  184. // 快捷键翻页
  185. const keydownListener = (e: KeyboardEvent) => {
  186. const key = e.key.toUpperCase()
  187. if (key === KEYS.UP || key === KEYS.LEFT || key === KEYS.PAGEUP) execPrev()
  188. else if (key === KEYS.DOWN || key === KEYS.RIGHT || key === KEYS.SPACE || key === KEYS.ENTER || key === KEYS.PAGEDOWN) execNext()
  189. }
  190. onMounted(() => document.addEventListener("keydown", keydownListener))
  191. onUnmounted(() => document.removeEventListener("keydown", keydownListener))
  192. // 切换到上一张/上一张幻灯片(无视元素的入场动画)
  193. const turnPrevSlide = () => {
  194. slidesStore.updateSlideIndex(slideIndex.value - 1)
  195. animationIndex.value = 0
  196. }
  197. const turnNextSlide = () => {
  198. slidesStore.updateSlideIndex(slideIndex.value + 1)
  199. animationIndex.value = 0
  200. }
  201. // 切换幻灯片到指定的页面
  202. const turnSlideToIndex = (index: number) => {
  203. slidesStore.updateSlideIndex(index)
  204. animationIndex.value = 0
  205. }
  206. const turnSlideToId = (id: string) => {
  207. const index = slides.value.findIndex(slide => slide.id === id)
  208. if (index !== -1) {
  209. slidesStore.updateSlideIndex(index)
  210. animationIndex.value = 0
  211. }
  212. }
  213. const screenStore = useScreenStore()
  214. if (["pptScreen", "mobileScreen"].includes(screenStore.mode)) {
  215. // mes 翻页
  216. changePageSlideMes(execPrev, execNext, animationIndex, formatedAnimations)
  217. }
  218. return {
  219. autoPlayTimer,
  220. autoPlayInterval,
  221. setAutoPlayInterval,
  222. autoPlay,
  223. closeAutoPlay,
  224. loopPlay,
  225. setLoopPlay,
  226. mousewheelListener,
  227. touchStartListener,
  228. touchEndListener,
  229. turnPrevSlide,
  230. turnNextSlide,
  231. turnSlideToIndex,
  232. turnSlideToId,
  233. execPrev,
  234. execNext,
  235. animationIndex
  236. }
  237. }