App.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <template>
  2. <template v-if="slides.length">
  3. <Screen v-if="viewMode !== 'student'" v-show="screening"/>
  4. <Editor v-if="viewMode !== 'student'" v-show="_isPC && !screening" :courseid="urlParams.courseid"/>
  5. <Student v-else-if="viewMode === 'student'" :courseid="urlParams.courseid" :type="urlParams.type" :userid="urlParams.userid" :oid="urlParams.oid" :org="urlParams.org" :cid="urlParams.cid" />
  6. <Mobile v-else />
  7. </template>
  8. <FullscreenSpin tip="数据初始化中,请稍等 ..." v-else loading :mask="false" />
  9. </template>
  10. <script lang="ts" setup>
  11. import { onMounted, ref, provide } from 'vue'
  12. import { storeToRefs } from 'pinia'
  13. import { useScreenStore, useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
  14. import { LOCALSTORAGE_KEY_DISCARDED_DB } from '@/configs/storage'
  15. import { deleteDiscardedDB } from '@/utils/database'
  16. import { isPC } from '@/utils/common'
  17. import api from '@/services'
  18. import Editor from './views/Editor/index.vue'
  19. import Screen from './views/Screen/index.vue'
  20. import Mobile from './views/Mobile/index.vue'
  21. import Student from './views/Student/index.vue'
  22. import FullscreenSpin from '@/components/FullscreenSpin.vue'
  23. const _isPC = isPC()
  24. const mainStore = useMainStore()
  25. const slidesStore = useSlidesStore()
  26. const snapshotStore = useSnapshotStore()
  27. const { databaseId } = storeToRefs(mainStore)
  28. const { slides } = storeToRefs(slidesStore)
  29. const { screening } = storeToRefs(useScreenStore())
  30. // 视图模式:'editor', 'student', 'screen'
  31. // 支持通过URL参数直接访问学生模式
  32. const getInitialViewMode = () => {
  33. // 检查URL参数
  34. const urlParams = new URLSearchParams(window.location.search)
  35. const modeFromUrl = urlParams.get('mode')
  36. if (modeFromUrl === 'student') {
  37. return 'student'
  38. }
  39. // 检查localStorage
  40. const modeFromStorage = localStorage.getItem('viewMode')
  41. if (modeFromStorage) {
  42. return modeFromStorage
  43. }
  44. // 默认返回编辑模式
  45. return 'editor'
  46. }
  47. // 获取URL参数中的courseid和type
  48. const getUrlParams = () => {
  49. const urlParams = new URLSearchParams(window.location.search)
  50. return {
  51. courseid: urlParams.get('courseid'),
  52. userid: urlParams.get('userid'),
  53. oid: urlParams.get('oid'),
  54. org: urlParams.get('org'),
  55. cid: urlParams.get('cid'),
  56. type: urlParams.get('type')
  57. }
  58. }
  59. const urlParams = getUrlParams()
  60. const viewMode = ref(getInitialViewMode())
  61. // 全局切换视图模式的函数
  62. const switchViewMode = (mode: string) => {
  63. viewMode.value = mode
  64. localStorage.setItem('viewMode', mode)
  65. // 更新URL参数
  66. const url = new URL(window.location.href)
  67. if (mode === 'student') {
  68. url.searchParams.set('mode', 'student')
  69. }
  70. else {
  71. url.searchParams.delete('mode')
  72. }
  73. // 使用 history.pushState 更新URL,不刷新页面
  74. window.history.pushState({}, '', url.toString())
  75. }
  76. // 使用provide提供切换函数,供子组件调用
  77. provide('switchViewMode', switchViewMode)
  78. if (import.meta.env.MODE !== 'development') {
  79. window.onbeforeunload = () => false
  80. }
  81. onMounted(async () => {
  82. const slides = await api.getFileData('slides')
  83. console.log(slides)
  84. slidesStore.setSlides(slides)
  85. // 初始化快照数据库
  86. // await deleteDiscardedDB()
  87. // snapshotStore.initSnapshotDatabase()
  88. // 监听视图模式切换事件
  89. window.addEventListener('viewModeChanged', (event: any) => {
  90. if (event.detail) {
  91. switchViewMode(event.detail)
  92. }
  93. })
  94. })
  95. // 应用注销时向 localStorage 中记录下本次 indexedDB 的数据库ID,用于之后清除数据库
  96. window.addEventListener('beforeunload', () => {
  97. const discardedDB = localStorage.getItem(LOCALSTORAGE_KEY_DISCARDED_DB)
  98. const discardedDBList: string[] = discardedDB ? JSON.parse(discardedDB) : []
  99. discardedDBList.push(databaseId.value)
  100. const newDiscardedDB = JSON.stringify(discardedDBList)
  101. localStorage.setItem(LOCALSTORAGE_KEY_DISCARDED_DB, newDiscardedDB)
  102. })
  103. </script>
  104. <style lang="scss">
  105. #app {
  106. height: 100%;
  107. }
  108. .image-preview {
  109. position: fixed;
  110. inset: 0;
  111. background: rgba(0,0,0,.85);
  112. display: flex;
  113. flex-direction: column;
  114. z-index: 6000;
  115. }
  116. .image-preview__toolbar {
  117. display: flex;
  118. gap: 8px;
  119. padding: 10px;
  120. justify-content: center;
  121. z-index: 9999;
  122. }
  123. .image-preview__toolbar button {
  124. padding: 8px 12px;
  125. border: none;
  126. background: linear-gradient(180deg, #3a8bff 0%, #2f80ed 100%);
  127. color: #fff;
  128. border-radius: 10px;
  129. cursor: pointer;
  130. transition: transform .15s ease, box-shadow .2s ease, background .2s ease;
  131. box-shadow: 0 2px 8px rgba(47,128,237,.3);
  132. }
  133. .image-preview__toolbar button:hover {
  134. transform: translateY(-1px);
  135. box-shadow: 0 6px 16px rgba(47,128,237,.35);
  136. }
  137. .image-preview__toolbar button:active {
  138. transform: translateY(0);
  139. box-shadow: 0 2px 8px rgba(47,128,237,.28);
  140. background: linear-gradient(180deg, #2f80ed 0%, #1b6dde 100%);
  141. }
  142. .image-preview__toolbar button:focus-visible {
  143. outline: 2px solid rgba(47,128,237,.6);
  144. outline-offset: 2px;
  145. }
  146. .image-preview__stage {
  147. flex: 1;
  148. display: flex;
  149. align-items: center;
  150. justify-content: center;
  151. cursor: grab;
  152. }
  153. .image-preview__stage:active { cursor: grabbing; }
  154. .image-preview__img {
  155. max-width: 92vw;
  156. max-height: 92vh;
  157. border-radius: 8px;
  158. box-shadow: 0 10px 30px rgba(0,0,0,.3);
  159. user-select: none;
  160. will-change: transform;
  161. }
  162. </style>