index.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. `turning-mode-${slide.turningMode || 'slideY'}`,
  12. {
  13. 'current': index === slideIndex,
  14. 'before': index < slideIndex,
  15. 'after': index > slideIndex,
  16. }
  17. ]"
  18. v-for="(slide, index) in slides"
  19. :key="slide.id"
  20. >
  21. <div
  22. class="slide-content"
  23. :style="{
  24. width: slideWidth + 'px',
  25. height: slideHeight + 'px',
  26. }"
  27. >
  28. <ScreenSlide
  29. :slide="slide"
  30. :scale="scale"
  31. :animationIndex="animationIndex"
  32. />
  33. </div>
  34. </div>
  35. </div>
  36. <Modal
  37. v-model:visible="slideThumbnailModelVisible"
  38. :footer="null"
  39. centered
  40. :width="1020"
  41. :bodyStyle="{ padding: '50px 20px 20px 20px' }"
  42. >
  43. <SlideThumbnails :turnSlideToIndex="turnSlideToIndex" />
  44. </Modal>
  45. <div class="tools">
  46. <IconLeftC class="tool-btn" @click="execPrev()" />
  47. <IconRightC class="tool-btn" @click="execNext()" />
  48. <IconSearch class="tool-btn" @click="slideThumbnailModelVisible = true" />
  49. <Popover trigger="click" v-model:visible="writingBoardToolVisible">
  50. <template #content>
  51. <WritingBoardTool @close="writingBoardToolVisible = false" />
  52. </template>
  53. <IconWrite class="tool-btn" />
  54. </Popover>
  55. </div>
  56. </div>
  57. </template>
  58. <script lang="ts">
  59. import { computed, defineComponent, onMounted, onUnmounted, provide, Ref, ref } from 'vue'
  60. import { useStore } from 'vuex'
  61. import throttle from 'lodash/throttle'
  62. import { MutationTypes, State } from '@/store'
  63. import { Slide } from '@/types/slides'
  64. import { VIEWPORT_ASPECT_RATIO, VIEWPORT_SIZE } from '@/configs/canvas'
  65. import { KEYS } from '@/configs/hotkey'
  66. import { ContextmenuItem } from '@/components/Contextmenu/types'
  67. import { isFullscreen } from '@/utils/fullscreen'
  68. import useScreening from '@/hooks/useScreening'
  69. import ScreenSlide from './ScreenSlide.vue'
  70. import SlideThumbnails from './SlideThumbnails.vue'
  71. import WritingBoardTool from './WritingBoardTool.vue'
  72. export default defineComponent({
  73. name: 'screen',
  74. components: {
  75. ScreenSlide,
  76. SlideThumbnails,
  77. WritingBoardTool,
  78. },
  79. setup() {
  80. const store = useStore<State>()
  81. const slides = computed(() => store.state.slides)
  82. const slideIndex = computed(() => store.state.slideIndex)
  83. const currentSlide: Ref<Slide> = computed(() => store.getters.currentSlide)
  84. const slideWidth = ref(0)
  85. const slideHeight = ref(0)
  86. const scale = computed(() => slideWidth.value / VIEWPORT_SIZE)
  87. const slideThumbnailModelVisible = ref(false)
  88. const writingBoardToolVisible = ref(false)
  89. const setSlideContentSize = () => {
  90. const winWidth = document.body.clientWidth
  91. const winHeight = document.body.clientHeight
  92. let width, height
  93. if(winHeight / winWidth === VIEWPORT_ASPECT_RATIO) {
  94. width = winWidth
  95. height = winHeight
  96. }
  97. else if(winHeight / winWidth > VIEWPORT_ASPECT_RATIO) {
  98. width = winWidth
  99. height = winWidth * VIEWPORT_ASPECT_RATIO
  100. }
  101. else {
  102. width = winHeight / VIEWPORT_ASPECT_RATIO
  103. height = winHeight
  104. }
  105. slideWidth.value = width
  106. slideHeight.value = height
  107. }
  108. const { exitScreening } = useScreening()
  109. const windowResizeListener = () => {
  110. setSlideContentSize()
  111. if(!isFullscreen()) exitScreening()
  112. }
  113. const animationIndex = ref(0)
  114. const animations = computed(() => currentSlide.value.animations || [])
  115. const runAnimation = () => {
  116. const prefix = 'animate__'
  117. const animation = animations.value[animationIndex.value]
  118. animationIndex.value += 1
  119. const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
  120. if(elRef) {
  121. const animationName = `${prefix}${animation.type}`
  122. elRef.classList.add(`${prefix}animated`, animationName)
  123. const handleAnimationEnd = () => {
  124. elRef.classList.remove(`${prefix}animated`, animationName)
  125. }
  126. elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
  127. }
  128. }
  129. const execPrev = () => {
  130. if(animations.value.length && animationIndex.value > 0) {
  131. animationIndex.value -= 1
  132. }
  133. else if(slideIndex.value > 0) {
  134. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value - 1)
  135. const lastIndex = animations.value ? animations.value.length : 0
  136. animationIndex.value = lastIndex
  137. }
  138. }
  139. const execNext = () => {
  140. if(animations.value.length && animationIndex.value < animations.value.length) {
  141. runAnimation()
  142. }
  143. else if(slideIndex.value < slides.value.length - 1) {
  144. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value + 1)
  145. animationIndex.value = 0
  146. }
  147. }
  148. const keydownListener = (e: KeyboardEvent) => {
  149. const key = e.key.toUpperCase()
  150. if(key === KEYS.UP || key === KEYS.LEFT) execPrev()
  151. else if(key === KEYS.DOWN || key === KEYS.RIGHT) execNext()
  152. }
  153. const mousewheelListener = throttle(function(e: WheelEvent) {
  154. if(e.deltaY < 0) execPrev()
  155. else if(e.deltaY > 0) execNext()
  156. }, 500, { leading: true, trailing: false })
  157. onMounted(() => {
  158. window.addEventListener('resize', windowResizeListener)
  159. document.addEventListener('keydown', keydownListener)
  160. })
  161. onUnmounted(() => {
  162. window.removeEventListener('resize', windowResizeListener)
  163. document.removeEventListener('keydown', keydownListener)
  164. })
  165. const turnPrevSlide = () => {
  166. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value - 1)
  167. animationIndex.value = 0
  168. }
  169. const turnNextSlide = () => {
  170. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value + 1)
  171. animationIndex.value = 0
  172. }
  173. const turnSlideToIndex = (index: number) => {
  174. slideThumbnailModelVisible.value = false
  175. store.commit(MutationTypes.UPDATE_SLIDE_INDEX, index)
  176. animationIndex.value = 0
  177. }
  178. const contextmenus = (): ContextmenuItem[] => {
  179. return [
  180. {
  181. text: '上一页',
  182. disable: slideIndex.value <= 0,
  183. handler: () => turnPrevSlide(),
  184. },
  185. {
  186. text: '下一页',
  187. disable: slideIndex.value >= slides.value.length - 1,
  188. handler: () => turnNextSlide(),
  189. },
  190. {
  191. text: '结束放映',
  192. subText: 'ESC',
  193. handler: exitScreening,
  194. },
  195. ]
  196. }
  197. provide('slideScale', scale)
  198. return {
  199. slides,
  200. slideIndex,
  201. slideWidth,
  202. slideHeight,
  203. scale,
  204. mousewheelListener,
  205. animationIndex,
  206. contextmenus,
  207. execPrev,
  208. execNext,
  209. slideThumbnailModelVisible,
  210. turnSlideToIndex,
  211. writingBoardToolVisible,
  212. }
  213. },
  214. })
  215. </script>
  216. <style lang="scss" scoped>
  217. .hamster-ppt-screen {
  218. width: 100%;
  219. height: 100%;
  220. position: relative;
  221. background-color: #111;
  222. }
  223. .slide-list {
  224. background: #1d1d1d;
  225. position: relative;
  226. width: 100%;
  227. height: 100%;
  228. }
  229. .slide-item {
  230. position: absolute;
  231. top: 0;
  232. left: 0;
  233. width: 100%;
  234. height: 100%;
  235. &.current {
  236. z-index: 2;
  237. }
  238. &.turning-mode-no {
  239. &.before {
  240. transform: translateY(-100%);
  241. }
  242. &.after {
  243. transform: translateY(100%);
  244. }
  245. }
  246. &.turning-mode-fade {
  247. transition: opacity .75s;
  248. &.before {
  249. pointer-events: none;
  250. opacity: 0;
  251. }
  252. &.after {
  253. pointer-events: none;
  254. opacity: 0;
  255. }
  256. }
  257. &.turning-mode-slideX {
  258. transition: transform .35s;
  259. &.before {
  260. transform: translateX(-100%);
  261. }
  262. &.after {
  263. transform: translateX(100%);
  264. }
  265. }
  266. &.turning-mode-slideY {
  267. transition: transform .35s;
  268. &.before {
  269. transform: translateY(-100%);
  270. }
  271. &.after {
  272. transform: translateY(100%);
  273. }
  274. }
  275. }
  276. .slide-content {
  277. background-color: #fff;
  278. position: absolute;
  279. top: 50%;
  280. left: 50%;
  281. transform: translate(-50%, -50%);
  282. display: flex;
  283. justify-content: center;
  284. align-items: center;
  285. }
  286. .tools {
  287. position: fixed;
  288. bottom: 8px;
  289. left: 8px;
  290. font-size: 25px;
  291. color: #666;
  292. z-index: 10;
  293. cursor: pointer;
  294. }
  295. .tool-btn {
  296. opacity: .35;
  297. &:hover {
  298. opacity: .7;
  299. }
  300. & + .tool-btn {
  301. margin-left: 8px;
  302. }
  303. }
  304. </style>