7 Commits 3e59ef1edd ... bf692e9a53

Author SHA1 Message Date
  lsc bf692e9a53 refactor: 完善 iframe 加载成功的日志信息 1 week ago
  lsc dbf3dbff5b style: 注释掉调试用的信息提示 1 week ago
  lsc 3b488aa3e6 feat: 添加后端代理兜底抓取 HTML 的方法 1 week ago
  lsc 65918e0298 fix: 优化纯 JS 跨域抓 HTML 的代理方案,增加多代理 fallback 机制 1 week ago
  lsc bb44d4f70b fix(frame): 增加跨域抓取 HTML 的兜底方案 1 week ago
  lsc 2db9c001c0 fix(frame-element): 添加加载失败时的提示信息 1 week ago
  lsc 5fe6e79157 style: 修改日志级别,将warning改为info 1 week ago

+ 53 - 2
src/hooks/useImport.ts

@@ -1,4 +1,4 @@
-import { ref, nextTick } from 'vue'
+import { ref, nextTick } from 'vue'
 import { storeToRefs } from 'pinia'
 import { parse, type Shape, type Element, type ChartItem, type BaseElement } from 'pptxtojson'
 import { nanoid } from 'nanoid'
@@ -1934,7 +1934,7 @@ export default () => {
     return new Promise((resolve, reject) => {
       console.log('直接使用原始 URL 获取文件:', url)
 
-      fetch(url)
+      fetch(url, {mode: 'cors'})
         .then(response => {
           if (!response.ok) {
             console.error('HTTP 错误:', response.status, response.statusText)
@@ -1954,6 +1954,56 @@ export default () => {
     })
   }
 
+  // 纯 JS 跨域抓 HTML(最后兜底方案):通过公共 CORS 代理获取目标页面源码
+  // 用于 type=73 在 getHTML/getFile/getFile2 全部失败后的兜底
+  // 公共代理可用性不稳定(尤其国内网络),依次尝试多个,一个失败换下一个
+  const CORS_PROXIES: Array<(url: string) => string> = [
+    url => 'https://api.allorigins.win/raw?url=' + encodeURIComponent(url),
+    url => 'https://corsproxy.io/?url=' + encodeURIComponent(url),
+    url => 'https://api.codetabs.com/v1/proxy?quest=' + encodeURIComponent(url),
+  ]
+
+  const fetchViaProxy = (proxyFn: (url: string) => string, url: string): Promise<string> => {
+    return new Promise((resolve, reject) => {
+      const xhr = new XMLHttpRequest()
+      xhr.open('GET', proxyFn(url), true)
+      xhr.timeout = 12000 // iPad WebView 可能挂起不回调,必须加超时
+
+      xhr.onreadystatechange = function() {
+        if (xhr.readyState === 4) {
+          if (xhr.status === 200 && xhr.responseText) {
+            resolve(xhr.responseText)
+          }
+          else if (xhr.status !== 0) {
+            // status=0 是网络错误,交给 onerror 处理,避免重复 reject
+            reject(new Error('HTTP Error: ' + xhr.status))
+          }
+        }
+      }
+      xhr.ontimeout = function() {
+        reject(new Error('Request Timeout'))
+      }
+      xhr.onerror = function() {
+        reject(new Error('Network Error'))
+      }
+      xhr.send()
+    })
+  }
+
+  const getHtmlPureJS = async (url: string): Promise<string> => {
+    let lastError: unknown = null
+    for (const proxyFn of CORS_PROXIES) {
+      try {
+        return await fetchViaProxy(proxyFn, url)
+      }
+      catch (err) {
+        console.warn('CORS 代理失败,尝试下一个:', proxyFn(url).slice(0, 50), err)
+        lastError = err
+      }
+    }
+    throw lastError || new Error('All proxies failed')
+  }
+
   return {
     importSpecificFile,
     importJSON,
@@ -1964,6 +2014,7 @@ export default () => {
     imgExporting,
     getFile,
     getFile2,
+    getHtmlPureJS,
     dataToFile,
     uploadFileToS3,
     getPPTInfo

+ 11 - 0
src/services/course.ts

@@ -87,6 +87,16 @@ export const getHTML = (url: string, showError: boolean = false): Promise<any> =
   return axios.get(`${url}`, { showError })
 }
 
+/**
+ * 后端代理抓取 HTML(type=73 低版本内核/平板兜底)
+ * 平板与自家后端通信稳定,由后端代抓 S3 上的 H5 页面返回 HTML 原文
+ * @param url 目标URL
+ * @returns Promise<any>
+ */
+export const proxyHtml = (url: string, showError: boolean = false): Promise<any> => {
+  return axios.get(`${API_URL}proxyHtml?url=${encodeURIComponent(url)}`, { showError })
+}
+
 /**
  * 开启/关闭课程跟随模式
  * @param sopen 1否 2是
@@ -262,6 +272,7 @@ export default {
   selectSWorks,
   selectWorksStudent,
   getHTML,
+  proxyHtml,
   updateCourseFollow,
   updateCourseFollowC,
   selectCourseSLook,

+ 16 - 5
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -356,32 +356,43 @@ const handleIframeLoad = async (event: Event) => {
         slidesStore.updateElement({ id: info.id, props: { isDone: true } })
       }
       else {
-        // message.warning('type=73 跨域,尝试抓取 HTML 做 srcdoc')
+        // message.info('type=73 跨域,尝试抓取 HTML 做 srcdoc')
         console.log('type=73 跨域,尝试抓取 HTML 做 srcdoc')
         let html: string | null = null
         try {
           html = await api.getHTML(info.url)
         }
         catch (_e) {
+          // message.info('getHTML 失败,尝试 getFile' + _e)
           try {
             const fd = await getFile(info.url)
             if (fd?.data) html = new TextDecoder('utf-8').decode(new Uint8Array(fd.data))
           }
           catch (_e2) {
+            // message.info('getFile 失败,尝试 getFile2' + _e2)
             try {
               const fd2 = await getFile2(info.url)
               if (fd2?.data) html = new TextDecoder('utf-8').decode(new Uint8Array(fd2.data))
             }
             catch (e3) {
-              message.warning('type=73 抓取全部失败:' + e3)
-              console.error('type=73 抓取全部失败:', e3)
+              // message.info('getFile2 失败,尝试后端 proxyHtml' + e3)
+              // 最后兜底:自家后端代理代抓(放最后防止服务器过载)
+              // 不用在这里注入 <base>:processedSrcDoc 会根据 curl 统一注入
+              try {
+                const ph = await api.proxyHtml(info.url)
+                if (ph) html = ph
+              }
+              catch (error) {
+                message.warning('type=73 抓取全部失败:' + error)
+                console.error('type=73 抓取全部失败:', e3, error)
+              }
             }
           }
         }
 
         if (html) {
-          // message.warning('type=73 抓取成功,切换为 srcdoc' + html)
-          console.log('type=73 抓取成功,切换为 srcdoc')
+          // message.info('type=73 抓取成功,切换为 srcdoc' + html)
+          console.log('type=73 抓取成功,切换为 srcdoc' + html)
           // 更新 slidesStore:设 isHTML、url=html、curl=原始链接、isDone=true
           // 触发 watch url → iframeKey++ → iframe 重建为 srcdoc 模式
           slidesStore.updateElement({