Browse Source

Merge branch 'beta' of https://git.cocorobo.cn/jack/PPT into beta

jack 12 hours ago
parent
commit
94ceee38d5

+ 261 - 90
src/components/CreateCourseDialog.vue

@@ -1,10 +1,10 @@
 <template>
 <template>
   <div class="create-course-dialog">
   <div class="create-course-dialog">
     <div class="dialog-header">
     <div class="dialog-header">
-      <button class="close-btn" @click="$emit('close')">
+      <button class="close-btn" @click="handleClose">
         <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
         <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-          <line x1="18" y1="6" x2="6" y2="18"/>
-          <line x1="6" y1="6" x2="18" y2="18"/>
+          <line x1="18" y1="6" x2="6" y2="18" />
+          <line x1="6" y1="6" x2="18" y2="18" />
         </svg>
         </svg>
       </button>
       </button>
     </div>
     </div>
@@ -24,10 +24,8 @@
           <p>AI自动生成完整教学内容</p>
           <p>AI自动生成完整教学内容</p>
           <div class="coming-soon">待上线</div>
           <div class="coming-soon">待上线</div>
         </div>
         </div>
-        <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation" @change="files => {
-          importPPTXFile(files)
-          $emit('close')
-        }">
+        <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
+          @change="handleFileUpload">
           <div class="option-card">
           <div class="option-card">
             <div class="option-icon">
             <div class="option-icon">
               <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
               <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -71,24 +69,96 @@
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
+
+    <!-- 解析状态弹窗 -->
+    <div v-if="exporting" class="parsing-modal">
+      <div class="parsing-content">
+        <div class="loading-spinner" v-if="exporting"></div>
+        <div class="success-icon" v-if="!exporting">
+          <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector" d="M5.41675 14.084L9.75008 18.4173L20.5834 7.58398" stroke="#FF9300"
+                stroke-width="2.16667" stroke-linecap="round" stroke-linejoin="round" />
+            </g>
+          </svg>
+
+        </div>
+        <h3>{{ exporting ? '解析中...' : '导出完成' }}</h3>
+        <p v-if="exporting">正在解析 {{ currentFileName }}</p>
+        <p v-if="!exporting">解析完成,已生成课件</p>
+        <button class="close-btn2" @click="handleParsingClose">
+          {{ exporting ? '关闭' : '完成' }}
+        </button>
+      </div>
+    </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script lang="ts" setup>
 <script lang="ts" setup>
+import { ref } from 'vue'
 import useImport from '@/hooks/useImport'
 import useImport from '@/hooks/useImport'
 import FileInput from '@/components/FileInput.vue'
 import FileInput from '@/components/FileInput.vue'
+import message from '@/utils/message'
 
 
 const emit = defineEmits<{
 const emit = defineEmits<{
   (e: 'close'): void
   (e: 'close'): void
   (e: 'select', option: string): void
   (e: 'select', option: string): void
 }>()
 }>()
 
 
-const { importPPTXFile } = useImport()
+const { importPPTXFile, exporting } = useImport()
+const currentFileName = ref('')
+const parsingStatus = ref<'parsing' | 'success'>('parsing')
+const parsingAbortController = ref<AbortController | null>(null)
 
 
 const handleOptionClick = (option: string) => {
 const handleOptionClick = (option: string) => {
   emit('select', option)
   emit('select', option)
   emit('close')
   emit('close')
 }
 }
+
+const handleClose = () => {
+  interface ParentWindowWithToolList extends Window {
+    goBack?: () => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.goBack?.()
+}
+
+const handleFileUpload = async (files: FileList) => {
+  if (!files || files.length === 0) return
+
+  const file = files[0]
+  currentFileName.value = file.name
+
+  try {
+    // 创建AbortController用于取消操作
+    parsingAbortController.value = new AbortController()
+    const signal = parsingAbortController.value.signal
+
+    // 调用importPPTXFile并传入signal
+    await importPPTXFile(files, { signal, onclose: () => emit('close') })
+  }
+  catch (error) {
+    if (error instanceof DOMException && error.name === 'AbortError') {
+      console.log('文件解析已取消')
+    }
+    else {
+      console.error('文件解析失败:', error)
+      message.error('文件解析失败,请重试')
+    }
+  }
+}
+
+const handleParsingClose = () => {
+  if (exporting.value && parsingAbortController.value) {
+    parsingAbortController.value.abort()
+    exporting.value = false
+    parsingAbortController.value = null
+    // message.info('解析已取消')
+  }
+  else if (!exporting.value) {
+    emit('close')
+  }
+}
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
@@ -150,104 +220,205 @@ const handleOptionClick = (option: string) => {
       gap: 20px;
       gap: 20px;
 
 
       .option-card {
       .option-card {
-          background: #fafbfc;
-          border: 1px solid #E5E7EB;
-          border-radius: 12px;
-          padding: 24px;
-          text-align: center;
-          cursor: pointer;
-          transition: all 0.3s;
-          position: relative;
+        background: #fafbfc;
+        border: 1px solid #E5E7EB;
+        border-radius: 12px;
+        padding: 24px;
+        text-align: center;
+        cursor: pointer;
+        transition: all 0.3s;
+        position: relative;
 
 
-          &:hover {
-            border-color: #FF9300;
-            // box-shadow: 0 4px 12px rgba(255, 147, 0, 0.15);
-            background: #FFFAF0;
+        &:hover {
+          border-color: #FF9300;
+          background: #FFFAF0;
 
 
-            .option-icon {
-              color: #FF9300;
-            }
+          .option-icon {
+            color: #FF9300;
           }
           }
+        }
+
+        &.active {
+          background: #FFFAF0;
+          border-color: #FF9300;
+        }
 
 
-          &.active {
-            background: #FFFAF0;
-            border-color: #FF9300;
+        &.disabled {
+          background: #f8f8f9;
+          border-color: #eff0f3;
+          cursor: not-allowed;
+
+          h3 {
+            color: #7c7f86;
           }
           }
 
 
-          &.disabled {
-            background: #f8f8f9;
-            border-color: #eff0f3;
-            cursor: not-allowed;
-
-            h3 {
-              color: #7c7f86;
-            }
-
-            p {
-              color: #b5b9bf;
-            }
-
-            .option-icon {
-              color: #a9aeb5;
-              background: #fff;
-            }
-
-            // &:hover {
-            //   border-color: #E5E7EB;
-            //   box-shadow: none;
-            //   background: #F3F4F6;
-
-            //   .option-icon {
-            //     color: #D1D5DB;
-            //   }
-            // }
+          p {
+            color: #b5b9bf;
           }
           }
 
 
           .option-icon {
           .option-icon {
-            width: 48px;
-            height: 48px;
-            background: #eef3ff;
-            border-radius: 12px;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            margin: 0 auto 16px;
-            color: #6b7280;
-            transition: all 0.3s;
-
-            svg {
-              width: 24px;
-              height: 24px;
-            }
+            color: #a9aeb5;
+            background: #fff;
           }
           }
+        }
 
 
-          h3 {
-            font-size: 18px;
-            font-weight: 600;
-            color: #333;
-            margin: 0 0 8px;
-          }
+        .option-icon {
+          width: 48px;
+          height: 48px;
+          background: #eef3ff;
+          border-radius: 12px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          margin: 0 auto 16px;
+          color: #6b7280;
+          transition: all 0.3s;
 
 
-          p {
-            font-size: 14px;
-            color: #999;
-            margin: 0 0 16px;
+          svg {
+            width: 24px;
+            height: 24px;
           }
           }
+        }
 
 
-          .coming-soon {
-            position: absolute;
-            top: 12px;
-            right: 12px;
-            background: #c5c9d0;
-            color: #fff;
-            font-size: 14px;
-            font-weight: 500;
-            padding: 4px 8px;
-            border-radius: 15px;
-            text-transform: uppercase;
-          }
+        h3 {
+          font-size: 18px;
+          font-weight: 600;
+          color: #333;
+          margin: 0 0 8px;
+        }
+
+        p {
+          font-size: 14px;
+          color: #999;
+          margin: 0 0 16px;
+        }
+
+        .coming-soon {
+          position: absolute;
+          top: 12px;
+          right: 12px;
+          background: #c5c9d0;
+          color: #fff;
+          font-size: 14px;
+          font-weight: 500;
+          padding: 4px 8px;
+          border-radius: 15px;
+          text-transform: uppercase;
+        }
+      }
+    }
+  }
+
+  .parsing-modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 1000;
+
+    .parsing-content {
+      background: white;
+      border-radius: 12px;
+      padding: 40px;
+      text-align: center;
+      max-width: 400px;
+      width: 90%;
+
+      .loading-spinner {
+        width: 48px;
+        height: 48px;
+        border: 4px solid #f3f3f3;
+        border-top: 4px solid #FF9300;
+        border-radius: 50%;
+        animation: spin 1s linear infinite;
+        margin: 0 auto 20px;
+      }
+
+      .success-icon {
+        width: 48px;
+        height: 48px;
+        margin: 0 auto 20px;
+        background: #FFFAF0;
+        border-radius: 5px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: white;
+        font-size: 24px;
+        font-weight: bold;
+      }
+
+      h3 {
+        font-size: 20px;
+        font-weight: 600;
+        color: #333;
+        margin: 0 0 12px;
+      }
+
+      p {
+        font-size: 14px;
+        color: #666;
+        margin: 0 0 24px;
+      }
+
+      .close-btn2 {
+        background: #FF9300;
+        color: white;
+        border: none;
+        border-radius: 8px;
+        padding: 12px 24px;
+        font-size: 14px;
+        font-weight: 500;
+        cursor: pointer;
+        width: 100%;
+        transition: all 0.3s;
+
+        &:hover {
+          background: #e68a00;
         }
         }
+      }
+    }
+
+    @keyframes spin {
+      0% {
+        transform: rotate(0deg);
+      }
+
+      100% {
+        transform: rotate(360deg);
+      }
+    }
+
+    .close-btn {
+      width: 32px;
+      height: 32px;
+      border: none;
+      background: none;
+      cursor: pointer;
+      color: #999;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 4px;
+      transition: all 0.2s;
+
+      &:hover {
+        background: #f0f0f0;
+        color: #666;
+      }
+
+      svg {
+        width: 16px;
+        height: 16px;
+      }
     }
     }
   }
   }
+
+
 }
 }
 </style>
 </style>

File diff suppressed because it is too large
+ 321 - 277
src/hooks/useImport.ts


+ 19 - 2
src/main.ts

@@ -1,6 +1,23 @@
 import { createApp } from 'vue'
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 import { createPinia } from 'pinia'
 import App from './App.vue'
 import App from './App.vue'
+import en from './views/lang/en.json'
+import cn from './views/lang/cn.json'
+import hk from './views/lang/hk.json'
+
+export let lang = cn
+if (window.location.href.includes('cocorobo.cn')) {
+  lang = cn
+}
+else if (window.location.href.includes('cocorobo.hk')) {
+  lang = hk
+}
+else if (window.location.href.includes('cocorobo.com')) {
+  lang = en
+}
+else {
+  lang = cn
+}
 
 
 // TypeScript declarations for global properties
 // TypeScript declarations for global properties
 declare module '@vue/runtime-core' {
 declare module '@vue/runtime-core' {
@@ -29,9 +46,9 @@ export const getCurrentVersion = () => {
 }
 }
 
 
 // 当前版本
 // 当前版本
-const currentVersion = getCurrentVersion()
+export const currentVersion = getCurrentVersion()
 
 
-export default currentVersion
+export default {currentVersion, lang}
 
 
 const app = createApp(App)
 const app = createApp(App)
 // 注册全局变量
 // 注册全局变量

+ 7 - 1
src/services/config.ts

@@ -79,7 +79,7 @@ instance.interceptors.response.use(
     if (response.status >= 200 && response.status < 400) {
     if (response.status >= 200 && response.status < 400) {
       return Promise.resolve(response.data)
       return Promise.resolve(response.data)
     }
     }
-    message.error(response.config.url || "")
+    message.error(response.config.url || '')
     message.error('未知的请求错误!')
     message.error('未知的请求错误!')
     return Promise.reject(response)
     return Promise.reject(response)
   },
   },
@@ -100,6 +100,12 @@ instance.interceptors.response.use(
           fullUrl += '?' + params
           fullUrl += '?' + params
         }
         }
       }
       }
+
+      // 检查是否需要显示错误信息
+      const showError = config.showError !== false
+      if (!showError) {
+        return Promise.reject(error)
+      }
     }
     }
 
 
     if (error && error.response) {
     if (error && error.response) {

+ 2 - 2
src/services/course.ts

@@ -83,8 +83,8 @@ export const selectWorksStudent = (oid: string, cid: string): Promise<any> => {
  * @param url 目标URL
  * @param url 目标URL
  * @returns Promise<any>
  * @returns Promise<any>
  */
  */
-export const getHTML = (url: string): Promise<any> => {
-  return axios.get(`${url}`)
+export const getHTML = (url: string, showError: boolean = false): Promise<any> => {
+  return axios.get(`${url}`, { showError })
 }
 }
 
 
 /**
 /**

+ 1 - 8
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -339,6 +339,7 @@ const processWorkContent = async (content: string, toolType: number): Promise<an
                   item2.content.trim()
                   item2.content.trim()
                 )
                 )
               ) {
               ) {
+                item2.content = item2.content.replace(/&lt;/g, '<').replace(/&quot;/g, '"').replace(/&gt;/g, '>')
                 item2.content = md.render(item2.content)
                 item2.content = md.render(item2.content)
               }
               }
             })
             })
@@ -415,18 +416,10 @@ const lookWorkData = computed(() => {
       (i: any) => i.id === lookWorkDetail.value
       (i: any) => i.id === lookWorkDetail.value
     )
     )
 
 
-    _workFind.content.forEach((i:any) => {
-      i.messages.forEach((i2:any) => {
-        // 转字符  例如&lt; 》 <   &quot; 》 "  &gt; 》 >
-        i2.content = i2.content.replace(/&lt;/g, '<').replace(/&quot;/g, '"').replace(/&gt;/g, '>')
-      })
-    })
     if (_workFind) {
     if (_workFind) {
       _result = _workFind
       _result = _workFind
     }
     }
 
 
-
-    console.log(_workFind)
   }
   }
 
 
   return _result
   return _result

+ 71 - 33
src/views/Student/index.vue

@@ -372,7 +372,7 @@ import useImport from '@/hooks/useImport'
 import message from '@/utils/message'
 import message from '@/utils/message'
 import api, { API_URL } from '@/services/course'
 import api, { API_URL } from '@/services/course'
 import axios from '@/services/config'
 import axios from '@/services/config'
-import currentVersion from '@/main'
+import {currentVersion, lang} from '@/main'
 import ShotWorkModal from './components/ShotWorkModal.vue'
 import ShotWorkModal from './components/ShotWorkModal.vue'
 import QAWorkModal from './components/QAWorkModal.vue'
 import QAWorkModal from './components/QAWorkModal.vue'
 import ChoiceWorkModal from './components/ChoiceWorkModal.vue'
 import ChoiceWorkModal from './components/ChoiceWorkModal.vue'
@@ -594,6 +594,7 @@ const connectionStatus = ref<'disconnected' | 'connecting' | 'connected'>('disco
 // 认证 token 相关变量
 // 认证 token 相关变量
 const authToken = ref<string | null>(null)
 const authToken = ref<string | null>(null)
 const authTokenUpdateTimer = ref<NodeJS.Timeout | null>(null)
 const authTokenUpdateTimer = ref<NodeJS.Timeout | null>(null)
+const socketCheckTimer = ref<NodeJS.Timeout | null>(null)
 
 
 // 同步数据最大保留时间(40分钟)
 // 同步数据最大保留时间(40分钟)
 const SYNC_DATA_MAX_AGE = 40 * 60 * 1000 // 40分钟 = 40 * 60 * 1000毫秒
 const SYNC_DATA_MAX_AGE = 40 * 60 * 1000 // 40分钟 = 40 * 60 * 1000毫秒
@@ -1503,16 +1504,16 @@ const processIframeLinks = async () => {
                   // 如果无法获取contentWindow,使用HTML方式
                   // 如果无法获取contentWindow,使用HTML方式
                   let html = null
                   let html = null
                   try {
                   try {
-                      console.log(`getFile2 失败,尝试使用 getHTML:`, error2)
-                      try {
-                        html = await api.getHTML(iframeSrc)
-                        console.log('getHTML 成功获取内容:', html)
-                      }
-                      catch (htmlError) {
-                        console.error('getHTML 也失败:', htmlError)
-                        console.error('无法获取内容: getFile、getFile2 和 getHTML 都失败了')
-                        // throw new Error(`无法获取内容: getFile、getFile2 和 getHTML 都失败了`)
-                      }
+                    console.log(`getFile2 失败,尝试使用 getHTML:`, error2)
+                    try {
+                      html = await api.getHTML(iframeSrc)
+                      console.log('getHTML 成功获取内容:', html)
+                    }
+                    catch (htmlError) {
+                      console.error('getHTML 也失败:', htmlError)
+                      console.error('无法获取内容: getFile、getFile2 和 getHTML 都失败了')
+                      // throw new Error(`无法获取内容: getFile、getFile2 和 getHTML 都失败了`)
+                    }
                   }
                   }
                   catch (error) {
                   catch (error) {
                     console.log(`getFile 失败,尝试使用 getFile2:`, error)
                     console.log(`getFile 失败,尝试使用 getFile2:`, error)
@@ -1906,13 +1907,17 @@ const handleHomeworkSubmit = async () => {
             // 直接对iframe内部的body进行截图
             // 直接对iframe内部的body进行截图
             const html2canvas = await import('html2canvas')
             const html2canvas = await import('html2canvas')
             const canvas = await html2canvas.default(iframeBody, {
             const canvas = await html2canvas.default(iframeBody, {
-              useCORS: true,
-              allowTaint: true,
-              scale: 1,
-              backgroundColor: '#ffffff',
-              logging: false,
-              foreignObjectRendering: true,
-              removeContainer: true
+              // useCORS: true,
+              // allowTaint: true,
+              // scale: 1,
+              // backgroundColor: '#ffffff',
+              // logging: false,
+              // foreignObjectRendering: true,
+              // removeContainer: true
+              scale: 2, // 提高清晰度
+              allowTaint: false, // 是否允许跨域污染画布
+              useCORS: true, // 尝试跨域加载图片
+              logging: true,
             })
             })
             imageData = canvas.toDataURL('image/png', 0.95)
             imageData = canvas.toDataURL('image/png', 0.95)
             
             
@@ -2083,7 +2088,7 @@ const handleHomeworkSubmit = async () => {
           '_js.onload = function(){\n' +
           '_js.onload = function(){\n' +
           ' var a = document.getElementsByTagName("img")\n' +
           ' var a = document.getElementsByTagName("img")\n' +
           ' for(var i = 0;i<a.length;i++){a[i].crossOrigin="anonymous"}\n' +
           ' for(var i = 0;i<a.length;i++){a[i].crossOrigin="anonymous"}\n' +
-          ' html2canvas(document.body).then(canvas => {\n' +
+          ' html2canvas(document.body, {scale: 2,allowTaint: false,useCORS: true,logging: true,}).then(canvas => {\n' +
           '  var base64Url = canvas.toDataURL("image/png");\n' +
           '  var base64Url = canvas.toDataURL("image/png");\n' +
           'var base64 = "<img src=" + base64Url + " />"\n' +
           'var base64 = "<img src=" + base64Url + " />"\n' +
           'var file = dataURLtoFile_shishi(base64Url, "截图")\n' +
           'var file = dataURLtoFile_shishi(base64Url, "截图")\n' +
@@ -3278,6 +3283,7 @@ const addOp3 = async (userTime: any, loadTime: any, object: any, status: any) =>
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
+  
   document.addEventListener('keydown', handleKeydown)
   document.addEventListener('keydown', handleKeydown)
 
 
   // 处理URL参数
   // 处理URL参数
@@ -3406,11 +3412,17 @@ onUnmounted(() => {
     authTokenUpdateTimer.value = null
     authTokenUpdateTimer.value = null
   }
   }
 
 
-  if (providerSocket.value) {
-    providerSocket.value.destroy()
-    providerSocket.value = null
+  // 清理 socket 连接检查定时器
+  if (socketCheckTimer.value) {
+    clearInterval(socketCheckTimer.value)
+    socketCheckTimer.value = null
   }
   }
 
 
+  // if (providerSocket.value) {
+  //   providerSocket.value.destroy()
+  //   providerSocket.value = null
+  // }
+
   // 清理画图延迟发送定时器
   // 清理画图延迟发送定时器
   if (drawingDelayTimer.value) {
   if (drawingDelayTimer.value) {
     clearTimeout(drawingDelayTimer.value)
     clearTimeout(drawingDelayTimer.value)
@@ -3524,7 +3536,7 @@ const manualReconnect = () => {
 }
 }
 
 
 // 创建WebSocket连接
 // 创建WebSocket连接
-const createWebSocketConnection = async () => {
+const createWebSocketConnection = async (type = 1) => {
   if (!api.yweb_socket || isConnecting.value) return
   if (!api.yweb_socket || isConnecting.value) return
   
   
   isConnecting.value = true
   isConnecting.value = true
@@ -3532,10 +3544,10 @@ const createWebSocketConnection = async () => {
   
   
   try {
   try {
     // 清理之前的连接
     // 清理之前的连接
-    if (providerSocket.value) {
-      providerSocket.value.destroy()
-      providerSocket.value = null
-    }
+    // if (providerSocket.value && type == 1) {
+    //   providerSocket.value.destroy()
+    //   providerSocket.value = null
+    // }
     
     
     // 清理之前的 token 更新定时器
     // 清理之前的 token 更新定时器
     if (authTokenUpdateTimer.value) {
     if (authTokenUpdateTimer.value) {
@@ -3639,7 +3651,7 @@ const createWebSocketConnection = async () => {
         console.log('👉 WebSocket连接断开')
         console.log('👉 WebSocket连接断开')
         connectionStatus.value = 'disconnected'
         connectionStatus.value = 'disconnected'
         isConnecting.value = false
         isConnecting.value = false
-        handleDisconnection()
+        createWebSocketConnection(2)
       }
       }
     })
     })
     
     
@@ -3648,7 +3660,7 @@ const createWebSocketConnection = async () => {
       console.error('👉 WebSocket连接错误:', error)
       console.error('👉 WebSocket连接错误:', error)
       connectionStatus.value = 'disconnected'
       connectionStatus.value = 'disconnected'
       isConnecting.value = false
       isConnecting.value = false
-      handleDisconnection()
+      createWebSocketConnection(2)
     })
     })
     
     
   }
   }
@@ -3656,8 +3668,11 @@ const createWebSocketConnection = async () => {
     console.error('👉 创建WebSocket连接失败:', error)
     console.error('👉 创建WebSocket连接失败:', error)
     connectionStatus.value = 'disconnected'
     connectionStatus.value = 'disconnected'
     isConnecting.value = false
     isConnecting.value = false
-    handleDisconnection()
+    createWebSocketConnection(2)
   }
   }
+
+  // 启动 socket 连接检查定时器
+  startSocketCheckTimer()
 }
 }
 
 
 // 处理连接断开
 // 处理连接断开
@@ -3668,7 +3683,7 @@ const handleDisconnection = () => {
     
     
     reconnectTimer.value = setTimeout(() => {
     reconnectTimer.value = setTimeout(() => {
       createWebSocketConnection()
       createWebSocketConnection()
-    }, reconnectInterval.value)
+    }, reconnectInterval.value) as unknown as NodeJS.Timeout
   }
   }
   else {
   else {
     console.error('👉 WebSocket重连次数已达上限,停止重连')
     console.error('👉 WebSocket重连次数已达上限,停止重连')
@@ -3677,6 +3692,29 @@ const handleDisconnection = () => {
   }
   }
 }
 }
 
 
+// 启动 socket 连接检查定时器
+const startSocketCheckTimer = () => {
+  // 清理之前的定时器
+  if (socketCheckTimer.value) {
+    clearInterval(socketCheckTimer.value)
+    socketCheckTimer.value = null
+  }
+  
+  // 每10秒检查一次 socket 连接状态
+  socketCheckTimer.value = setInterval(() => {
+    if (providerSocket.value) {
+      // 直接检查 providerSocket 的连接状态
+      // WebsocketProvider 有一个 connected 属性来表示连接状态
+      const isConnected = (providerSocket.value as any).ws.readyState
+      console.log('🔍 定时器检查 socket 连接状态:', isConnected)
+      if (isConnected !== 1) {
+        console.log('🔍 定时器检查发现 socket 未连接,执行重连')
+        createWebSocketConnection(2)
+      }
+    }
+  }, 10000) as unknown as NodeJS.Timeout
+}
+
 // 工具函数:格式化时间
 // 工具函数:格式化时间
 const formatTime = (totalSec: number) => {
 const formatTime = (totalSec: number) => {
   const m = Math.floor(totalSec / 60)
   const m = Math.floor(totalSec / 60)
@@ -4890,7 +4928,7 @@ const clearTimerState = () => {
 /* 作业提交按钮样式 */
 /* 作业提交按钮样式 */
 .homework-submit-btn {
 .homework-submit-btn {
   position: fixed;
   position: fixed;
-  bottom: 60px;
+  bottom: 160px;
   z-index: 100;
   z-index: 100;
   background: #191a19;
   background: #191a19;
   color: white;
   color: white;
@@ -4950,7 +4988,7 @@ const clearTimerState = () => {
 /* 刷新网页按钮样式 */
 /* 刷新网页按钮样式 */
 .refresh-page-btn {
 .refresh-page-btn {
   position: fixed;
   position: fixed;
-  bottom: 60px;
+  bottom: 160px;
   z-index: 100;
   z-index: 100;
   color: #000;
   color: #000;
   padding: 5px 20px;
   padding: 5px 20px;

+ 3 - 0
src/views/lang/cn.json

@@ -0,0 +1,3 @@
+ {
+  "lang": "cn"
+ }

+ 3 - 0
src/views/lang/en.json

@@ -0,0 +1,3 @@
+ {
+  "lang": "en"
+ }

+ 3 - 0
src/views/lang/hk.json

@@ -0,0 +1,3 @@
+ {
+  "lang": "hk"
+ }

Some files were not shown because too many files changed in this diff