App.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. <template>
  2. <!-- 移动端横屏提示遮罩(全局统一) -->
  3. <div class="mobile-landscape-hint" :class="{ 'mobile-show': showLandscapeHint }">
  4. <div class="hint-icon">📱</div>
  5. <div class="hint-text">请横屏使用</div>
  6. <div class="hint-desc">为获得更好的体验,请将设备横屏放置</div>
  7. <button class="hint-close-btn" @click="closeLandscapeHint">知道了({{ countdown }}秒)</button>
  8. </div>
  9. <!-- 路由视图 -->
  10. <router-view></router-view>
  11. <footer v-if="isLoginPage" class="login-record-footer">
  12. <a
  13. href="https://beian.miit.gov.cn/#/Integrated/index"
  14. target="_blank"
  15. rel="noopener noreferrer"
  16. >
  17. ICP备案:京ICP备2025136397号-1
  18. </a>
  19. </footer>
  20. </template>
  21. <script setup>
  22. import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
  23. import { useRoute } from 'vue-router'
  24. const route = useRoute()
  25. const showLandscapeHint = ref(false)
  26. const countdown = ref(3) // 倒计时初始值
  27. let countdownTimer = null
  28. const loginRoutes = new Set([
  29. '/',
  30. '/login',
  31. '/login-mobile',
  32. '/quick-login',
  33. '/promotion-login',
  34. '/login-qsfl',
  35. '/register-login',
  36. '/ai-login',
  37. '/blockly-login'
  38. ])
  39. const isLoginPage = computed(() => loginRoutes.has(route.path))
  40. let hasShownOnce = false // 用于跟踪当前会话中是否已经显示过提示
  41. // 检测是否为移动设备
  42. const isMobileDevice = () => {
  43. return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
  44. (window.innerWidth <= 768 && window.innerHeight <= 1024)
  45. }
  46. // 检测设备是否为横屏
  47. const isLandscape = () => {
  48. return window.innerWidth > window.innerHeight
  49. }
  50. // 检查当前路由是否为排除页面
  51. const isExcludedRoute = () => {
  52. return route.path.includes('/parent-mobile-course') ||
  53. route.path.includes('/parent-mobile-course-detail') ||
  54. route.path.includes('/login') ||
  55. route.path.includes('/ai-develop') ||
  56. route.path.includes('/ai-general-course')
  57. }
  58. // 关闭横屏提示
  59. const closeLandscapeHint = () => {
  60. showLandscapeHint.value = false
  61. hasShownOnce = true // 标记本次会话中已显示过提示
  62. // 清理倒计时定时器
  63. if (countdownTimer) {
  64. clearInterval(countdownTimer)
  65. countdownTimer = null
  66. }
  67. }
  68. // 开始倒计时
  69. const startCountdown = () => {
  70. // 重置倒计时
  71. countdown.value = 3
  72. // 清理之前的定时器
  73. if (countdownTimer) {
  74. clearInterval(countdownTimer)
  75. }
  76. // 开始新的倒计时
  77. countdownTimer = setInterval(() => {
  78. countdown.value--
  79. if (countdown.value <= 0) {
  80. closeLandscapeHint()
  81. }
  82. }, 1000)
  83. }
  84. // 检查是否应该显示横屏提示
  85. const checkLandscapeHint = () => {
  86. // 对于排除的路由,始终不显示横屏提示
  87. if (isExcludedRoute()) {
  88. showLandscapeHint.value = false
  89. return
  90. }
  91. if (isMobileDevice() && !isLandscape() && !isExcludedRoute() && !hasShownOnce) {
  92. // 如果是移动设备、竖屏、不在排除路由中,且本次会话中未显示过提示,则显示横屏提示
  93. showLandscapeHint.value = true
  94. // 开始倒计时
  95. startCountdown()
  96. }
  97. }
  98. // 监听窗口大小变化(仅处理屏幕旋转)
  99. const handleResize = () => {
  100. // 如果是由于旋转导致的横屏,则关闭提示
  101. if (isLandscape()) {
  102. closeLandscapeHint()
  103. } else {
  104. // 如果变为竖屏,且不在排除路由中,且本次会话中未显示过提示,则检查是否显示提示
  105. if (!isExcludedRoute() && isMobileDevice() && !hasShownOnce) {
  106. showLandscapeHint.value = true
  107. startCountdown()
  108. }
  109. }
  110. }
  111. // 监听路由变化
  112. watch(() => route.path, () => {
  113. // 如果进入排除路由,立即关闭提示
  114. if (isExcludedRoute()) {
  115. showLandscapeHint.value = false
  116. // 清理可能存在的倒计时
  117. if (countdownTimer) {
  118. clearInterval(countdownTimer)
  119. countdownTimer = null
  120. }
  121. }
  122. // 每次路由变化时都检查是否应该显示横屏提示
  123. checkLandscapeHint()
  124. }, { immediate: true })
  125. // 组件挂载时更新状态
  126. onMounted(() => {
  127. checkLandscapeHint() // 页面加载时检查
  128. window.addEventListener('resize', handleResize)
  129. // 监听设备方向变化(如果支持的话)
  130. if (window.screen && window.screen.orientation) {
  131. window.screen.orientation.addEventListener('change', handleResize)
  132. }
  133. })
  134. // 组件卸载时移除事件监听
  135. onUnmounted(() => {
  136. window.removeEventListener('resize', handleResize)
  137. if (window.screen && window.screen.orientation) {
  138. window.screen.orientation.removeEventListener('change', handleResize)
  139. }
  140. // 清理定时器
  141. if (countdownTimer) {
  142. clearInterval(countdownTimer)
  143. }
  144. })
  145. </script>
  146. <style scoped>
  147. /* 移动端横屏提示遮罩 */
  148. .mobile-landscape-hint {
  149. position: fixed;
  150. top: 0;
  151. left: 0;
  152. width: 100%;
  153. height: 100%;
  154. background: rgba(0, 0, 0, 0.9);
  155. display: none;
  156. flex-direction: column;
  157. justify-content: center;
  158. align-items: center;
  159. z-index: 9999;
  160. color: white;
  161. text-align: center;
  162. }
  163. .mobile-landscape-hint.mobile-show {
  164. display: flex;
  165. }
  166. .hint-icon {
  167. font-size: 60px;
  168. margin-bottom: 20px;
  169. }
  170. .hint-text {
  171. font-size: 24px;
  172. margin-bottom: 10px;
  173. font-weight: bold;
  174. }
  175. .hint-desc {
  176. font-size: 16px;
  177. opacity: 0.8;
  178. margin-bottom: 15px;
  179. }
  180. .hint-close-btn {
  181. background-color: #409eff;
  182. color: white;
  183. border: none;
  184. padding: 8px 16px;
  185. border-radius: 4px;
  186. cursor: pointer;
  187. font-size: 16px;
  188. margin-top: 10px;
  189. }
  190. .hint-close-btn:hover {
  191. background-color: #66b1ff;
  192. }
  193. .login-record-footer {
  194. position: fixed;
  195. right: 16px;
  196. bottom: 16px;
  197. left: 16px;
  198. z-index: 1000;
  199. text-align: center;
  200. }
  201. .login-record-footer a {
  202. color: rgba(255, 255, 255, 0.86);
  203. font-size: 12px;
  204. line-height: 1.5;
  205. text-decoration: none;
  206. text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
  207. }
  208. .login-record-footer a:hover,
  209. .login-record-footer a:focus-visible {
  210. color: #fff;
  211. text-decoration: underline;
  212. }
  213. /* 移动端样式 - 屏幕宽度小于 768px */
  214. @media (max-width: 768px) {
  215. .login-record-footer {
  216. right: 12px;
  217. bottom: 12px;
  218. left: 12px;
  219. }
  220. .login-record-footer a {
  221. font-size: 11px;
  222. }
  223. router-view {
  224. padding: 10px;
  225. }
  226. }
  227. /* PC 端样式 - 屏幕宽度大于 1200px */
  228. @media (min-width: 1200px) {
  229. router-view {
  230. padding: 20px;
  231. }
  232. }
  233. * {
  234. margin: 0;
  235. padding: 0;
  236. }
  237. html,
  238. body{
  239. width: 100vw;
  240. height: 100vh;
  241. overflow: hidden;
  242. }
  243. </style>