App.vue 5.7 KB

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