Sfoglia il codice sorgente

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

jack 5 giorni fa
parent
commit
e454d32d95

+ 20 - 2
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -383,6 +383,20 @@ import useSlideHandler from '@/hooks/useSlideHandler'
 const { createSlide } = useSlideHandler()
 const { createFrameElement } = useCreateElement()
 
+const setUrl = () => {
+  let url = 'https://beta.pbl.cocorobo.cn'
+  if (lang.lang === 'cn') {
+    url = 'https://pbl.cocorobo.cn/'
+  }
+  else if (lang.lang === 'hk') {
+    url = 'https://pbl.cocorobo.hk/'
+  }
+  else if (lang.lang === 'en') {
+    url = 'https://pbl.cocorobo.com/'
+  }
+  return url
+}
+
 const generate = (message: ChatMessage) => {
   if (message.gLoading || message.jsonData?.isGenerate) {
     return
@@ -400,7 +414,8 @@ const generate = (message: ChatMessage) => {
       console.log('选择题', JSON.parse(res))
       gType.value = 'chat'
       setPageId(45, res).then(res => {
-        const url = `https://beta.pbl.cocorobo.cn/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${45}`
+        const baseUrl = setUrl()
+        const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${45}`
         createSlide()
         createFrameElement(url, 45)
         message.gLoading = false
@@ -420,7 +435,8 @@ const generate = (message: ChatMessage) => {
       console.log('问答题', JSON.parse(res))
       gType.value = 'chat'
       setPageId(15, res).then(res => {
-        const url = `https://beta.pbl.cocorobo.cn/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${15}`
+        const baseUrl = setUrl()
+        const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${15}`
         createSlide()
         createFrameElement(url, 15)
         message.gLoading = false
@@ -735,6 +751,8 @@ onMounted(() => {
 }
 
 .message-content {
+    word-break: break-word;
+
     &.ai-message {
         align-self: flex-start;
         background: #fafbfc;

+ 2 - 0
src/plugins/icon.ts

@@ -130,6 +130,7 @@ import {
   More,
   LoadingFour, // 引入loadingIcon
   UpTwo,
+  Refresh,
 } from '@icon-park/vue-next'
 
 export interface Icons {
@@ -265,6 +266,7 @@ export const icons: Icons = {
   IconMore: More,
   IconLoading: LoadingFour, // 添加loadingIcon
   UpTwo: UpTwo,
+  IconRefresh: Refresh,
 }
 
 export default {

+ 23 - 10
src/tools/aiChat.ts

@@ -45,7 +45,12 @@ export const chat_no_stream = (msg: string, agentId: string, userId: string, lan
     const params: ChatParams = {
       ...DEFAULT_PARAMS,
       id: agentId,
-      message: msg,
+      message: `Language: ${language === 'en'
+        ? 'English'
+        : language === 'hk'
+          ? 'Traditional Chinese'
+          : 'Chinese'
+      } ${msg}`,
       uid: uuidv4(),
       stream: false,
       model: agentData?.modelType || 'open-doubao',
@@ -105,7 +110,12 @@ export const chat_stream = async (
     ...DEFAULT_PARAMS,
     id: agentId,
     file_ids: file_ids || [],
-    message: msg,
+    message: `Language: ${language === 'en'
+      ? 'English'
+      : language === 'hk'
+        ? 'Traditional Chinese'
+        : 'Chinese'
+    } ${msg}`,
     uid: uuidv4(),
     stream: true,
     model: agentData?.modelType || 'open-doubao',
@@ -159,6 +169,10 @@ export const chat_stream = async (
       throw err
     },
   }).catch(err => {
+    onMessage({
+      type: 'error',
+      data: err || 'Unknown error'
+    })
     console.log('err', err)
   })
   
@@ -301,7 +315,7 @@ export const getAgentChatList = async (id: string, userId: string): Promise<any[
       }
 
       try {
-        json.jsonData = item.jsonData ? JSON.parse(decodeURIComponent(item.jsonData)) : null
+        json.jsonData = item.jsonData !== 'undefined' && item.jsonData !== null ? JSON.parse(decodeURIComponent(item.jsonData)) : null
         // 从 jsonData 中读取 syncTranscriptionText 值
         if (json.jsonData && json.jsonData.syncTranscriptionText !== undefined) {
           json.syncTranscriptionText = json.jsonData.syncTranscriptionText
@@ -359,6 +373,7 @@ export interface InsertChatParams {
   latestMessage?: any;
   agentHeadUrl?: string;
   agentAssistantName?: string;
+  jsonData?: any;
 }
 
 export interface InsertChatResult {
@@ -376,18 +391,16 @@ export const insertChat = async (params: InsertChatParams): Promise<InsertChatRe
     fileId,
     latestMessage,
     agentHeadUrl,
-    agentAssistantName
+    agentAssistantName,
+    jsonData
   } = params
 
-  const jsonData: any = {
+  const jsonData2: any = {
     headUrl: agentHeadUrl || '',
-    assistantName: agentAssistantName || ''
+    assistantName: agentAssistantName || '',
+    ...jsonData
   }
 
-  // 如果存在sourceArray,则添加到jsonData中
-  if (latestMessage && latestMessage.jsonData && latestMessage.jsonData.sourceArray) {
-    jsonData.sourceArray = latestMessage.jsonData.sourceArray
-  }
 
   try {
     const response = await axios.post('https://gpt4.cocorobo.cn/insert_chat', {

+ 116 - 25
src/views/Student/components/aiChat.vue

@@ -30,9 +30,9 @@
             </div>
           </div>
         </div>
-        <div class="message-content ai-message chat" v-if="message.aiContent || message.loading">
+        <div class="message-content ai-message chat" v-if="message.aiContent || message.loading || message?.jsonData?.error || !message.aiContent">
           <div v-if="message.aiContent" v-html="message.aiContent"></div>
-          <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32"
+          <svg v-else-if="message.loading" xmlns="http://www.w3.org/2000/svg" width="32" height="32"
             viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
             <circle cx="4" cy="12" r="3" fill="currentColor">
               <animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s"
@@ -47,6 +47,12 @@
                 calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
             </circle>
           </svg>
+          <div v-else-if="message?.jsonData?.error || !message.aiContent" class="error-message">
+            <div class="error-text">{{ message?.jsonData?.errorMessage || lang.ssRetryMessage }}</div>
+            <button class="retry-btn" @click="retryMessage(index)">
+              <IconRefresh class="retry-icon"/>
+            </button>
+          </div>
         </div>
       </div>
     </div>
@@ -141,6 +147,9 @@ interface ChatMessage {
     url?: string
   }>
   jsonData?: {
+    error?: boolean
+    errorMessage?: string
+    retryable?: boolean
     gType?: string
     isGenerate?: boolean
     headUrl?: string
@@ -197,6 +206,9 @@ watch(() => props.visible, (newVal) => {
         chatLoading.value = true
         sendAction('')
       }
+      else {
+        prevChatResult()
+      }
     })
   }
 })
@@ -328,6 +340,9 @@ const sendMessage = () => {
       title: file.title,
       id: file.id
     }))
+    if (!messages.value.at(-1).jsonData) {
+      messages.value.at(-1).jsonData = {}
+    }
     chatLoading.value = true
     sendAction(inputText.value)
     inputText.value = ''
@@ -424,6 +439,14 @@ const sendQuickAction = (action: string) => {
   sendMessage()
 }
 
+const retryMessage = (index: number) => {
+  const message = messages.value[index]
+  if (message && message?.jsonData?.retryable) {
+    inputText.value = message.content || ''
+    sendMessage()
+  }
+}
+
 import { v4 as uuidv4 } from 'uuid'
 const session_name = ref('')
 const slidesStore = useSlidesStore()
@@ -490,34 +513,66 @@ const sendAction = async (action: string) => {
   ${promptText}
   query:${action}
   `
-  chat_stream(prompt, agentid2.value, props.userid || '', lang.lang, (event) => {
-    if (event.type === 'message') {
-      messages.value.at(-1).aiContent = md.render(event.data)
-
-      messages.value.at(-1).loading = false
-      prevChatResult()
-    }
-    else if (event.type === 'messageEnd') {
-      messages.value.at(-1).aiContent = md.render(event.data)
-      messages.value.at(-1).chatloading = false
-      chatLoading.value = false
-      prevChatResult()
-      insertChat({
-        answer: messages.value.at(-1).aiContent,
-        problem: messages.value.at(-1).content,
-        type: 'chat',
-        alltext: messages.value.at(-1).aiContent,
-        assistant_id: props.workJson.id
-      })
-    }
-  }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
+  
+  // 设置超时
+  const timeoutPromise = new Promise<never>((_, reject) => {
+    setTimeout(() => {
+      reject(new Error('请求超时'))
+    }, 30000) // 30秒超时
+  })
+  
+  try {
+    const controller = await Promise.race([
+      chat_stream(prompt, agentid2.value, props.userid || '', lang.lang, (event) => {
+        if (event.type === 'message') {
+          messages.value.at(-1).aiContent = md.render(event.data)
+          messages.value.at(-1).loading = false
+          prevChatResult()
+        }
+        else if (event.type === 'messageEnd') {
+          messages.value.at(-1).aiContent = md.render(event.data)
+          messages.value.at(-1).chatloading = false
+          chatLoading.value = false
+          prevChatResult()
+          addChat()
+        }
+        else if (event.type === 'error') {
+          errorSet()
+        }
+      }, session_name.value, messages.value.at(-1)?.sourceFiles?.map(file => file.id).filter(Boolean)),
+      timeoutPromise
+    ])
+    
     streamController.value = controller
-  }).catch(err => {
-    chatLoading.value = false
+  }
+  catch (err) {
     console.log('err', err)
+    errorSet()
     stopMessage()
+  }
+}
+
+const errorSet = () => {
+  chatLoading.value = false
+  messages.value.at(-1).chatloading = false
+  messages.value.at(-1).loading = false
+  messages.value.at(-1).jsonData.error = true
+  messages.value.at(-1).jsonData.errorMessage = lang.ssRetryMessage
+  messages.value.at(-1).jsonData.retryable = true
+  addChat()
+}
+
+const addChat = () => {
+  insertChat({
+    answer: messages.value.at(-1).aiContent,
+    problem: messages.value.at(-1).content,
+    type: 'chat',
+    alltext: messages.value.at(-1).aiContent,
+    assistant_id: props.workJson.id,
+    jsonData: messages.value.at(-1).jsonData
   })
 }
+
 import useCreateElement from '@/hooks/useCreateElement'
 import useSlideHandler from '@/hooks/useSlideHandler'
 const { createSlide } = useSlideHandler()
@@ -895,6 +950,8 @@ onMounted(() => {
 }
 
 .message-content {
+  word-break: break-word;
+
   &.ai-message {
     align-self: flex-start;
     background: #fff;
@@ -917,6 +974,40 @@ onMounted(() => {
   }
 }
 
+.error-message {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: #ef4444;
+  font-size: 14px;
+
+  .error-text {
+    flex: 1;
+  }
+
+  .retry-btn {
+    background: none;
+    border: none;
+    cursor: pointer;
+    color: #ff9300;
+    padding: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.3s ease;
+
+    .retry-icon {
+      width: 16px;
+      height: 16px;
+    }
+
+    &:hover {
+      color: #e68a00;
+      transform: rotate(180deg);
+    }
+  }
+}
+
 .initial-state {
   display: flex;
   flex-direction: column;

+ 11 - 6
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -574,15 +574,20 @@ const setEchartsArea1 = () => {
                 // 非JSON字符串,直接返回
                 // 如果文本文字超过8个字,换行
                 if (typeof value === 'string') {
+                  // 判断value是否全是英文
+                  const isAllEnglish = /^[a-zA-Z0-9\s\p{P}]+$/u.test(value)
                   let displayValue = value
-                  if (value.length > 20) {
-                    displayValue = value.substr(0, 20) + '...'
+                  const maxLength = isAllEnglish ? 30 : 20
+                  const lineLength = isAllEnglish ? 16 : 8
+                  
+                  if (value.length > maxLength) {
+                    displayValue = value.substr(0, maxLength) + '...'
                   }
-                  // 8个字换行
+                  // 换行处理
                   let output = ''
-                  for (let i = 0; i < displayValue.length; i += 8) {
-                    output += displayValue.substr(i, 8)
-                    if (i + 8 < displayValue.length) {
+                  for (let i = 0; i < displayValue.length; i += lineLength) {
+                    output += displayValue.substr(i, lineLength)
+                    if (i + lineLength < displayValue.length) {
                       output += '\n'
                     }
                   }

+ 265 - 239
src/views/Student/index.vue

@@ -78,7 +78,7 @@
           left: isFullscreen ? '0' : `${(containerWidth - slideWidth * canvasScale) / 2}px`,
           top: isFullscreen ? '0' : `${(containerHeight - slideHeight * canvasScale) / 2}px`
         }" @mousemove="handleLaserMove">
-          <div class="homework-check-box" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo" v-show="currentSlideHasIframe" :style="{
+          <div class="homework-check-box" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && props.type == '1'" v-show="currentSlideHasIframe" :style="{
             top: isFullscreen ? '0' : `15px`
           }">
             <div class="homework-check-box-item" @click="openChoiceQuestionDetail2(slideIndex)" :class="{'active': !choiceQuestionDetailDialogOpenList.includes(slideIndex)}">
@@ -88,13 +88,13 @@
               <div class="homework-check-box-item-title">{{ lang.ssAnswer }}</div>
             </div>
           </div>
-          <div class="aiBtn" ref="aiBtnRef" v-if="isQuestionFrame && hasWork && false" 
+          <div class="aiBtn" ref="aiBtnRef" v-if="isQuestionFrame && hasWork && props.type == '2'" 
             :style="{ right: aiBtnPosition.x + 'px', bottom: aiBtnPosition.y + 'px' }" @click="openAiChat">
             <IconComment class="aiBtn-icon" />
             <span>AI对话</span>
           </div>
-          <aiChat v-show="visibleAIChat && props.cid" :position="aiBtnPosition" @close="visibleAIChat = false" :userid="props.userid" :workJson="myWork" :visible="visibleAIChat" :cid="props.cid"/>
-         <!-- && props.type == '2' -->
+          <aiChat v-show="visibleAIChat" :position="aiBtnPosition" @close="visibleAIChat = false" :userid="props.userid" :workJson="myWork" :visible="visibleAIChat" :cid="props.cid"/>
+         <!--  -->
           <div class="viewport" v-if="false">
             <div class="background" :style="backgroundStyle"></div>
 
@@ -1452,241 +1452,12 @@ const processIframeLinks = async () => {
             slide.elements.map(async (element) => {
               // 检查是否是iframe元素
               if (element.type === ElementTypes.FRAME && element.url) {
-                let iframeSrc = element.url
-                const toolType = element.toolType
-                console.log('当前版本:', currentVersion)
-                // 替换beta环境域名
-                iframeSrc = iframeSrc.replace(/https?:\/\/beta\.pbl\.cocorobo\.cn/g, 'https://pbl.cocorobo.cn')
-
-                // 根据当前版本统一域名
-                const versionMap = {
-                  cn: /cocorobo\.(hk|com)/g,
-                  hk: /cocorobo\.(cn|com)/g,
-                  com: /cocorobo\.(cn|hk)/g
-                }
-
-                const targetDomain = `cocorobo.${currentVersion}`
-                iframeSrc = iframeSrc.replace(versionMap[currentVersion], targetDomain)
-
-                if (iframeSrc.includes('setWorkPage')) {
-                  iframeSrc = iframeSrc.replace(/setWorkPage/g, 'workPageNew')
-                }
-
-                if (iframeSrc.includes('workPage')) {
-                  hasIframe = true
-                  console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
-
-                  try {
-                    // 解析URL,处理hash部分
-                    let baseUrl = iframeSrc
-                    let hashPart = ''
-
-                    // 分离base URL和hash部分
-                    if (iframeSrc.includes('#')) {
-                      const parts = iframeSrc.split('#')
-                      baseUrl = parts[0]
-                      hashPart = parts[1]
-                    }
-
-                    // 构建新的hash部分,添加参数
-                    // 使用当前幻灯片索引作为task参数
-                    let newHash = hashPart
-                    if (newHash.includes('?')) {
-                      // 如果hash中已经有查询参数,添加&
-                      newHash += `&courseid=${props.courseid || ''}&userid=${props.userid || ''}&stage=0&task=${slideIndex}&tool=0`
-                    } 
-                    else {
-                      // 如果hash中没有查询参数,添加?
-                      newHash += `?courseid=${props.courseid || ''}&userid=${props.userid || ''}&stage=0&task=${slideIndex}&tool=0`
-                    }
-
-                    // 构建新的URL
-                    let newUrl = `${baseUrl}#${newHash}`
-
-                    console.log(`幻灯片 ${slideIndex + 1} 的iframe链接已更新:`, newUrl)
-                      
-                    if (window.location.href.includes('beta') && !newUrl.includes('beta')) {
-                      newUrl = newUrl.replace('pbl.cocorobo.cn', 'beta.pbl.cocorobo.cn')
-                    }
-                    else if (newUrl.includes('beta') && !window.location.href.includes('beta')) {
-                      newUrl = newUrl.replace('beta.pbl.cocorobo.cn', 'pbl.cocorobo.cn')
-                    }
-                    // 返回更新后的元素
-                    return {
-                      ...element,
-                      url: newUrl
-                    }
-                  } 
-                  catch (error) {
-                    console.error(`处理幻灯片 ${slideIndex + 1} 的iframe链接时出错:`, error)
-                    return element
-                  }
-                }
-                else if (iframeSrc.includes('aichat.cocorobo') || iframeSrc.includes('knowledge.cocorobo')) {
-                  hasIframe = true
-                  try {
-                    // 解析URL,处理hash部分
-                    let baseUrl = iframeSrc
-                    let hashPart = ''
-                    let isHashPart = false
-                    // 分离base URL和hash部分
-                    if (iframeSrc.includes('#')) {
-                      const parts = iframeSrc.split('#')
-                      baseUrl = parts[0]
-                      hashPart = parts[1]
-                      isHashPart = true
-                    }
-
-                    // 构建新的hash部分,添加参数
-                    // 使用当前幻灯片索引作为task参数
-                    let newHash = hashPart
-                    if (newHash.includes('?')) {
-                      // 如果hash中已经有查询参数,添加&
-                      newHash += `&courseid=${props.courseid || ''}&layout=laptop`
-                    } 
-                    else {
-                      // 如果hash中没有查询参数,添加?
-                      newHash += `?courseid=${props.courseid || ''}&layout=laptop`
-                    }
-
-                    // 构建新的URL
-                    let newUrl = `${baseUrl}#${newHash}`
-                    if (!isHashPart) {
-                      newUrl = `${baseUrl}${newHash}`
-                    }
-
-                    console.log(`幻灯片 ${slideIndex + 1} 的iframe链接已更新:`, newUrl)
-                    // 返回更新后的元素
-                    return {
-                      ...element,
-                      url: newUrl
-                    }
-                  }
-                  catch (error) {
-                    console.error(`处理幻灯片 ${slideIndex + 1} 的iframe链接时出错:`, error)
-                    return element
-                  }
-                }
-                else if (toolType == 76) {
-                  hasIframe = true
-                  try {
-                    // 解析URL,处理hash部分
-                    let baseUrl = iframeSrc
-                    let hashPart = ''
-
-                    // 分离base URL和hash部分
-                    if (iframeSrc.includes('#')) {
-                      const parts = iframeSrc.split('#')
-                      baseUrl = parts[0]
-                      hashPart = parts[1]
-                    }
-
-                    // 构建新的hash部分,添加参数
-                    // 使用当前幻灯片索引作为task参数
-                    let newHash = hashPart
-                    if (newHash.includes('?')) {
-                      // 如果hash中已经有查询参数,添加&
-                      newHash += `&mode=pptMode`
-                    } 
-                    else {
-                      // 如果hash中没有查询参数,添加?
-                      newHash += `?mode=pptMode`
-                    }
-
-                    // 构建新的URL
-                    const newUrl = `${baseUrl}#${newHash}`
-
-                    console.log(`幻灯片 ${slideIndex + 1} 的iframe链接已更新:`, newUrl)
-                    // 返回更新后的元素
-                    return {
-                      ...element,
-                      url: newUrl
-                    }
-                  }
-                  catch (error) {
-                    console.error(`处理幻灯片 ${slideIndex + 1} 的iframe链接时出错:`, error)
-                    return element
-                  }
-                }
-                else if (toolType == 73) {
-                  hasIframe = true
-                  
-                  // 先尝试获取iframe的contentWindow,如果获取不到再使用HTML方式
-                  try {
-                    // 创建一个临时的iframe来测试是否能获取contentWindow
-                    const tempIframe = document.createElement('iframe')
-                    tempIframe.style.display = 'none'
-                    tempIframe.src = iframeSrc
-                    
-                    // 先将临时iframe添加到body,否则onload事件不会触发
-                    document.body.appendChild(tempIframe)
-                    // 等待iframe加载完成
-                    await new Promise((resolve, reject) => {
-                      tempIframe.onload = resolve
-                      tempIframe.onerror = reject
-                      // 可选:设置超时时间,避免长时间无响应
-                      setTimeout(() => reject(new Error('Timeout')), 5000)
-                    })
-                    
-                    // 尝试获取contentWindow
-                    if (tempIframe.contentWindow && tempIframe.contentWindow.document) {
-                      console.log(`iframe ${iframeSrc} 可以获取contentWindow,使用直接加载方式`)
-                      // 移除临时iframe
-                      document.body.removeChild(tempIframe)
-                      
-                      return {
-                        ...element,
-                        isHTML: false,
-                        url: iframeSrc
-                      }
-                    } 
-                    // 加载完成但无法获取contentWindow,也要移除iframe
-                    document.body.removeChild(tempIframe)
-                    
-                  }
-                  catch (error) {
-                    console.log(`iframe ${iframeSrc} 无法获取contentWindow,使用HTML方式:`, error)
-                  }
-                  
-                  // 如果无法获取contentWindow,使用HTML方式
-                  let html = null
-                  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 都失败了`)
-                    }
-                  }
-                  catch (error) {
-                    console.log(`getFile 失败,尝试使用 getFile2:`, error)
-                    try {
-                      const fileData = await getFile(iframeSrc)
-                      if (fileData && fileData.data) {
-                        const uint8Array = new Uint8Array(fileData.data)
-                        html = new TextDecoder('utf-8').decode(uint8Array)
-                        console.log('getFile 成功获取内容:', html)
-                      }
-                    }
-                    catch (error2) {
-                      const fileData2 = await getFile2(iframeSrc)
-                      if (fileData2 && fileData2.data) {
-                        const uint8Array = new Uint8Array(fileData2.data)
-                        html = new TextDecoder('utf-8').decode(uint8Array)
-                        console.log('getFile2 成功获取内容:', html)
-                      }
-                    }
-                  }
-                  console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
-                  return {
-                    ...element,
-                    isHTML: true,  
-                    url: html
-                  }
+                const { element: updatedElement, hasIframe: updatedHasIframe } = await elementDone(element, slideIndex)
+                hasIframe = updatedHasIframe
+                console.log('更新后的iframe元素:', updatedElement)
+                return {
+                  ...updatedElement,
+                  isDone: true
                 }
               }
 
@@ -1724,6 +1495,261 @@ const processIframeLinks = async () => {
   }
 }
 
+const elementDone = async (element: any, slideIndex: number) => {
+  let hasIframe = false
+  let _element = {...element}
+
+  let iframeSrc = element.url
+  const toolType = element.toolType
+  console.log('当前版本:', currentVersion)
+  // 替换beta环境域名
+  iframeSrc = iframeSrc.replace(/https?:\/\/beta\.pbl\.cocorobo\.cn/g, 'https://pbl.cocorobo.cn')
+
+  // 根据当前版本统一域名
+  const versionMap = {
+    cn: /cocorobo\.(hk|com)/g,
+    hk: /cocorobo\.(cn|com)/g,
+    com: /cocorobo\.(cn|hk)/g
+  }
+
+  const targetDomain = `cocorobo.${currentVersion}`
+  iframeSrc = iframeSrc.replace(versionMap[currentVersion], targetDomain)
+
+  if (iframeSrc.includes('setWorkPage')) {
+    iframeSrc = iframeSrc.replace(/setWorkPage/g, 'workPageNew')
+  }
+
+  if (iframeSrc.includes('workPage')) {
+    hasIframe = true
+    console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
+
+    try {
+      // 解析URL,处理hash部分
+      let baseUrl = iframeSrc
+      let hashPart = ''
+
+      // 分离base URL和hash部分
+      if (iframeSrc.includes('#')) {
+        const parts = iframeSrc.split('#')
+        baseUrl = parts[0]
+        hashPart = parts[1]
+      }
+
+      // 构建新的hash部分,添加参数
+      // 使用当前幻灯片索引作为task参数
+      let newHash = hashPart
+      if (newHash.includes('?')) {
+        // 如果hash中已经有查询参数,添加&
+        newHash += `&courseid=${props.courseid || ''}&userid=${props.userid || ''}&stage=0&task=${slideIndex}&tool=0`
+      } 
+      else {
+        // 如果hash中没有查询参数,添加?
+        newHash += `?courseid=${props.courseid || ''}&userid=${props.userid || ''}&stage=0&task=${slideIndex}&tool=0`
+      }
+
+      // 构建新的URL
+      let newUrl = `${baseUrl}#${newHash}`
+
+      console.log(`幻灯片 ${slideIndex + 1} 的iframe链接已更新:`, newUrl)
+                      
+      if (window.location.href.includes('beta') && !newUrl.includes('beta')) {
+        newUrl = newUrl.replace('pbl.cocorobo.cn', 'beta.pbl.cocorobo.cn')
+      }
+      else if (newUrl.includes('beta') && !window.location.href.includes('beta')) {
+        newUrl = newUrl.replace('beta.pbl.cocorobo.cn', 'pbl.cocorobo.cn')
+      }
+      // 返回更新后的元素
+      _element = {
+        ...element,
+        url: newUrl
+      }
+    } 
+    catch (error) {
+      console.error(`处理幻灯片 ${slideIndex + 1} 的iframe链接时出错:`, error)
+      return {
+        element: _element,
+        hasIframe
+      }
+    }
+  }
+  else if (iframeSrc.includes('aichat.cocorobo') || iframeSrc.includes('knowledge.cocorobo')) {
+    hasIframe = true
+    try {
+      // 解析URL,处理hash部分
+      let baseUrl = iframeSrc
+      let hashPart = ''
+      let isHashPart = false
+      // 分离base URL和hash部分
+      if (iframeSrc.includes('#')) {
+        const parts = iframeSrc.split('#')
+        baseUrl = parts[0]
+        hashPart = parts[1]
+        isHashPart = true
+      }
+
+      // 构建新的hash部分,添加参数
+      // 使用当前幻灯片索引作为task参数
+      let newHash = hashPart
+      if (newHash.includes('?')) {
+        // 如果hash中已经有查询参数,添加&
+        newHash += `&courseid=${props.courseid || ''}&layout=laptop`
+      } 
+      else {
+        // 如果hash中没有查询参数,添加?
+        newHash += `?courseid=${props.courseid || ''}&layout=laptop`
+      }
+
+      // 构建新的URL
+      let newUrl = `${baseUrl}#${newHash}`
+      if (!isHashPart) {
+        newUrl = `${baseUrl}${newHash}`
+      }
+
+      console.log(`幻灯片 ${slideIndex + 1} 的iframe链接已更新:`, newUrl)
+      // 返回更新后的元素
+      _element = {
+        ...element,
+        url: newUrl
+      }
+    }
+    catch (error) {
+      console.error(`处理幻灯片 ${slideIndex + 1} 的iframe链接时出错:`, error)
+      return {
+        element: _element,
+        hasIframe
+      }
+    }
+  }
+  else if (toolType === 76) {
+    hasIframe = true
+    try {
+      // 解析URL,处理hash部分
+      let baseUrl = iframeSrc
+      let hashPart = ''
+
+      // 分离base URL和hash部分
+      if (iframeSrc.includes('#')) {
+        const parts = iframeSrc.split('#')
+        baseUrl = parts[0]
+        hashPart = parts[1]
+      }
+
+      // 构建新的hash部分,添加参数
+      // 使用当前幻灯片索引作为task参数
+      let newHash = hashPart
+      if (newHash.includes('?')) {
+        // 如果hash中已经有查询参数,添加&
+        newHash += `&mode=pptMode`
+      } 
+      else {
+        // 如果hash中没有查询参数,添加?
+        newHash += `?mode=pptMode`
+      }
+
+      // 构建新的URL
+      const newUrl = `${baseUrl}#${newHash}`
+
+      console.log(`幻灯片 ${slideIndex + 1} 的iframe链接已更新:`, newUrl)
+      // 返回更新后的元素
+      _element = {
+        ...element,
+        url: newUrl
+      }
+    }
+    catch (error) {
+      console.error(`处理幻灯片 ${slideIndex + 1} 的iframe链接时出错:`, error)
+      return {
+        element: _element,
+        hasIframe
+      }
+    }
+  }
+  else if (toolType === 73) {
+    hasIframe = true
+                  
+    // 先尝试获取iframe的contentWindow,如果获取不到再使用HTML方式
+    try {
+      // 创建一个临时的iframe来测试是否能获取contentWindow
+      const tempIframe = document.createElement('iframe')
+      tempIframe.style.display = 'none'
+      tempIframe.src = iframeSrc
+                    
+      // 先将临时iframe添加到body,否则onload事件不会触发
+      document.body.appendChild(tempIframe)
+      // 等待iframe加载完成
+      await new Promise((resolve, reject) => {
+        tempIframe.onload = resolve
+        tempIframe.onerror = reject
+        // 可选:设置超时时间,避免长时间无响应
+        setTimeout(() => reject(new Error('Timeout')), 5000)
+      })
+                    
+      // 尝试获取contentWindow
+      if (tempIframe.contentWindow && tempIframe.contentWindow.document) {
+        console.log(`iframe ${iframeSrc} 可以获取contentWindow,使用直接加载方式`)
+        // 移除临时iframe
+        document.body.removeChild(tempIframe)
+                      
+        _element = {
+          ...element,
+          isHTML: false,
+          url: iframeSrc
+        }
+      } 
+      // 加载完成但无法获取contentWindow,也要移除iframe
+      document.body.removeChild(tempIframe)
+                    
+    }
+    catch (error) {
+      console.log(`iframe ${iframeSrc} 无法获取contentWindow,使用HTML方式:`, error)
+                  
+      // 如果无法获取contentWindow,使用HTML方式
+      let html = null
+      try {
+        html = await api.getHTML(iframeSrc)
+        console.log('getHTML 成功获取内容:', html)
+      }
+      catch (error) {
+        console.log(`getHTML 失败,尝试使用 getFile:`, error)
+        try {
+          const fileData = await getFile(iframeSrc)
+          if (fileData && fileData.data) {
+            const uint8Array = new Uint8Array(fileData.data)
+            html = new TextDecoder('utf-8').decode(uint8Array)
+            console.log('getFile 成功获取内容:', html)
+          }
+        }
+        catch (error2) {
+          console.log(`getFile 失败,尝试使用 getHTML:`, error2)
+          try {
+            const fileData2 = await getFile2(iframeSrc)
+            if (fileData2 && fileData2.data) {
+              const uint8Array = new Uint8Array(fileData2.data)
+              html = new TextDecoder('utf-8').decode(uint8Array)
+              console.log('getFile2 成功获取内容:', html)
+            }
+          }
+          catch (error3) {
+            console.error('getFile2 也失败:', error3)
+            console.error('无法获取内容: getFile、getFile2 和 getHTML 都失败了')
+          }
+        }
+      }
+      console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
+      _element = {
+        ...element,
+        isHTML: true,  
+        url: html
+      }
+    }
+  }
+
+  return {
+    element: _element,
+    hasIframe
+  }
+}
+ 
 // 导入JSON功能
 const importJSON = (jsonData: any) => {
   try {

+ 55 - 0
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -22,6 +22,11 @@
       :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
     >
       <div class="element-content">
+        <div class="fullscreen-spin mask" v-if="!elementInfo.isDone && !isThumbnail && isVisible">
+          <div class="spin">
+            <div class="spinner"></div>
+          </div>
+        </div>
         <!-- 视频类型(type 74):使用 video 标签 -->
         <video
           v-if="elementInfo.toolType === 74 && !isThumbnail && isVisible"
@@ -343,4 +348,54 @@ const handleIframeLoad = async (event: Event) => {
     left: 100%;
   }
 }
+
+.fullscreen-spin {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  &.mask {
+    background-color: rgba($color: #f1f1f1, $alpha: .7);
+  }
+}
+.spin {
+  width: 200px;
+  height: 200px;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  margin-top: -100px;
+  margin-left: -100px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+.spinner {
+  width: 36px;
+  height: 36px;
+  border: 3px solid #f6c82b;
+  border-top-color: transparent;
+  border-radius: 50%;
+  animation: spinner .8s linear infinite;
+}
+.text {
+  margin-top: 20px;
+  color: #f6c82b;
+}
+
+@keyframes spinner {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
 </style>

+ 2 - 1
src/views/lang/cn.json

@@ -737,5 +737,6 @@
   "ssAiChatWaitUpload": "请等待文件上传完成后再发送消息",
   "ssAiChatFileSizeLimit": "文件大小不能超过10MB",
   "ssAiChatUploadFailed": "文件上传失败:",
-  "ssClassroomAiAssistant": "课堂AI助手"
+  "ssClassroomAiAssistant": "课堂AI助手",
+  "ssRetryMessage": "网络有点慢,请稍后重试"
 }

+ 2 - 1
src/views/lang/en.json

@@ -737,5 +737,6 @@
   "ssAiChatWaitUpload": "Please wait for the file upload to complete before sending a message",
   "ssAiChatFileSizeLimit": "File size cannot exceed 10MB",
   "ssAiChatUploadFailed": "File upload failed:",
-  "ssClassroomAiAssistant": "Classroom AI Assistant"
+  "ssClassroomAiAssistant": "Classroom AI Assistant",
+  "ssRetryMessage": "Network is slow, please try again later"
 }

+ 2 - 1
src/views/lang/hk.json

@@ -737,5 +737,6 @@
   "ssAiChatWaitUpload": "請等待文件上傳完成後再發送消息",
   "ssAiChatFileSizeLimit": "文件大小不能超過10MB",
   "ssAiChatUploadFailed": "文件上傳失敗:",
-  "ssClassroomAiAssistant": "課堂AI助手"
+  "ssClassroomAiAssistant": "課堂AI助手",
+  "ssRetryMessage": "網絡稍慢,請稍後重試"
 }