index.vue 8.6 KB

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