index.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <template>
  2. <div class="hamster-ppt-screen">
  3. <div
  4. class="slide-list"
  5. @mousewheel="$event => mousewheelListener($event)"
  6. v-contextmenu="contextmenus"
  7. >
  8. <div
  9. :class="[
  10. 'slide-item',
  11. {
  12. 'show': index === slideIndex,
  13. 'prev': index < slideIndex,
  14. 'next': index > slideIndex,
  15. }
  16. ]"
  17. v-for="(slide, index) in slides"
  18. :key="slide.id"
  19. >
  20. <div
  21. class="slide-content"
  22. :style="{
  23. width: slideWidth + 'px',
  24. height: slideHeight + 'px',
  25. }"
  26. >
  27. <ScreenSlide
  28. :scale="scale"
  29. :slide="slide"
  30. :animationIndex="animationIndex"
  31. />
  32. </div>
  33. </div>
  34. </div>
  35. </div>
  36. </template>
  37. <script lang="ts">
  38. import { computed, defineComponent, onMounted, onUnmounted, Ref, ref } from 'vue'
  39. import { useStore } from 'vuex'
  40. import throttle from 'lodash/throttle'
  41. import { MutationTypes, State } from '@/store'
  42. import { exitFullscreen, isFullscreen } from '@/utils/fullscreen'
  43. import { VIEWPORT_ASPECT_RATIO, VIEWPORT_SIZE } from '@/configs/canvas'
  44. import { KEYS } from '@/configs/hotkey'
  45. import { ContextmenuItem } from '@/components/Contextmenu/types'
  46. import ScreenSlide from './ScreenSlide.vue'
  47. import { Slide } from '@/types/slides'
  48. export default defineComponent({
  49. name: 'screen',
  50. components: {
  51. ScreenSlide,
  52. },
  53. setup() {
  54. const store = useStore<State>()
  55. const slides = computed(() => store.state.slides)
  56. const slideIndex = computed(() => store.state.slideIndex)
  57. const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
  58. const slideWidth = ref(0)
  59. const slideHeight = ref(0)
  60. const scale = computed(() => slideWidth.value / VIEWPORT_SIZE)
  61. const setSlideContentSize = () => {
  62. const winWidth = document.body.clientWidth
  63. const winHeight = document.body.clientHeight
  64. let width, height
  65. if(winHeight / winWidth === VIEWPORT_ASPECT_RATIO) {
  66. width = winWidth
  67. height = winHeight
  68. }
  69. else if(winHeight / winWidth > VIEWPORT_ASPECT_RATIO) {
  70. width = winWidth
  71. height = winWidth * VIEWPORT_ASPECT_RATIO
  72. }
  73. else {
  74. width = winHeight / VIEWPORT_ASPECT_RATIO
  75. height = winHeight
  76. }
  77. slideWidth.value = width
  78. slideHeight.value = height
  79. }
  80. const windowResizeListener = () => {
  81. setSlideContentSize()
  82. if(!isFullscreen()) store.commit(MutationTypes.SET_SCREENING, false)
  83. }
  84. const animationIndex = ref(0)
  85. const animations = computed(() => currentSlide.value.animations || [])
  86. const runAnimation = () => {
  87. const prefix = 'animate__'
  88. const animation = animations.value[animationIndex.value]
  89. animationIndex.value += 1
  90. const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
  91. if(elRef) {
  92. const animationName = `${prefix}${animation.type}`
  93. elRef.classList.add(`${prefix}animated`, animationName)
  94. const handleAnimationEnd = () => {
  95. elRef.classList.remove(`${prefix}animated`, animationName)
  96. }
  97. elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
  98. }
  99. }
  100. const execPrev = () => {
  101. if(animations.value.length && animationIndex.value > 0) {
  102. animationIndex.value -= 1
  103. }
  104. else if(slideIndex.value > 0) {
  105. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value - 1)
  106. const lastIndex = animations.value ? animations.value.length : 0
  107. animationIndex.value = lastIndex
  108. }
  109. }
  110. const execNext = () => {
  111. if(animations.value.length && animationIndex.value < animations.value.length) {
  112. runAnimation()
  113. }
  114. else if(slideIndex.value < slides.value.length - 1) {
  115. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value + 1)
  116. animationIndex.value = 0
  117. }
  118. }
  119. const keydownListener = (e: KeyboardEvent) => {
  120. const key = e.key.toUpperCase()
  121. if(key === KEYS.UP || key === KEYS.LEFT) execPrev()
  122. else if(key === KEYS.DOWN || key === KEYS.RIGHT) execNext()
  123. }
  124. const mousewheelListener = throttle(function(e: WheelEvent) {
  125. if(e.deltaY < 0) execPrev()
  126. else if(e.deltaY > 0) execNext()
  127. }, 500, { leading: true, trailing: false })
  128. onMounted(() => {
  129. window.addEventListener('resize', windowResizeListener)
  130. document.addEventListener('keydown', keydownListener)
  131. })
  132. onUnmounted(() => {
  133. window.removeEventListener('resize', windowResizeListener)
  134. document.removeEventListener('keydown', keydownListener)
  135. })
  136. const turnPrevSlide = () => {
  137. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value - 1)
  138. animationIndex.value = 0
  139. }
  140. const turnNextSlide = () => {
  141. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value + 1)
  142. animationIndex.value = 0
  143. }
  144. const contextmenus = (): ContextmenuItem[] => {
  145. return [
  146. {
  147. text: '上一页',
  148. disable: slideIndex.value <= 0,
  149. handler: () => turnPrevSlide(),
  150. },
  151. {
  152. text: '下一页',
  153. disable: slideIndex.value >= slides.value.length - 1,
  154. handler: () => turnNextSlide(),
  155. },
  156. { divider: true },
  157. {
  158. text: '结束放映',
  159. subText: 'ESC',
  160. handler: exitFullscreen,
  161. },
  162. ]
  163. }
  164. return {
  165. slides,
  166. slideIndex,
  167. slideWidth,
  168. slideHeight,
  169. scale,
  170. mousewheelListener,
  171. animationIndex,
  172. contextmenus,
  173. }
  174. },
  175. })
  176. </script>
  177. <style lang="scss" scoped>
  178. .hamster-ppt-screen {
  179. width: 100%;
  180. height: 100%;
  181. position: relative;
  182. background-color: #111;
  183. }
  184. .slide-list {
  185. background: #1d1d1d;
  186. position: relative;
  187. width: 100%;
  188. height: 100%;
  189. }
  190. .slide-item {
  191. position: absolute;
  192. top: 0;
  193. left: 0;
  194. width: 100%;
  195. height: 100%;
  196. transition-property: transform;
  197. transition-duration: .4s;
  198. &.show {
  199. z-index: 2;
  200. }
  201. &.prev {
  202. transform: translateY(-100%);
  203. }
  204. &.next {
  205. transform: translateY(100%);
  206. }
  207. }
  208. .slide-content {
  209. background-color: #fff;
  210. position: absolute;
  211. top: 50%;
  212. left: 50%;
  213. transform: translate(-50%, -50%);
  214. display: flex;
  215. justify-content: center;
  216. align-items: center;
  217. }
  218. </style>