PresenterView.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <template>
  2. <div class="presenter-view">
  3. <div class="toolbar">
  4. <div class="tool-btn" @click="changeViewMode('base')"><IconListView class="tool-icon" /><span>普通视图</span></div>
  5. <div class="tool-btn" :class="{ active: writingBoardToolVisible }" @click="writingBoardToolVisible = !writingBoardToolVisible">
  6. <IconWrite class="tool-icon" /><span>画笔</span>
  7. </div>
  8. <div class="tool-btn" :class="{ active: laserPen }" @click="laserPen = !laserPen"><IconMagic class="tool-icon" /><span>激光笔</span></div>
  9. <div class="tool-btn" :class="{ active: timerlVisible }" @click="timerlVisible = !timerlVisible">
  10. <IconStopwatchStart class="tool-icon" /><span>计时器</span>
  11. </div>
  12. <div v-if="!queryParams.hideFullScreen" class="tool-btn" @click="() => (fullscreenState ? manualExitFullscreen() : enterFullscreen())">
  13. <IconOffScreenOne class="tool-icon" v-if="fullscreenState" />
  14. <IconOffScreenOne class="tool-icon" v-else />
  15. <span>{{ fullscreenState ? "退出全屏" : "全屏" }}</span>
  16. </div>
  17. <Divider class="divider" />
  18. <div v-if="screenStore.mode === 'pptEditor'" class="tool-btn" @click="exitScreening()">
  19. <IconPower class="tool-icon" /><span>结束放映</span>
  20. </div>
  21. </div>
  22. <div class="content">
  23. <div class="slide-list-wrap" :class="{ 'laser-pen': laserPen }" ref="slideListWrapRef">
  24. <ScreenSlideList
  25. :slideWidth="slideWidth"
  26. :slideHeight="slideHeight"
  27. :animationIndex="animationIndex"
  28. :turnSlideToId="turnSlideToId"
  29. :manualExitFullscreen="manualExitFullscreen"
  30. @wheel="$event => mousewheelListener($event)"
  31. @touchstart="$event => touchStartListener($event)"
  32. @touchend="$event => touchEndListener($event)"
  33. v-contextmenu="contextmenus"
  34. />
  35. <WritingBoardTool
  36. :slideWidth="slideWidth"
  37. :slideHeight="slideHeight"
  38. :left="-365"
  39. :top="-155"
  40. v-if="writingBoardToolVisible"
  41. @close="writingBoardToolVisible = false"
  42. />
  43. <CountdownTimer v-if="timerlVisible" :left="75" @close="timerlVisible = false" />
  44. </div>
  45. <div class="thumbnails" ref="thumbnailsRef" @wheel.prevent="$event => handleMousewheelThumbnails($event)">
  46. <div
  47. class="thumbnail"
  48. :class="{ active: index === slideIndex }"
  49. v-for="(slide, index) in slides"
  50. :key="slide.id"
  51. @click="turnSlideToIndex(index)"
  52. >
  53. <ThumbnailSlide :slide="slide" :size="120 / viewportRatio" :visible="index < slidesLoadLimit" />
  54. </div>
  55. </div>
  56. </div>
  57. <div class="remark">
  58. <div class="header">
  59. <span>演讲者备注</span>
  60. <span>P {{ slideIndex + 1 }} / {{ slides.length }}</span>
  61. </div>
  62. <div class="remark-content ProseMirror-static" :style="{ fontSize: remarkFontSize + 'px' }" v-html="currentSlideRemark"></div>
  63. <div class="remark-scale">
  64. <div :class="['scale-btn', { disable: remarkFontSize === 12 }]" @click="setRemarkFontSize(remarkFontSize - 2)"><IconMinus /></div>
  65. <div :class="['scale-btn', { disable: remarkFontSize === 40 }]" @click="setRemarkFontSize(remarkFontSize + 2)"><IconPlus /></div>
  66. </div>
  67. </div>
  68. </div>
  69. </template>
  70. <script lang="ts" setup>
  71. import { computed, nextTick, ref, watch } from "vue"
  72. import { storeToRefs } from "pinia"
  73. import { useSlidesStore, useScreenStore } from "@/store"
  74. import type { ContextmenuItem } from "@/components/Contextmenu/types"
  75. import { enterFullscreen } from "@/utils/fullscreen"
  76. import { parseText2Paragraphs } from "@/utils/textParser"
  77. import useScreening from "@/hooks/useScreening"
  78. import useLoadSlides from "@/hooks/useLoadSlides"
  79. import useExecPlay from "./hooks/useExecPlay"
  80. import useSlideSize from "./hooks/useSlideSize"
  81. import useFullscreen from "./hooks/useFullscreen"
  82. import ThumbnailSlide from "@/views/components/ThumbnailSlide/index.vue"
  83. import ScreenSlideList from "./ScreenSlideList.vue"
  84. import WritingBoardTool from "./WritingBoardTool.vue"
  85. import CountdownTimer from "./CountdownTimer.vue"
  86. import Divider from "@/components/Divider.vue"
  87. import queryParams from "@/queryParams"
  88. const props = defineProps<{
  89. changeViewMode: (mode: "base" | "presenter") => void
  90. }>()
  91. const { slides, slideIndex, viewportRatio, currentSlide } = storeToRefs(useSlidesStore())
  92. const slideListWrapRef = ref<HTMLElement>()
  93. const thumbnailsRef = ref<HTMLElement>()
  94. const writingBoardToolVisible = ref(false)
  95. const timerlVisible = ref(false)
  96. const laserPen = ref(false)
  97. const screenStore = useScreenStore()
  98. const { mousewheelListener, touchStartListener, touchEndListener, turnPrevSlide, turnNextSlide, turnSlideToIndex, turnSlideToId, animationIndex } =
  99. useExecPlay()
  100. const { slideWidth, slideHeight } = useSlideSize(slideListWrapRef)
  101. const { exitScreening } = useScreening()
  102. const { slidesLoadLimit } = useLoadSlides()
  103. const { fullscreenState, manualExitFullscreen } = useFullscreen()
  104. const remarkFontSize = ref(16)
  105. const currentSlideRemark = computed(() => {
  106. return parseText2Paragraphs(currentSlide.value.remark || "无备注")
  107. })
  108. const handleMousewheelThumbnails = (e: WheelEvent) => {
  109. if (!thumbnailsRef.value) return
  110. thumbnailsRef.value.scrollBy(e.deltaY, 0)
  111. }
  112. const setRemarkFontSize = (fontSize: number) => {
  113. if (fontSize < 12 || fontSize > 40) return
  114. remarkFontSize.value = fontSize
  115. }
  116. watch(slideIndex, () => {
  117. nextTick(() => {
  118. if (!thumbnailsRef.value) return
  119. const activeThumbnailRef: HTMLElement | null = thumbnailsRef.value.querySelector(".thumbnail.active")
  120. if (!activeThumbnailRef) return
  121. const width = thumbnailsRef.value.offsetWidth
  122. const offsetLeft = activeThumbnailRef.offsetLeft
  123. thumbnailsRef.value.scrollTo({ left: offsetLeft - width / 2, behavior: "smooth" })
  124. })
  125. })
  126. const contextmenus = (): ContextmenuItem[] => {
  127. const menusData: any[] = [
  128. {
  129. text: "上一页",
  130. subText: "↑ ←",
  131. disable: slideIndex.value <= 0,
  132. handler: () => turnPrevSlide()
  133. },
  134. {
  135. text: "下一页",
  136. subText: "↓ →",
  137. disable: slideIndex.value >= slides.value.length - 1,
  138. handler: () => turnNextSlide()
  139. },
  140. {
  141. text: "第一页",
  142. disable: slideIndex.value === 0,
  143. handler: () => turnSlideToIndex(0)
  144. },
  145. {
  146. text: "最后一页",
  147. disable: slideIndex.value === slides.value.length - 1,
  148. handler: () => turnSlideToIndex(slides.value.length - 1)
  149. },
  150. { divider: true },
  151. {
  152. text: "画笔工具",
  153. handler: () => (writingBoardToolVisible.value = true)
  154. },
  155. {
  156. text: "普通视图",
  157. handler: () => props.changeViewMode("base")
  158. },
  159. { divider: true }
  160. ]
  161. if (screenStore.mode === "pptEditor") {
  162. menusData.push({
  163. text: "结束放映",
  164. subText: "ESC",
  165. handler: exitScreening
  166. })
  167. }
  168. return menusData
  169. }
  170. </script>
  171. <style lang="scss" scoped>
  172. .presenter-view {
  173. width: 100%;
  174. height: 100%;
  175. display: flex;
  176. }
  177. .toolbar {
  178. width: 70px;
  179. height: 100%;
  180. background-color: #fff;
  181. border-right: solid 1px #eee;
  182. font-size: 12px;
  183. margin: 20px 0;
  184. .tool-btn {
  185. display: flex;
  186. flex-direction: column;
  187. justify-content: center;
  188. align-items: center;
  189. cursor: pointer;
  190. & + .tool-btn {
  191. margin-top: 22px;
  192. }
  193. &:hover,
  194. &.active {
  195. color: $themeColor;
  196. }
  197. }
  198. .divider {
  199. width: 70%;
  200. margin: 24px 15% !important;
  201. }
  202. .tool-icon {
  203. margin-bottom: 8px;
  204. font-size: 22px;
  205. }
  206. }
  207. .content {
  208. width: calc(100% - 430px);
  209. height: 100%;
  210. background-color: #1d1d1d;
  211. }
  212. .slide-list-wrap {
  213. height: calc(100% - 190px);
  214. margin: 20px;
  215. overflow: hidden;
  216. position: relative;
  217. &.laser-pen {
  218. cursor:
  219. url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABHNCSVQICAgIfAhkiAAACCJJREFUWIXtmLuO3MYShv/qZl9IzqwXo2BkSAtsIK+z8wwOBcOJ9C56Cr2LlThQcgBnfofVBnswXlgTaLHaIdk3dtcJOKOzd8n2MeDABRDDgKz/m+pudv0N/BN/Luj/kYSZJQBxJR8DKESU/2zuPwTIzAKnpxqHhxUuLir0vYSUAkS0ewA5F7Rtxv7+iNPTEYeHkYjKXwrIzHK9XtultRohaKSkkFIVhqGCEAIxTvm0ZpRSTNOMUGqEUgnGxLX3cblc+t9T2S8GXK1W9dP53OLiwoLZhMtLQ4CiGBVKkchZIOcpn5QMKQuEyKx1YiCZvb0AooD9ff/rZuMPDg7cl+hWn3uAmQWABut1g/PzOnZdTd5bMY6aQtAIQQGQGEd5bYirKgPIZExiY2IKIbK1XpeinzaN2s7b4XPD/iAgM0ucn7fYbNrQ963Juaauq8k5i3E01PcG46iQs0TO1wGlzJAyo6oS2jagqgLGUQNQwTllvJeYzwUz9w8N+b2AzCxwft6i72fBuZkYhnbcbBqKsSbvazhnEIJBzqrEqGQpAlO1AaKShShC6wQpE4UQUNcBKenReyXm8yoIIYwQtNXq7qvkQxVssNm0wbmZuLiYUQgtnGtps2ngfQ3vLaVkEKOmGKcqMtMWkEnKTFonaB3Z+4AQPFmreD6vSAghxpECAFMKY7EoALovBlytVjXW6yb0fSuGoaUQWrq8nKHvW/R9S943xbmavJ+qmNIO8FMFIWXert7A1gYxjprHsSLmaTHt7UF0HYdSilmv82q1ynctnFuAzCzx8aPF+Xltcq7HzaaBcy36vsUwzKjrZhiGRgxDA+8tUjIUgkbOEqVMgEIUkjLDmAjvgwjBI6WKxlHybp5KyVRKMcaMGIb0dLFIzBxvzsdbgOv12i69t7HrpgURY02bTYO+b6nrZui6qZLONdz3jTg5ORDHx0f48OExQpgBAIzp8OjRez46Oi7Pnq1ot5BKETQVgYmosJRj6rrEQNJCxLX3EUB/LyAzC3z8qOGcIe8tOWdpmm81ed9gGJpdJdF1rXz79jucnX1za454P8fZ2ZzOzr6Rx8fvyvPnP38afiEKVVXmqhrJ+wSlIqoqYj73S2s1M7urC0ZcS3x6qhGCDpeXBuOoMY4Gzhl4b4tzNYahgXMNuq4Vb978cCfczTg7+0a8efMDuq6Fcw2GoSnO1fDewjmDcTQYx0kzBI3TU3319euAh4cVUlIEKApBU98bhGAoJSO8N/Dect834u3b73B+/vVn4XZxfv61ePv2O+77Bt5b4b2hlKbcfW8oBE2AQkoKh4fXRvU64MVFhZQqilEhBLX9CCvEqLer1YiTk4MvqtxdlTw5OcAWDDFq5DxphDBtmSlNzcddgMws0fcyDEOFUiQAiZxliVGVGFVJSXEImo6Pj3433Dbo+PiIQ9AlJbXLi5wnrVIm7b6X223wOiAAASkFhBDIWWAcJXKWshQhcpYiZ0k5S3z48PhO9ZcvgV9+ma6XL+8m/PDhMW1ziW1u5Cy3WpO2lOIq11VAAhEhRkLO0z0RgVmAefotRXz6lNyMV6+AxWK6Xr26GzCEGXZb4i7nTifnSXv6Tn7qssTdmf4+cRWQwczQmiHldM/MICogmn6FKDDmzj0Tr18D5+fT9fr13WrGdBCiXMu505Fy0mZmTJYBwPUPdUHOBaUUSFlQVRlS5rzbtqTMJGXGo0fvcXY2vyX+44/T9VA8evSepcy8zcdCFDG1ZBlSTto5FwC3P9RElNG22TTNCCEygAwps9A6Ca2TUCqRMZGPjo4fprg/+OjomIyJQqm0ywspJy0hJu22zVf34+tzcH9/hFIja51gTEJVJUiZoHWEMQFKhfLs2QpPnrz73XRPnrwrz56toFSAMQFaR0g5aRiTWOsEpUbs749XX7u51Y1QKjGQ2JjIbRtgTGClQrE2wFpPbTuU589/xmLx2xfDLRa/lefPf6a2HWCtL9YG3oJy2wY2JjKQoFTC6ekDgIeHEcZEs7cXUFURVTV1wtZ6UdcOTTOgrgfMZn158eKnL6rkkyfvyosXP2E261HXA5pmEHXtYK1HXU9WoKomTWMiDg/j1devbStEVN6/fx+XRIGt9RhHjZQ0Wat4HCsax//1fEQlf//9v8XJyTF9rt1q2+mPtW2PphnY2gHWOrbWcV17ttaDKKy9j4/398u9gACwXC49Pn7UuhQNQI3eT206s2DadptCFEiZqaoS/+tfvnz77X/oRsPKUmYyJpJSAdZ6NM2Aphl4Pu/QND3P5wO0dmo2c5jNHPb3/fKrr/xNnluARJRXq5V/2jQqOKfE1kPsPC8zM1VVLkqNwpiAEAxbq+hGy89SZtq2/MXaIOrasbUDmqZH2/Zo257bdghSOtM07tfNxh/s799yd3d6koODA8fM0ngvw9bgYG9vatOJClfVSFUVYe3UldxhmiBlxtY0kVLTlLHW8Xw+oG17NqYvs1lv6rrHcjkcEN1p5B9ydQPmc2GEoABAdB1TKYWlnDph5wJvbSdPpwvXbCcLUXhrO2FMQF0HttZBa8dtO5TZrDdt26FtewDDfRD3AhJRYeYemKxh2Bqc1HVTm17Xn4y7yFnyDeMurhh33hp3rmuvZjMXpHSmrqehXiz6h04XHjxZIKLMzB0Wi2LW64xhSAwkVFXEOGpo/dmjD2yPPlBVka31mM2caRqH5XLAnz362FUSQLdarfLTxSJpISLmcx8uLw217R8/PLpnzt3S/5KHdvG3Pn67Afr3PMB8APgvOwL+J/5s/BeEBm1u1Gu4+QAAAABJRU5ErkJggg==)
  220. 20 20,
  221. default !important;
  222. }
  223. }
  224. .thumbnails {
  225. height: 150px;
  226. padding: 15px;
  227. white-space: nowrap;
  228. overflow-x: auto;
  229. overflow-y: hidden;
  230. border-top: solid 1px #3a3a3a;
  231. }
  232. .thumbnail {
  233. display: inline-block;
  234. outline: 2px solid #aaa;
  235. & + .thumbnail {
  236. margin-left: 10px;
  237. }
  238. &:hover {
  239. outline-color: $themeColor;
  240. }
  241. &.active {
  242. outline-width: 3px;
  243. outline-color: $themeColor;
  244. }
  245. }
  246. .remark {
  247. width: 360px;
  248. height: 100%;
  249. position: relative;
  250. background-color: #2a2a2a;
  251. border-left: solid 1px #3a3a3a;
  252. color: #fff;
  253. .header {
  254. height: 60px;
  255. padding: 0 20px;
  256. display: flex;
  257. justify-content: space-between;
  258. align-items: center;
  259. font-size: 18px;
  260. border-bottom: 1px solid #3a3a3a;
  261. }
  262. .remark-content {
  263. height: calc(100% - 60px);
  264. padding: 20px;
  265. line-height: 1.5;
  266. @include overflow-overlay();
  267. }
  268. .remark-scale {
  269. position: absolute;
  270. right: 5px;
  271. bottom: 5px;
  272. font-size: 22px;
  273. display: flex;
  274. }
  275. .scale-btn {
  276. width: 40px;
  277. height: 40px;
  278. display: flex;
  279. justify-content: center;
  280. align-items: center;
  281. user-select: none;
  282. cursor: pointer;
  283. &.disable {
  284. color: #666;
  285. cursor: no-drop;
  286. }
  287. &:not(.disable):hover {
  288. background-color: #333;
  289. }
  290. }
  291. }
  292. ::-webkit-scrollbar {
  293. width: 0;
  294. height: 0;
  295. }
  296. </style>