Browse Source

fix: 修复 iPad Safari 下 iframe 加载异常及超时问题

优化临时 iframe 的隐藏方式,改用绝对定位偏移而非 display:none,避免 Safari 资源节流导致 onload 不触发
将超时时间从 5 秒放宽到 12 秒,适配 iPad 较慢的网络/渲染速度
改用 contentDocument 属性判断 iframe 加载状态,避免跨域时触发 SecurityError 异常
添加加载失败回退逻辑,防止白屏,保持页面显示一致性
lsc 1 week ago
parent
commit
7b8606f6bc
2 changed files with 62 additions and 21 deletions
  1. 33 12
      src/views/Student/index.vue
  2. 29 9
      src/views/Student/index2.vue

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

@@ -1852,7 +1852,13 @@ const elementDone = async (element: any, slideIndex: number) => {
     try {
     try {
       // 创建一个临时的iframe来测试是否能获取contentWindow
       // 创建一个临时的iframe来测试是否能获取contentWindow
       const tempIframe = document.createElement('iframe')
       const tempIframe = document.createElement('iframe')
-      tempIframe.style.display = 'none'
+      // 不用 display:none:iPad Safari/WKWebView 会对隐藏 iframe 资源节流,
+      // onload 迟迟不触发导致超时误判为"禁止嵌入",误入抓 HTML 路径(跨站 Cookie 被 ITP 拦截必然失败)
+      tempIframe.style.position = 'absolute'
+      tempIframe.style.left = '-9999px'
+      tempIframe.style.top = '-9999px'
+      tempIframe.style.width = '1px'
+      tempIframe.style.height = '1px'
       tempIframe.src = iframeSrc
       tempIframe.src = iframeSrc
                     
                     
       // 先将临时iframe添加到body,否则onload事件不会触发
       // 先将临时iframe添加到body,否则onload事件不会触发
@@ -1861,12 +1867,13 @@ const elementDone = async (element: any, slideIndex: number) => {
       await new Promise((resolve, reject) => {
       await new Promise((resolve, reject) => {
         tempIframe.onload = resolve
         tempIframe.onload = resolve
         tempIframe.onerror = reject
         tempIframe.onerror = reject
-        // 可选:设置超时时间,避免长时间无响应
-        setTimeout(() => reject(new Error('Timeout')), 5000)
+        // iPad 网络/渲染较慢,超时放宽到 12 秒
+        setTimeout(() => reject(new Error('Timeout')), 12000)
       })
       })
                     
                     
-      // 尝试获取contentWindow
-      if (tempIframe.contentWindow && tempIframe.contentWindow.document) {
+      // 尝试获取contentDocument(标准属性:跨域返回 null 不抛异常;
+      // 不能用 contentWindow.document,iPad Safari 跨域访问会直接抛 SecurityError,又会误入抓 HTML 路径)
+      if (tempIframe.contentDocument) {
         console.log(`iframe ${iframeSrc} 可以获取contentWindow,使用直接加载方式`)
         console.log(`iframe ${iframeSrc} 可以获取contentWindow,使用直接加载方式`)
         // 移除临时iframe
         // 移除临时iframe
         document.body.removeChild(tempIframe)
         document.body.removeChild(tempIframe)
@@ -1918,11 +1925,23 @@ const elementDone = async (element: any, slideIndex: number) => {
         }
         }
       }
       }
       console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
       console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
-      _element = {
-        ...element,
-        isHTML: true,  
-        url: html,
-        curl: iframeSrc
+      if (html) {
+        _element = {
+          ...element,
+          isHTML: true,
+          url: html,
+          curl: iframeSrc
+        }
+      }
+      else {
+        // getHTML/getFile/getFile2 全失败:不能置空 url 硬切 srcdoc(会白屏),
+        // 回退为直接加载原始链接,至少保持与进页面时一致的显示效果
+        console.warn(`幻灯片 ${slideIndex + 1} 未获取到HTML内容,回退为直接加载:`, iframeSrc)
+        _element = {
+          ...element,
+          isHTML: false,
+          url: iframeSrc
+        }
       }
       }
     }
     }
   }
   }
@@ -3968,8 +3987,10 @@ const openChoiceQuestionDetail3 = (index:number) => {
 
 
 const handlePageUnload = () => {
 const handlePageUnload = () => {
   if (isCreator.value && timerIndicator.value.visible && props.type === '1') {
   if (isCreator.value && timerIndicator.value.visible && props.type === '1') {
-    sendMessage({ type: 'timer_stop', courseid: props.courseid, isCreator: isCreator.value ? 'T' : 'F' })
+    // sendMessage({ type: 'timer_stop', courseid: props.courseid, isCreator: isCreator.value ? 'T' : 'F' })
+    onTimerStop()
   }
   }
+  // sad
   // 创建老师刷新/关闭页面时,清空所有同步状态
   // 创建老师刷新/关闭页面时,清空所有同步状态
   if (isCreator.value && props.type === '1') {
   if (isCreator.value && props.type === '1') {
     clearAllSyncStates()
     clearAllSyncStates()
@@ -4180,7 +4201,7 @@ onMounted(() => {
         '11. ws.readyState': providerSocket.value?.ws?.readyState,
         '11. ws.readyState': providerSocket.value?.ws?.readyState,
         '12. messageV3?.get("slideIndex")': msgV3Type?.get?.('slideIndex'),
         '12. messageV3?.get("slideIndex")': msgV3Type?.get?.('slideIndex'),
         '13. yTimerMesssage': doc.getArray('timerMessages'),
         '13. yTimerMesssage': doc.getArray('timerMessages'),
-        '14. timerState':docSocket.value.getMap('timerState')
+        '14. timerState': docSocket.value.getMap('timerState')
       }
       }
       console.log('════════ Yjs 诊断 ════════')
       console.log('════════ Yjs 诊断 ════════')
       for (const [k, v] of Object.entries(report)) {
       for (const [k, v] of Object.entries(report)) {

+ 29 - 9
src/views/Student/index2.vue

@@ -1817,7 +1817,13 @@ const elementDone = async (element: any, slideIndex: number) => {
     try {
     try {
       // 创建一个临时的iframe来测试是否能获取contentWindow
       // 创建一个临时的iframe来测试是否能获取contentWindow
       const tempIframe = document.createElement('iframe')
       const tempIframe = document.createElement('iframe')
-      tempIframe.style.display = 'none'
+      // 不用 display:none:iPad Safari/WKWebView 会对隐藏 iframe 资源节流,
+      // onload 迟迟不触发导致超时误判为"禁止嵌入",误入抓 HTML 路径(跨站 Cookie 被 ITP 拦截必然失败)
+      tempIframe.style.position = 'absolute'
+      tempIframe.style.left = '-9999px'
+      tempIframe.style.top = '-9999px'
+      tempIframe.style.width = '1px'
+      tempIframe.style.height = '1px'
       tempIframe.src = iframeSrc
       tempIframe.src = iframeSrc
                     
                     
       // 先将临时iframe添加到body,否则onload事件不会触发
       // 先将临时iframe添加到body,否则onload事件不会触发
@@ -1826,12 +1832,13 @@ const elementDone = async (element: any, slideIndex: number) => {
       await new Promise((resolve, reject) => {
       await new Promise((resolve, reject) => {
         tempIframe.onload = resolve
         tempIframe.onload = resolve
         tempIframe.onerror = reject
         tempIframe.onerror = reject
-        // 可选:设置超时时间,避免长时间无响应
-        setTimeout(() => reject(new Error('Timeout')), 5000)
+        // iPad 网络/渲染较慢,超时放宽到 12 秒
+        setTimeout(() => reject(new Error('Timeout')), 12000)
       })
       })
                     
                     
-      // 尝试获取contentWindow
-      if (tempIframe.contentWindow && tempIframe.contentWindow.document) {
+      // 尝试获取contentDocument(标准属性:跨域返回 null 不抛异常;
+      // 不能用 contentWindow.document,iPad Safari 跨域访问会直接抛 SecurityError,又会误入抓 HTML 路径)
+      if (tempIframe.contentDocument) {
         console.log(`iframe ${iframeSrc} 可以获取contentWindow,使用直接加载方式`)
         console.log(`iframe ${iframeSrc} 可以获取contentWindow,使用直接加载方式`)
         // 移除临时iframe
         // 移除临时iframe
         document.body.removeChild(tempIframe)
         document.body.removeChild(tempIframe)
@@ -1883,10 +1890,23 @@ const elementDone = async (element: any, slideIndex: number) => {
         }
         }
       }
       }
       console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
       console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
-      _element = {
-        ...element,
-        isHTML: true,  
-        url: html
+      if (html) {
+        _element = {
+          ...element,
+          isHTML: true,
+          url: html,
+          curl: iframeSrc
+        }
+      }
+      else {
+        // getHTML/getFile/getFile2 全失败:不能置空 url 硬切 srcdoc(会白屏),
+        // 回退为直接加载原始链接,至少保持与进页面时一致的显示效果
+        console.warn(`幻灯片 ${slideIndex + 1} 未获取到HTML内容,回退为直接加载:`, iframeSrc)
+        _element = {
+          ...element,
+          isHTML: false,
+          url: iframeSrc
+        }
       }
       }
     }
     }
   }
   }