فهرست منبع

feat(student): add result array status sync for type 2 pages

1. 新增isResultArray响应式状态与相关计算属性
2. 新增作业结果数组的消息同步逻辑,包括创建者初始化、广播和接收更新
3. 新增对应消息类型的清理和消息数组适配
4. 调整作业勾选框显示条件,适配type为2的页面
5. 修复WebSocket重连定时器逻辑,改为30分钟更新token并添加重连延迟
lsc 1 هفته پیش
والد
کامیت
4ac112c676
2فایلهای تغییر یافته به همراه227 افزوده شده و 126 حذف شده
  1. 119 117
      src/views/Student/components/choiceQuestionDetailDialog.vue
  2. 108 9
      src/views/Student/index.vue

+ 119 - 117
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -15,7 +15,7 @@
             props.showData.choiceQuestionListData[props.showData.workIndex]
               .teststitle
           }}</div>
-          <div class="c_t45_msg">
+          <div class="c_t45_msg" v-if="props.roleType == 1">
             <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
                 v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                   props.showData.unsubmittedStudents.length
@@ -47,7 +47,7 @@
         }">
           <div id="echartsArea1" ref="echartsArea1"></div>
         </div>
-        <div class="aiAnalysis" v-if="props.workArray.length > 0">
+        <div class="aiAnalysis" v-if="props.workArray.length > 0 && props.roleType == 1">
           <div class="ai_header">
             <div class="ai_title">
               <svg viewBox="0 0 1024 1024" width="200" height="200">
@@ -108,7 +108,7 @@
             props.showData.choiceQuestionListData[props.showData.workIndex]
               .teststitle
           }}</div>
-          <div class="c_t45_msg">
+          <div class="c_t45_msg" v-if="props.roleType == 1">
             <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
                 v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                   props.showData.unsubmittedStudents.length
@@ -199,7 +199,7 @@
       <div class="c_t15" v-if="workDetail && workDetail.type === '15' && props.showData">
         <div class="c_t15_title">{{ workDetail.json.answerQ }}</div>
         <span class="c_t15_type">{{ lang.ssQATest }}</span>
-        <div class="c_t15_msg">
+        <div class="c_t15_msg" v-if="props.roleType == 1">
           <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
               v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                 props.showData.unsubmittedStudents.length
@@ -220,7 +220,7 @@
         </div>
 
         <div class="aiAnalysis" style="margin-top:1rem ;"
-          v-if="processedWorkArray.length > 0 && lookWorkData === null && workDetail.type === '15'"
+          v-if="processedWorkArray.length > 0 && lookWorkData === null && workDetail.type === '15' && props.roleType == 1"
           @click.stop="clickContent(false)">
           <div class="ai_header">
             <div class="ai_title">
@@ -279,7 +279,7 @@
       <div class="c_t15" v-if="workDetail && workDetail.type === '79' && props.showData">
         <div class="c_t15_title">{{ workDetail.json.answerQ }}</div>
         <span class="c_t15_type">{{ lang.ssPhoto }}</span>
-        <div class="c_t15_msg">
+        <div class="c_t15_msg" v-if="props.roleType == 1">
           <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
               v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                 props.showData.unsubmittedStudents.length
@@ -342,7 +342,7 @@
       <div class="c_t72" v-if="props.showData && props.showData.toolType === 72">
         <div class="c_t72_title">{{ lang.ssAiApp }}</div>
         <span class="c_t72_type">{{ lang.ssAiApp }}</span>
-        <div class="c_t72_msg">
+        <div class="c_t72_msg" v-if="props.roleType == 1">
           <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
               v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                 props.showData.unsubmittedStudents.length
@@ -360,7 +360,7 @@
         </div>
 
         <div class="aiAnalysis" style="margin-top:1rem ;"
-          v-if="processedWorkArray.length > 0 && lookWorkData === null && props.showData.toolType === 72"
+          v-if="processedWorkArray.length > 0 && lookWorkData === null && props.showData.toolType === 72 && props.roleType == 1"
           @click.stop="clickContent(false)">
           <div class="ai_header">
             <div class="ai_title">
@@ -442,7 +442,7 @@
       <div class="c_t73" v-if="props.showData && props.showData.toolType === 73">
         <div class="c_t73_title">{{ lang.ssPageImage }}</div>
         <span class="c_t73_type">{{ lang.ssHPage }}</span>
-        <div class="c_t73_msg">
+        <div class="c_t73_msg" v-if="props.roleType == 1">
           <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
               v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                 props.showData.unsubmittedStudents.length
@@ -504,6 +504,7 @@ const props = defineProps<{
   workId: string;
   cid: string;
   workUrl: string;
+  roleType: number;
 }>()
 
 const emit = defineEmits<{
@@ -779,7 +780,7 @@ const setEchartsArea1 = () => {
           fontSize: 17,
           lineHeight: 20,
           interval: 0,
-          formatter: function (value: any, idx: number) {
+          formatter: function(value: any, idx: number) {
             // 如果是字符串且格式为JSON(图片),则解析处理
             if (typeof value === 'string') {
               try {
@@ -1390,7 +1391,7 @@ const getWordCloud15 = () => {
 const aiAnalysisRefresh72 = async () => {
 
   let chatMsg = ``
-  let agentId = 'a7741704-ba56-40b7-a6b8-62a423ef9376'
+  const agentId = 'a7741704-ba56-40b7-a6b8-62a423ef9376'
   console.log('processedWorkArray.value', processedWorkArray.value)
   processedWorkArray.value.forEach((i) => {
     if (typeof i.content === 'object') {
@@ -1484,7 +1485,7 @@ ${a.content}\n`
   }
   else if (['6c56ec0e-2c74-11ef-bee5-005056b86db5', 'aea65da6-4399-11f1-9985-005056924926'].includes(props.userId)) {
 
-    if(props.workUrl=='https://knowledge.cocorobo.cn/zh-CN/story-telling/a1a339d4-f522-4336-9aa9-e8394bea9731'){
+    if (props.workUrl == 'https://knowledge.cocorobo.cn/zh-CN/story-telling/a1a339d4-f522-4336-9aa9-e8394bea9731') {
       msg = `# 角色定位
 
 你是K-12阶段的AI课堂分析助手,负责基于学生对世界名画的英语宣传稿(promotional text)批改记录生成课堂学情分析报告。
@@ -1670,7 +1671,8 @@ ${a.content}\n`
 当前页面答题数据(AI应用):【分析重点】
 - AI应用
 - 对话数据:${chatMsg}`
-    }else if(props.workUrl=='https://knowledge.cocorobo.cn/zh-CN/story-telling/0d04cef1-876a-41b4-9768-6547088bc162'){
+    }
+    else if (props.workUrl == 'https://knowledge.cocorobo.cn/zh-CN/story-telling/0d04cef1-876a-41b4-9768-6547088bc162') {
       msg = `# 角色定位
 
 你是K-12阶段的AI课堂分析助手,负责基于学生词句训练对话记录生成课堂学情分析报告。
@@ -1832,110 +1834,110 @@ ${a.content}\n`
 - 对话数据:${chatMsg}`
     }
 
-//     msg = `# 角色定位
-// 你是K-12阶段的AI课堂分析助手,负责基于学生词句训练对话记录生成课堂学情分析报告。
-// 本次分析的环节为:学生与AI就画作内容进行词句问答训练,分为 Level A、Level B、Level C 三个等级。
-// ---
-// # 重要:数据解读规则(必须在生成报告前完成)
-// 在输出任何报告内容之前,必须先完成以下两步结构化提取。
-// 所有报告数字必须来源于此,禁止前后矛盾。
-// ---
-// ## 第一步:逐学生提取数据
-// (此步骤为内部处理步骤,不输出提取结果。)
-// ### 等级识别
-// 从对话记录中的 sender 字段识别等级:
-// - sender 含"Level A" → Level A
-// - sender 含"Level B" → Level B
-// - sender 含"Level C" → Level C
-// ### 完成状态判断
-// - **完整完成**:所有句子/问题均有学生回应记录
-// - **部分完成**:至少一条学生回应,但未完成全部
-// - **未完成**:对话记录中仅有 AI 开场消息,无任何学生回应内容
-// ### Level A 提取项目
-// **主题覆盖性:** 检查学生是否主动提问了以下 6 个主题:
-// ① name ② artist ③ kind ④ scene ⑤ colours ⑥ why
-// - ✅ 已提问 / — 未提问
-// **问题准确性(语法):** 识别以下错误类型并记录人次:
-// - 疑问句结构错误 / 主谓一致错误 / 时态错误 / 其他
-// **创新性:** 6 个主题之外的问题,记录原文。
-// ### Level B 提取项目
-// **主题覆盖性:**(同 Level A,6个主题)
-// **问题准确性(语法):**(同 Level A)
-// ### Level C 提取项目
-// **流程完成性:** 逐句核对学生是否跟读了全部 6 个问句。
-// - ✅ 跟读基本正确 / ⚠️ 跟读明显偏差 / — 未跟读
-// **创新亮点:** 学生是否有自发延伸表达,记录原文。
-// ---
-// ## 第二步:汇总数据
-// 将所有学生数据按等级分组汇总。
-// **报告中所有数字必须与汇总表严格一致,禁止前后矛盾。**
-// ---
-// # 输出格式
-// ## 第一步输出
-// 输出以下一句话,然后立即继续输出完整报告,不得停止:
-// "正在逐个提取学生数据中,请稍候……"
-// ## 第二步输出:完整报告
-// 紧接上一句,输出以下完整报告内容:
-// ---
-// ## 词句问答训练 课堂学情分析报告
-// **数据来源:** 学生对话记录 | **统计人数:** X 人
-// ---
-// ### 一、分层总览
-// | 等级 | 人数 | 完整完成 | 主题覆盖率均值 | 语法问题人数 | 创新提问/亮点 |
-// |---|---|---|---|---|---|
-// | Level A | X | X人(XX%) | XX% | X人 | X人 |
-// | Level B | X | X人(XX%) | XX% | X人 | — |
-// | Level C | X | X人(XX%) | — | — | X人 |
-// [若有未完成学生:<span style="color:red">⚠️ 未完成学生:[姓名列表]</span>]
-// ---
-// ### 二、主题覆盖明细(Level A / B)
-// | 主题 | A 覆盖率 | B 覆盖率 |
-// |---|---|---|
-// | name | XX% | XX% |
-// | artist | XX% | XX% |
-// | kind | XX% | XX% |
-// | scene | XX% | XX% |
-// | colours | XX% | XX% |
-// | why | XX% | XX% |
-// *若本次无某等级学生,对应列填"—"。*
-// ---
-// ### 三、语法问题(Level A / B)
-// | 问题类型 | A 出现人次 | B 出现人次 |
-// |---|---|---|
-// | 疑问句结构错误 | X | X |
-// | 主谓一致错误 | X | X |
-// | 时态错误 | X | X |
-// | 其他 | X | X |
-// *若无语法问题,写:"本次无明显语法问题。✅"*
-// ---
-// ### 四、创新与亮点
-// **Level A 创新提问:**
-// [列出学生姓名及原文;若无,写"本次暂无。"]
-// **Level C 自发延伸:**
-// [列出学生姓名及原文;若无,写"本次暂无。"]
-// ---
-// ### 五、总结
-// **整体:** [1句,简述全班完成情况。]
-// **突出:** [1句,指出表现最好的维度或等级。]
-// **关注:** <span style="color:red">[1句,指出覆盖率最低的主题或问题最集中的点。]</span>
-// **跟进:** <span style="color:red">[列出需个别跟进的学生姓名及原因;若无,写"全员表现均衡,暂无需特别跟进。"]</span>
-// ---
-// # 注意事项
-// - 所有数字来源于结构化提取,输出前核对一致性,禁止前后矛盾。
-// - 对需要教师重点关注的内容使用 <span style="color:red"> 内容 </span> 高亮。
-// - 若某等级无学生数据,相关行/列填"—",不单独输出该等级报告。
-// - 禁止输出"如需进一步生成"等对话式内容。
-// - 不输出教学建议或干预措施。
-
-// #INPUT#
-// 课程数据:
-// - 课程名称:${props.courseDetail.title}
-// - 课程学科:${props.courseDetail.name}
-// - 需要提交人数:${props.showData.workArray.length + props.showData.unsubmittedStudents.length}
-// - 已提交人数:${props.showData.workArray.length}
-// 当前页面答题数据(AI应用):【分析重点】
-// - AI应用
-// - 对话数据:${chatMsg}`
+    //     msg = `# 角色定位
+    // 你是K-12阶段的AI课堂分析助手,负责基于学生词句训练对话记录生成课堂学情分析报告。
+    // 本次分析的环节为:学生与AI就画作内容进行词句问答训练,分为 Level A、Level B、Level C 三个等级。
+    // ---
+    // # 重要:数据解读规则(必须在生成报告前完成)
+    // 在输出任何报告内容之前,必须先完成以下两步结构化提取。
+    // 所有报告数字必须来源于此,禁止前后矛盾。
+    // ---
+    // ## 第一步:逐学生提取数据
+    // (此步骤为内部处理步骤,不输出提取结果。)
+    // ### 等级识别
+    // 从对话记录中的 sender 字段识别等级:
+    // - sender 含"Level A" → Level A
+    // - sender 含"Level B" → Level B
+    // - sender 含"Level C" → Level C
+    // ### 完成状态判断
+    // - **完整完成**:所有句子/问题均有学生回应记录
+    // - **部分完成**:至少一条学生回应,但未完成全部
+    // - **未完成**:对话记录中仅有 AI 开场消息,无任何学生回应内容
+    // ### Level A 提取项目
+    // **主题覆盖性:** 检查学生是否主动提问了以下 6 个主题:
+    // ① name ② artist ③ kind ④ scene ⑤ colours ⑥ why
+    // - ✅ 已提问 / — 未提问
+    // **问题准确性(语法):** 识别以下错误类型并记录人次:
+    // - 疑问句结构错误 / 主谓一致错误 / 时态错误 / 其他
+    // **创新性:** 6 个主题之外的问题,记录原文。
+    // ### Level B 提取项目
+    // **主题覆盖性:**(同 Level A,6个主题)
+    // **问题准确性(语法):**(同 Level A)
+    // ### Level C 提取项目
+    // **流程完成性:** 逐句核对学生是否跟读了全部 6 个问句。
+    // - ✅ 跟读基本正确 / ⚠️ 跟读明显偏差 / — 未跟读
+    // **创新亮点:** 学生是否有自发延伸表达,记录原文。
+    // ---
+    // ## 第二步:汇总数据
+    // 将所有学生数据按等级分组汇总。
+    // **报告中所有数字必须与汇总表严格一致,禁止前后矛盾。**
+    // ---
+    // # 输出格式
+    // ## 第一步输出
+    // 输出以下一句话,然后立即继续输出完整报告,不得停止:
+    // "正在逐个提取学生数据中,请稍候……"
+    // ## 第二步输出:完整报告
+    // 紧接上一句,输出以下完整报告内容:
+    // ---
+    // ## 词句问答训练 课堂学情分析报告
+    // **数据来源:** 学生对话记录 | **统计人数:** X 人
+    // ---
+    // ### 一、分层总览
+    // | 等级 | 人数 | 完整完成 | 主题覆盖率均值 | 语法问题人数 | 创新提问/亮点 |
+    // |---|---|---|---|---|---|
+    // | Level A | X | X人(XX%) | XX% | X人 | X人 |
+    // | Level B | X | X人(XX%) | XX% | X人 | — |
+    // | Level C | X | X人(XX%) | — | — | X人 |
+    // [若有未完成学生:<span style="color:red">⚠️ 未完成学生:[姓名列表]</span>]
+    // ---
+    // ### 二、主题覆盖明细(Level A / B)
+    // | 主题 | A 覆盖率 | B 覆盖率 |
+    // |---|---|---|
+    // | name | XX% | XX% |
+    // | artist | XX% | XX% |
+    // | kind | XX% | XX% |
+    // | scene | XX% | XX% |
+    // | colours | XX% | XX% |
+    // | why | XX% | XX% |
+    // *若本次无某等级学生,对应列填"—"。*
+    // ---
+    // ### 三、语法问题(Level A / B)
+    // | 问题类型 | A 出现人次 | B 出现人次 |
+    // |---|---|---|
+    // | 疑问句结构错误 | X | X |
+    // | 主谓一致错误 | X | X |
+    // | 时态错误 | X | X |
+    // | 其他 | X | X |
+    // *若无语法问题,写:"本次无明显语法问题。✅"*
+    // ---
+    // ### 四、创新与亮点
+    // **Level A 创新提问:**
+    // [列出学生姓名及原文;若无,写"本次暂无。"]
+    // **Level C 自发延伸:**
+    // [列出学生姓名及原文;若无,写"本次暂无。"]
+    // ---
+    // ### 五、总结
+    // **整体:** [1句,简述全班完成情况。]
+    // **突出:** [1句,指出表现最好的维度或等级。]
+    // **关注:** <span style="color:red">[1句,指出覆盖率最低的主题或问题最集中的点。]</span>
+    // **跟进:** <span style="color:red">[列出需个别跟进的学生姓名及原因;若无,写"全员表现均衡,暂无需特别跟进。"]</span>
+    // ---
+    // # 注意事项
+    // - 所有数字来源于结构化提取,输出前核对一致性,禁止前后矛盾。
+    // - 对需要教师重点关注的内容使用 <span style="color:red"> 内容 </span> 高亮。
+    // - 若某等级无学生数据,相关行/列填"—",不单独输出该等级报告。
+    // - 禁止输出"如需进一步生成"等对话式内容。
+    // - 不输出教学建议或干预措施。
+
+    // #INPUT#
+    // 课程数据:
+    // - 课程名称:${props.courseDetail.title}
+    // - 课程学科:${props.courseDetail.name}
+    // - 需要提交人数:${props.showData.workArray.length + props.showData.unsubmittedStudents.length}
+    // - 已提交人数:${props.showData.workArray.length}
+    // 当前页面答题数据(AI应用):【分析重点】
+    // - AI应用
+    // - 对话数据:${chatMsg}`
 
 
   }

+ 108 - 9
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 && props.type == '1'" v-show="currentSlideHasIframe" :style="{
+          <div class="homework-check-box" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && (props.type == '1' || (props.type == '2' && currentIsResultArray.value.can))"  v-show="currentSlideHasIframe" :style="{
             top: isFullscreen ? '0' : `0`
           }">
             <div class="homework-check-box-item" @click="openChoiceQuestionDetail2(slideIndex)" :class="{'active': !choiceQuestionDetailDialogOpenList.includes(slideIndex)}">
@@ -106,7 +106,7 @@
           <ScreenSlideList :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
             :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
 
-          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType !== 77" :cid="props.cid" :workId="workId" :workUrl="workUrl" :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
+          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType !== 77" :roleType="props.type" :cid="props.cid" :workId="workId" :workUrl="workUrl" :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
           <SpeakingClassPanel
             v-else-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType === 77"
             ref="speakingPanelRef"
@@ -640,6 +640,7 @@ const rightPanelMode = ref<'homework' | 'dialogue' | 'choice' | ''>('homework')
 const courseDetail = ref<any>({})
 const aiAssistant = ref<boolean>(false)
 const studentArray = ref<any>([])
+const isResultArray = ref<any>([])
 
 // 跟随模式相关状态
 const isCreator = ref(false) // 是否为创建人
@@ -669,6 +670,7 @@ const yWritingBoardState = ref<any | null>(null)
 const yTimerMessages = ref<any | null>(null)
 const yLaserMessages = ref<any | null>(null)
 const yWritingBoardMessages = ref<any | null>(null)
+const yIsResultArrayMessages = ref<any | null>(null)
 const providerSocket = ref<WebsocketProvider | null>(null)
 // 学生端画图同步数据
 const writingBoardSyncDataURL = ref<string | null>(null)
@@ -1076,6 +1078,11 @@ const currentSlideHasEnglishSpeaking = computed(() => {
   )
 })
 
+// 检查当前的结果状态
+const currentIsResultArray = computed(() => {
+  return isResultArray.value[slideIndex.value] || {}
+})
+
 
 // 跳转到指定幻灯片
 const goToSlide = (index: number) => {
@@ -2778,6 +2785,29 @@ const getCourseDetail = async () => {
           const contentDescription = pptContent.join('\n')
           checkPPTFile(contentDescription)
           importJSON(jsonObj)
+          if (isCreator.value) { 
+            console.log('jsonObj', jsonObj)
+            for (let i = 0; i < jsonObj.slides.length; i++) {
+              const notTool = [74, 75, 76, 82]
+              const elements = jsonObj.slides[i].elements.some((element: any) => {
+                return element.type === 'frame' && !notTool.includes(element.toolType)
+              })
+              const json = {
+                isTool: elements,
+                can: false,
+                like: false,
+              }
+              isResultArray.value[i] = json
+            }
+            setIsResultArray()
+            // 广播消息
+            sendMessage({ 
+              type: 'isResultArray', 
+              isResultArray: isResultArray.value,
+              courseid: props.courseid 
+            })
+            console.log(isResultArray.value)
+          }
         }
         catch (e) {
           console.error('解析pptdata.data失败:', e)
@@ -3013,6 +3043,18 @@ const checkParentMode = () => {
   }
 }
 
+const setIsResultArray = () => {
+  // @ts-ignore
+  if (window.parent && typeof window.parent.setIsResultArray === 'function') {
+    // @ts-ignore
+    window.parent.setIsResultArray(isResultArray.value)
+  }
+}
+
+const setCan = (Array: any[]) => {
+  isResultArray.value = Array
+}
+
 const forceLogout = () => {
   sendMessage({ type: 'logout' })
 }
@@ -3157,6 +3199,39 @@ const messageInit = () => {
     })
   }
 
+  if (!yIsResultArrayMessages.value) {
+    console.log('初始化isResultArray消息数组')
+    yIsResultArrayMessages.value = docSocket.value.getArray('isResultArrayMessages')
+
+    // 初始化时检查一次是否需要清理
+    if (yIsResultArrayMessages.value && yIsResultArrayMessages.value.length > 1 && isCreator.value && docSocket.value) {
+      smartCleanupMessages(yIsResultArrayMessages.value, 1, 'isResultArray', docSocket.value)
+    }
+
+    yIsResultArrayMessages.value.observe((e: any) => {
+      console.log('yIsResultArrayMessages', yIsResultArrayMessages.value.length)
+      console.log('yIsResultArrayMessages', yIsResultArrayMessages.value)
+      
+      // 每次有新消息添加后,检查是否需要清理
+      if (yIsResultArrayMessages.value && yIsResultArrayMessages.value.length > 1 && isCreator.value && docSocket.value) {  
+        // 使用 setTimeout 确保在 observe 回调执行完成后再清理
+        setTimeout(() => {
+          if (docSocket.value && yIsResultArrayMessages.value && yIsResultArrayMessages.value.length > 1) {
+            smartCleanupMessages(yIsResultArrayMessages.value, 1, 'isResultArray', docSocket.value)
+          }
+        }, 0)
+      }
+      
+      e.changes.added.forEach((i: any) => {
+        const message = i.content.getContent()[0]
+        if (message.mId !== mId.value) {
+          getMessages(message)
+        }
+      })
+    })
+    
+  }
+
   // 如果是首次进入且是创建者,清空所有同步状态
   if (isFirstEnter.value && isCreator.value && docSocket.value) {
     console.log('🧹 首次进入且为创建者,清空所有同步状态')
@@ -3269,13 +3344,15 @@ const isSpecialMessageType = (type: string): boolean => {
   const timerTypes = ['timer_start', 'timer_pause', 'timer_reset', 'timer_stop', 'timer_finish', 'timer_update']
   const laserTypes = ['laser_toggle'] // laser_move 直接通过 Map,不通过消息数组
   const writingBoardTypes = ['writing_board_update', 'writing_board_close', 'writing_board_blackboard']
-  return timerTypes.includes(type) || laserTypes.includes(type) || writingBoardTypes.includes(type)
+  const isResultArrayTypes = ['isResultArray']
+  
+  return timerTypes.includes(type) || laserTypes.includes(type) || writingBoardTypes.includes(type) || isResultArrayTypes.includes(type)
 }
 
 /**
  * 判断是否为开关类型的消息(需要保留的状态消息)
  */
-const isToggleMessageType = (type: string, category: 'timer' | 'laser' | 'writingBoard'): boolean => {
+const isToggleMessageType = (type: string, category: 'timer' | 'laser' | 'writingBoard' | 'isResultArray'): boolean => {
   if (category === 'laser') {
     return type === 'laser_toggle'
   }
@@ -3285,13 +3362,16 @@ const isToggleMessageType = (type: string, category: 'timer' | 'laser' | 'writin
   if (category === 'timer') {
     return ['timer_start', 'timer_stop', 'timer_reset'].includes(type)
   }
+  if (category === 'isResultArray') {
+    return ['isResultArray'].includes(type)
+  }
   return false
 }
 
 /**
  * 智能清理消息数组,保留最新的开关消息
  */
-const smartCleanupMessages = (messageArray: any, maxLength: number, category: 'timer' | 'laser' | 'writingBoard', docSocket: Y.Doc) => {
+const smartCleanupMessages = (messageArray: any, maxLength: number, category: 'timer' | 'laser' | 'writingBoard' | 'isResultArray', docSocket: Y.Doc) => {
   if (!messageArray || messageArray.length <= maxLength) return
   
   const allMessages = messageArray.toArray()
@@ -3347,6 +3427,7 @@ const getMessageArrayByType = (type: string): any | null => {
   const timerTypes = ['timer_start', 'timer_pause', 'timer_reset', 'timer_stop', 'timer_finish', 'timer_update']
   const laserTypes = ['laser_toggle'] // laser_move 直接通过 Map,不通过消息数组
   const writingBoardTypes = ['writing_board_update', 'writing_board_close', 'writing_board_blackboard']
+  const isResultArrayTypes = ['isResultArray']
   
   if (timerTypes.includes(type)) {
     return yTimerMessages.value
@@ -3357,6 +3438,9 @@ const getMessageArrayByType = (type: string): any | null => {
   if (writingBoardTypes.includes(type)) {
     return yWritingBoardMessages.value
   }
+  if (isResultArrayTypes.includes(type)) {
+    return yIsResultArrayMessages.value
+  }
   return null
 }
 
@@ -3532,6 +3616,10 @@ const getMessages = (msgObj: any) => {
     }
   }
 
+  // 获取是否展开结果数组
+  if (props.type == '2' && msgObj.type === 'isResultArray' && msgObj.courseid === props.courseid) {
+    isResultArray.value = msgObj.isResultArray || []
+  }
 }
 
 
@@ -3740,6 +3828,7 @@ onMounted(() => {
     manualReconnect,
     connectionStatus: computed(() => connectionStatus.value),
     forceLogout,
+    setCan,
   }
 
   console.log('PPTist Student View 已加载,可通过 window.PPTistStudent 访问功能')
@@ -3969,7 +4058,11 @@ const createWebSocketConnection = async (type = 1) => {
     )
     
     // 启动定期更新 token
-    updateAuthToken()
+    // 30分钟后再次更新
+    if (authTokenUpdateTimer.value) {
+      clearTimeout(authTokenUpdateTimer.value)
+    }
+    authTokenUpdateTimer.value = setTimeout(updateAuthToken, 30 * 60 * 1000) as unknown as NodeJS.Timeout
 
     providerSocket.value.on('status', (event: any) => {
       console.log('👉 WebSocket状态:', event.status)
@@ -4042,7 +4135,9 @@ const createWebSocketConnection = async (type = 1) => {
         console.log('👉 WebSocket连接断开')
         connectionStatus.value = 'disconnected'
         isConnecting.value = false
-        createWebSocketConnection(2)
+        setTimeout(() => {
+          createWebSocketConnection(2)
+        }, 5000)
       }
     })
     
@@ -4051,7 +4146,9 @@ const createWebSocketConnection = async (type = 1) => {
       console.error('👉 WebSocket连接错误:', error)
       connectionStatus.value = 'disconnected'
       isConnecting.value = false
-      createWebSocketConnection(2)
+      setTimeout(() => {
+        createWebSocketConnection(2)
+      }, 5000)
     })
     
   }
@@ -4059,7 +4156,9 @@ const createWebSocketConnection = async (type = 1) => {
     console.error('👉 创建WebSocket连接失败:', error)
     connectionStatus.value = 'disconnected'
     isConnecting.value = false
-    createWebSocketConnection(2)
+    setTimeout(() => {
+      createWebSocketConnection(2)
+    }, 5000)
   }
 
   // 启动 socket 连接检查定时器