Procházet zdrojové kódy

Merge branch 'beta' into feat/english-speaking-for-merge

# Conflicts:
#	src/views/Student/index.vue
#	src/views/lang/cn.json
#	src/views/lang/en.json
#	src/views/lang/hk.json
jimmylee před 2 dny
rodič
revize
3d30748f0b

+ 2 - 2
index.html

@@ -34,7 +34,7 @@
       .first-screen-loading-spinner {
         width: 36px;
         height: 36px;
-        border: 3px solid #d14424;
+        border: 3px solid #ff9300;
         border-top-color: transparent;
         border-radius: 50%;
         box-sizing: border-box;
@@ -42,7 +42,7 @@
       }
       .first-screen-loading-text {
         margin-top: 20px;
-        color: #d14424;
+        color: #ff9300;
       }
       @keyframes spinner {
         0% {

+ 23 - 6
src/assets/styles/prosemirror.scss

@@ -15,6 +15,11 @@
   p {
     margin: 0;
   }
+  .empty {
+    margin: 0;
+    font-size: 0;
+    padding: 0;
+  }
   p:first-child {
     margin-top: 0;
     // font-size: 0;
@@ -25,14 +30,21 @@
   }
   ul {
     
-    list-style-type: disc;
-    padding-inline-start: 0;
+    list-style-type: none;
+    padding-left: 1.5em;       /* 为伪元素留出空间 */
 
-    li {
-      list-style-type: inherit;
-      padding: 0;
+    li::before {
+      content: "•";
+      list-style-type: none;
+      padding: 0.125em 0;
+      float: left;
+      margin-right: 0.4em;
+    }
+
+    li.empty-li::before {
+      font-size: 0;
+      content: none !important;
     }
-    
   }
 
   ol {
@@ -42,6 +54,11 @@
     li {
       list-style-type: inherit;
       padding: 0.125em 0;
+      float: left;
+      margin-right: 0.4em;
+    }
+    li.empty-li::before {
+      content: none !important;
     }
   }
 

+ 9 - 1
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -355,12 +355,20 @@ const sendAction = async (action: string) => {
 
   // 提取当前页面中 type 为 'text' 的元素的纯文本内容
   const textContents = slidesStore.currentSlide?.elements
-    .filter((element: any) => element.type === 'text')
+    .filter((element: any) => element.type === 'text' || (element.type === 'shape' && element.text && element.text.content))
     .map((textElement: any) => {
+      if (textElement.type === 'shape') {
+        // 创建一个临时元素来解析 HTML 并提取纯文本
+        const tempElement = document.createElement('div')
+        tempElement.innerHTML = textElement.text.content
+        return tempElement.textContent || tempElement.innerText || ''
+      }
       // 创建一个临时元素来解析 HTML 并提取纯文本
       const tempElement = document.createElement('div')
       tempElement.innerHTML = textElement.content
       return tempElement.textContent || tempElement.innerText || ''
+      
+
     })
     .filter(content => content.trim() !== '') || []
   console.log('textContents', textContents)

+ 4 - 2
src/components/FullscreenSpin.vue

@@ -51,14 +51,16 @@ withDefaults(defineProps<{
 .spinner {
   width: 36px;
   height: 36px;
-  border: 3px solid $themeColor;
+  // border: 3px solid $themeColor;
+  border: 3px solid #ff9300;
   border-top-color: transparent;
   border-radius: 50%;
   animation: spinner .8s linear infinite;
 }
 .text {
   margin-top: 20px;
-  color: $themeColor;
+  // color: $themeColor;
+  color: #ff9300;
 }
 @keyframes spinner {
   0% {

+ 11 - 7
src/hooks/useImport.ts

@@ -1694,11 +1694,15 @@ export default () => {
 
       // 收集当前幻灯片内所有上传任务
       const uploadTasks: Promise<void>[] = []
-
+      
       // 遍历每一张幻灯片
       for (const item of json.slides) {
-        // ----- 解析背景 -----
-        const { type, value } = item.fill
+        let type = "", value = "";
+        if (item.fill) {
+          // ----- 解析背景 -----
+          type = item.fill.type
+          value = item.fill.value
+        }
         let background: SlideBackground
         if (type === 'image') {
           // 背景图片也可能需要上传(但 PPTX 背景图通常是内嵌 base64)
@@ -1706,7 +1710,7 @@ export default () => {
           background = {
             type: 'image',
             image: {
-              src: value.picBase64,
+              src: value?.picBase64,
               size: 'cover',
             },
           }
@@ -1715,12 +1719,12 @@ export default () => {
           background = {
             type: 'gradient',
             gradient: {
-              type: value.path === 'line' ? 'linear' : 'radial',
-              colors: value.colors.map(item => ({
+              type: value?.path === 'line' ? 'linear' : 'radial',
+              colors: value?.colors.map(item => ({
                 ...item,
                 pos: parseInt(item.pos),
               })),
-              rotate: value.rot + 90,
+              rotate: value?.rot + 90,
             },
           }
         }

+ 90 - 8
src/utils/prosemirror/schema/nodes.ts

@@ -24,8 +24,8 @@ const orderedList: NodeSpec = {
   content: 'list_item+',
   group: 'block',
   parseDOM: [
-    { 
-      tag: 'ol', 
+    {
+      tag: 'ol',
       getAttrs: dom => {
         const order = ((dom as HTMLElement).hasAttribute('start') ? (dom as HTMLElement).getAttribute('start') : 1) || 1
         const attr: Attr = { order: +order }
@@ -188,9 +188,33 @@ const listItem: NodeSpec = {
     }
     if (marginTop) style += `margin-top: ${marginTop};`;
     if (marginBottom) style += `margin-bottom: ${marginBottom};`;
-    if (marginLeft) style += `margin-left: ${marginLeft};`;
+    if (marginLeft) {
+      // 解析数值和单位
+      const str = String(marginLeft).trim();
+      const match = str.match(/^([+-]?\d*\.?\d+)(px|pt|em|rem|%|vw|vh)?$/i);
+      if (match) {
+        let num = parseFloat(match[1]);
+        const unit = match[2] || 'px';
+        const absNum = Math.abs(num); // 负数转正
+        const val = absNum + unit;
+        style += `margin-left: max(min(0px, 100% - ${val}), 0px);`;
+      } else {
+        style += `margin-left: ${marginLeft};`;
+      }
+    }
     if (marginRight) style += `margin-right: ${marginRight};`;
-    if (lineHeight) style += `line-height: ${lineHeight * 1.2};`;
+    if (lineHeight) {
+      let finalValue;
+      const str = String(lineHeight).trim();
+      // 匹配纯数字(整数或小数,可带负号)
+      if (/^-?\d+(\.\d+)?$/.test(str)) {
+        finalValue = parseFloat(str) * 1.2;
+      } else {
+        // 带单位或其他非纯数字内容,直接使用原值
+        finalValue = lineHeight;
+      }
+      style += `line-height: ${finalValue};`;
+    }
     if (paddingTop) style += `padding-top: ${paddingTop};`;
     if (paddingRight) style += `padding-right: ${paddingRight};`;
     if (paddingBottom) style += `padding-bottom: ${paddingBottom};`;
@@ -199,8 +223,23 @@ const listItem: NodeSpec = {
       style += `white-space: ${whiteSpace};`; // 添加 white-space
     }
 
-    const attrs: { style?: string } = {};
+    //const attrs: { style?: string } = {};
+    //if (style) attrs.style = style;
+
+    let isEmpty = false;
+    const firstChild = node.content.firstChild;
+    if (firstChild && firstChild.type.name === 'paragraph') {
+      // 段落无任何子节点(包括 text 和 inline 节点)
+      if (firstChild.content.size === 0) {
+        isEmpty = true;
+      }
+    }
+    // 如果整个 li 的内容长度为 0 也可以作为判断
+    if (node.content.size === 0) isEmpty = true;
+
+    const attrs: { style?: string; class?: string } = {};
     if (style) attrs.style = style;
+    if (isEmpty) attrs.class = 'empty-li';
 
     return ['li', attrs, 0];
   },
@@ -303,9 +342,36 @@ const paragraph: NodeSpec = {
     }
     if (marginTop) style += `margin-top: ${marginTop};`;
     if (marginBottom) style += `margin-bottom: ${marginBottom};`;
+    /*
+    if (marginLeft) {
+      // 解析数值和单位
+      const str = String(marginLeft).trim();
+      const match = str.match(/^([+-]?\d*\.?\d+)(px|pt|em|rem|%|vw|vh)?$/i);
+      if (match) {
+        let num = parseFloat(match[1]);
+        const unit = match[2] || 'px';
+        const absNum = Math.abs(num); // 负数转正
+        const val = absNum + unit;
+        style += `margin-left: max(min(0px, 100% - ${val}), 0px);`;
+      } else {
+        style += `margin-left: ${marginLeft};`;
+      }
+    }
+    */
     if (marginLeft) style += `margin-left: ${marginLeft};`;
     if (marginRight) style += `margin-right: ${marginRight};`;
-    if (lineHeight) style += `line-height: ${lineHeight * 1.2};`;
+    if (lineHeight) {
+      let finalValue;
+      const str = String(lineHeight).trim();
+      // 匹配纯数字(整数或小数,可带负号)
+      if (/^-?\d+(\.\d+)?$/.test(str)) {
+        finalValue = parseFloat(str) * 1.2;
+      } else {
+        // 带单位或其他非纯数字内容,直接使用原值
+        finalValue = lineHeight;
+      }
+      style += `line-height: ${finalValue};`;
+    }
     if (paddingTop) style += `padding-top: ${paddingTop};`;
     if (paddingRight) style += `padding-right: ${paddingRight};`;
     if (paddingBottom) style += `padding-bottom: ${paddingBottom};`;
@@ -314,8 +380,24 @@ const paragraph: NodeSpec = {
       style += `white-space: ${whiteSpace};`; // 添加 white-space
     }
 
-    const attr: Attr = { style };
-    return ['p', attr, 0];
+    const attrs: { style?: string; class?: string } = {};
+/*
+    let isEmpty = false;
+    const firstChild = node.content.firstChild;
+    if (firstChild) {
+      // 段落无任何子节点(包括 text 和 inline 节点)
+      if (firstChild.content.size === 0) {
+        isEmpty = true;
+      }
+    }
+    
+    // 如果整个 li 的内容长度为 0 也可以作为判断
+    if (node.content.size === 0) isEmpty = true;
+    if (style) attrs.style = style;
+    //if (isEmpty) attrs.class = 'empty';
+*/
+    if (style) attrs.style = style;
+    return ['p', attrs, 0];
   },
 };
 

+ 72 - 51
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -17,10 +17,11 @@
           }}</div>
           <div class="c_t45_msg">
             <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
-                v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.unsubmittedStudents.length
+                v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
+                  props.showData.unsubmittedStudents.length
                 }}</span></div>
             <span v-if="props.showData.unsubmittedStudents.length > 0" @click="viewUnsubmittedStudents()">{{
-              lang.ssViewUnsubmittedStudents }}</span>
+              lang.ssViewSubmitStatus2 }}</span>
           </div>
           <!--<span class="c_t45_t_btn" :class="{'c_t45_t_btn_noActive': props.showData.workIndex <= 0}" @click="changeWorkIndex(0)">{{ lang.ssPrevQ }}</span>-->
           <!--<span class="c_t45_t_btn" :class="{'c_t45_t_btn_noActive': props.showData.workIndex >= props.showData.choiceQuestionListData.length - 1}" @click="changeWorkIndex(1)">{{ lang.ssNextQ }}</span>-->
@@ -75,7 +76,8 @@
           <div class="ai_echartsData" v-if="currentAnalysis && currentAnalysis.json.keyword">
             {{ currentAnalysis.json.keyword }}
           </div>
-          <div class="generatingContent" v-if="currentAnalysis && currentAnalysis.loading">{{lang.ssGeneratingContent}}...</div>
+          <div class="generatingContent" v-if="currentAnalysis && currentAnalysis.loading">
+            {{ lang.ssGeneratingContent }}...</div>
           <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}
           </div>
         </div>
@@ -144,10 +146,11 @@
           </div>
           <div class="ai_echartsData" v-if="currentAnalysis && currentAnalysis.json.keyword">
             <div class="title">{{ lang.ssKeyword }}:</div>
-            <span v-for="(item,index) in currentAnalysis.json.keyword" :key="index">{{ item }}</span>
+            <span v-for="(item, index) in currentAnalysis.json.keyword" :key="index">{{ item }}</span>
             <div class="btn" @click="openEchatsDialog()">{{ lang.ssViewKeywordCloud }}</div>
           </div>
-          <div class="generatingContent" v-if="currentAnalysis && currentAnalysis.generatingContent">{{ lang.ssGeneratingContent }}...</div>
+          <div class="generatingContent" v-if="currentAnalysis && currentAnalysis.generatingContent">{{
+            lang.ssGeneratingContent }}...</div>
           <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}
           </div>
         </div>
@@ -316,6 +319,7 @@ const props = defineProps<{
   courseDetail: any;
   userId: string;
   workId: string;
+  cid: string;
 }>()
 
 const emit = defineEmits<{
@@ -731,7 +735,7 @@ const getAnalysis = () => {
   }
   console.log('props.workId', props.workId)
   const params = {
-    pid: props.workId,
+    pid: props.workId + (props.cid ? ',' + props.cid : ''),
   }
   axios.get('https://pbl.cocorobo.cn/api/pbl/select_pptAnalysisByPid?pid=' + params.pid).then(res => {
     const data = res[0]
@@ -868,7 +872,7 @@ watch(
 
 // 查看未提交学生
 const viewUnsubmittedStudents = () => {
-  selectUserDialogRef.value.open(lang.ssUnsubmittedStudents, { user: props.showData.unsubmittedStudents.map((item: any) => item.name) })
+  selectUserDialogRef.value.open(lang.ssSubmitStatus, { user: props.showData.unsubmittedStudents.map((item: any) => item.name), notFiledUser: props.showData.unsubmittedStudents.map((item: any) => item.name), isSubmitUser: props.workArray.map((item: any) => item.name) })
   // if (props.unsubmittedStudents.length > 0) {
   // unsubmittedStudentsDialogRef.value.open(props.unsubmittedStudents)
   // }
@@ -909,7 +913,7 @@ const aiAnalysisRefresh45 = () => {
 
   if (!currentAnalysis.value) {
     aiAnalysisData.value.push({
-      pid: props.workId,
+      pid: props.workId + (props.cid ? ',' + props.cid : ''),
       index: props.showData.workIndex,
       loading: true,
       generatingContent: true,
@@ -921,32 +925,32 @@ const aiAnalysisRefresh45 = () => {
   }
   else {
     aiAnalysisData.value.find((item: any) => {
-      return item.pid === props.workId && item.index === props.showData.workIndex
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
     }).loading = true
     aiAnalysisData.value.find((item: any) => {
-      return item.pid === props.workId && item.index === props.showData.workIndex
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
     }).json = { text: '', echartsData: '' }
   }
 
   chat_stream(msg, 'a7741704-ba56-40b7-a6b8-62a423ef9376', props.userId, lang.lang, (event) => {
     if (event.type === 'message') {
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).json.text = event.data
 
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).loading = false
     }
     else if (event.type === 'messageEnd') {
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).json.text = event.data
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).noEnd = false
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
       saveAnalysis()
     }
@@ -988,7 +992,7 @@ const aiAnalysisRefresh15 = () => {
 30人中15人提交问答题(参与率50%),核心概念“同理心地图”掌握薄弱。发现学生13在智能体对话中6次答“不知道”,需单独辅导设计思维基础概念。建议课程增加POV框架实例演练,强化痛点识别能力训练。`
   if (!currentAnalysis.value) {
     aiAnalysisData.value.push({
-      pid: props.workId,
+      pid: props.workId + (props.cid ? ',' + props.cid : ''),
       index: props.showData.workIndex,
       loading: true,
       generatingContent: true,
@@ -1000,19 +1004,19 @@ const aiAnalysisRefresh15 = () => {
   }
   else {
     aiAnalysisData.value.find((item: any) => {
-      return item.pid === props.workId && item.index === props.showData.workIndex
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
     }).loading = true
     aiAnalysisData.value.find((item: any) => {
-      return item.pid === props.workId && item.index === props.showData.workIndex
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
     }).json = { text: '', echartsData: '' }
     aiAnalysisData.value.find((item: any) => {
-      return item.pid === props.workId && item.index === props.showData.workIndex
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
     }).generatingContent = true
   }
   getWordCloud15().then((res) => {
     flag += 1
     aiAnalysisData.value.find((item: any) => {
-      return item.pid === props.workId && item.index === props.showData.workIndex
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
     }).generatingContent = false
     if (flag == 2) {
       saveAnalysis()
@@ -1022,22 +1026,22 @@ const aiAnalysisRefresh15 = () => {
     console.log(event)
     if (event.type === 'message') {
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).json.text = event.data
 
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).loading = false
     }
     else if (event.type === 'messageEnd') {
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).json.text = event.data
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).noEnd = false
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
       flag += 1
       if (flag == 2) {
@@ -1053,7 +1057,7 @@ const aiAnalysisRefresh15 = () => {
 // 打开词云图弹窗
 const openEchatsDialog = () => {
   if (echartsDialogRef.value && currentAnalysis.value.json.echartsData) {
-    echartsDialogRef.value.open(lang.ssKeywordCloud,currentAnalysis.value.json.echartsData)
+    echartsDialogRef.value.open(lang.ssKeywordCloud, currentAnalysis.value.json.echartsData)
   }
 }
 
@@ -1063,12 +1067,17 @@ const openEchatsDialog = () => {
 const getWordCloud15 = () => {
 
   return new Promise((resolve,) => {
-    const msg = `## 任务 请基于以下文本,提炼出10 - 30个关键词,用于绘制词云图。请给出相应的关键词,以及关键词出现的频次。请确保输出的关键字准确反映该段文本的主要内容和主题。
-## 要求
-1. ** 提取关键词 **:从提供的文本中提取出10 - 30个最具代表性的关键字。关键词应该涵盖该文本的主要概念、重要术语和核心主题。尽量选择多样化的关键词,避免过于集中在某一个主题或概念上。
-    2. ** 词频统计 **:计算每个关键字在文本中出现的频率。
-    3. ** 词汇大小 **:根据词频数量,确定每个关键字在词云图中的大小。词频越高,词汇大小数值越大,数值范围1 - 100。
-    4. ** 输出格式 **:输出结果应包含输出相应的关键词或元话语、对应的词频数量以及词汇大小数值,请以json格式输出,严格按照输出示例输出。
+    const msg = `## 任务
+请针对文本中学生提交的问答题回答内容进行深度分析,提炼出 10 - 30 个核心关键词,用于绘制词云图。
+## 提取准则
+1. 聚焦内容主体:仅从学生的具体回答文本中提取关键词。
+2. 严格排除杂质:禁止提取任何属于系统元数据或固定格式的词汇,包括但不限于:“课程数据”、“学生姓名”、“回答结果”、“课程标题”、“提交时间”、“作业名称”、“分数”等。
+3. 语义去重:将意思相近的词进行合并(例如“高效”与“效率高”),保留最具代表性的词条。
+4. 涵盖核心:关键词应准确反映学生回答中的核心观点、关键知识点、高频论据或情感倾向。
+## 任务要求
+1. 词频统计:计算每个有效关键字在回答内容中出现的频率。
+2. 词汇大小:根据词频,确定每个关键字在词云中的权重。词频越高,数值越大,范围 1 - 100。
+3. 输出格式:请严格按照 JSON 格式输出,包含关键词、词频及对应的词汇大小。
 
 ## 文本
 课程数据:
@@ -1076,7 +1085,7 @@ const getWordCloud15 = () => {
 - 课程学科:${props.courseDetail.name}
 当前页面答题数据(问答题):【分析重点】
 - 问答题题目:${props.showData.workDetail.json.answerQ}
-- 回答数据:${JSON.stringify(processedWorkArray.value.map((i) => ({ user: i.name, answer: i.content.answer })))}
+- 回答数据:${JSON.stringify(processedWorkArray.value.map((i) => ({ answer: i.content.answer })))}
 
 ## 输出示例
 {"tooltip":{"show":false},"series":[{"type":"wordCloud","sizeRange":[14,38],"rotationRange":[0,0],"keepAspect":false,"shape":"circle","left":"center","top":"center","right":null,"bottom":null,"width":"100%","height":"100%","rotationStep":20,"data":[{"value":"词汇大小,数值范围1-100","name":"词汇","textStyle":{"color":"词汇颜色(16进制)"}},{"value":"词汇大小,数值范围1-100","name":"词汇","textStyle":{"color":"(16进制)"}}]}]}`
@@ -1084,7 +1093,7 @@ const getWordCloud15 = () => {
 
     chat_no_stream(msg, 'a7741704-ba56-40b7-a6b8-62a423ef9376', props.userId, lang.lang).promise.then((res) => {
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).json.echartsData = JSON.parse(res)
       resolve(1)
     }).catch(err => {
@@ -1103,10 +1112,18 @@ const aiAnalysisRefresh72 = async () => {
 
   processedWorkArray.value.forEach((i) => {
     i.content.forEach(j => {
-      j.messages.forEach((a) => {
-        chatMsg += `\n${a.sender}:
+      if (j.messages) {
+        j.messages.forEach((a) => {
+          chatMsg += `\n${a.sender}:
 ${a.content}\n`
-      })
+        })
+      }
+      if(j.imageUrls){
+        j.imageUrls.forEach((a) => {
+          chatMsg += `\n${a}\n`
+        })
+      }
+
     })
 
   })
@@ -1145,7 +1162,7 @@ ${a.content}\n`
   console.log("cs", msg)
   if (!currentAnalysis.value) {
     aiAnalysisData.value.push({
-      pid: props.workId,
+      pid: props.workId + (props.cid ? ',' + props.cid : ''),
       index: props.showData.workIndex,
       loading: true,
       json: { text: '', echartsData: '' },
@@ -1156,32 +1173,32 @@ ${a.content}\n`
   }
   else {
     aiAnalysisData.value.find((item: any) => {
-      return item.pid === props.workId && item.index === props.showData.workIndex
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
     }).loading = true
     aiAnalysisData.value.find((item: any) => {
-      return item.pid === props.workId && item.index === props.showData.workIndex
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
     }).json = { text: '', echartsData: '' }
   }
 
   chat_stream(msg, 'a7741704-ba56-40b7-a6b8-62a423ef9376', props.userId, lang.lang, (event) => {
     if (event.type === 'message') {
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).json.text = event.data
 
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).loading = false
     }
     else if (event.type === 'messageEnd') {
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).json.text = event.data
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).noEnd = false
       aiAnalysisData.value.find((item: any) => {
-        return item.pid === props.workId && item.index === props.showData.workIndex
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
       }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
       saveAnalysis()
     }
@@ -1194,7 +1211,7 @@ ${a.content}\n`
 // 当前分析
 const currentAnalysis = computed(() => {
   let _result = aiAnalysisData.value.find((item: any) => {
-    return item.pid === props.workId && item.index === props.showData.workIndex
+    return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
   })
 
   if (_result?.json?.echartsData) {
@@ -1216,7 +1233,7 @@ const saveAnalysis = () => {
     return
   }
   const params = [{
-    pid: props.workId,
+    pid: props.workId + (props.cid ? ',' + props.cid : ''),
     idx: props.showData.workIndex,
     json: JSON.stringify(currentAnalysis.value.json),
   }]
@@ -2021,11 +2038,13 @@ onUnmounted(() => {
     font-size: 1rem;
     font-weight: 500;
   }
-  &>.ai_echartsData{
+
+  &>.ai_echartsData {
     display: flex;
     flex-wrap: wrap;
     gap: .5rem;
-    &>span{
+
+    &>span {
       padding: .2rem .5rem;
       border-radius: .25rem;
       background: #E6F4FF;
@@ -2035,10 +2054,12 @@ onUnmounted(() => {
       color: #141517;
       font-size: .75rem;
     }
-    &>.title{
+
+    &>.title {
       font-weight: 600;
     }
-    &>.btn{
+
+    &>.btn {
       font-size: .9rem;
       text-decoration: underline;
       color: #F6C82B;

+ 24 - 1
src/views/Student/components/selectUserDialog.vue

@@ -5,7 +5,16 @@
       <div class="close" @click="close()"><svg  viewBox="0 0 1024 1024" width="200" height="200"><path d="M999.819275 905.894092c26.805506 27.003004 26.78811 70.77902-0.051166 97.756441-13.398148 13.484106-30.970362 20.234857-48.534389 20.234857-17.58961 0-35.171034-6.758937-48.577369-20.277836L511.657192 609.698113 120.30704 1003.263723c-13.407358 13.475919-30.970362 20.217461-48.53439 20.217461-17.58961 0-35.17922-6.758937-48.585555-20.269649-26.813692-27.003004-26.78811-70.77902 0.042979-97.764628l391.33378-393.557424L23.572882 117.989251c-26.813692-27.012214-26.796296-70.780043 0.034792-97.764627 26.839275-26.985608 70.332858-26.960025 97.137341 0.042979l390.989949 393.909441L903.07693 20.61962c26.831089-26.977421 70.315462-26.960025 97.129154 0.042979 26.813692 27.01119 26.78811 70.770833-0.042979 97.764627L608.812953 511.98465l391.006322 393.909442z"></path></svg></div>
     </div>
     <div class="content">
-      <div v-for="(item, index) in data.user" :key="index">
+      <div v-for="(item, index) in data.user" :key="index" v-if="!(data.notFiledUser && data.notFiledUser)">
+        {{ item }}
+      </div>
+      <span v-if="data.isSubmitUser">{{ lang.ssSubStu2 }}:</span>
+      <div v-for="(item, index) in data.isSubmitUser" :key="index" v-if="data.isSubmitUser">
+        {{ item }}
+      </div>
+      <div class="line" v-if="data.isSubmitUser && data.isSubmitUser"></div>
+      <span v-if="data.notFiledUser">{{ lang.ssUnsubStu2 }}:</span>
+      <div v-for="(item, index) in data.notFiledUser" :key="index" v-if="data.notFiledUser">
         {{ item }}
       </div>
     </div>
@@ -15,7 +24,9 @@
 <script setup>
 import { ref } from 'vue'
 import Modal from '@/components/Modal.vue'
+import { lang } from '@/main'
 const show = ref(false)
+
 const data = ref({})
 const title = ref("")
 
@@ -95,5 +106,17 @@ defineExpose({
     font-weight: 500;
     font-size: 1rem;
   }
+  &>span{
+    width: 100%;
+    display: block;
+    font-weight: 600;
+    font-size: 1rem;
+  }
+  &>.line{
+    padding: 0;
+    width: 100%;
+    height: 2px;
+    background: #FDF6DE;
+  }
 }
 </style>

+ 36 - 7
src/views/Student/index.vue

@@ -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" :workId="workId"  :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)" :cid="props.cid" :workId="workId"  :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"
@@ -134,7 +134,26 @@
               </div>
               <div class="slide-bottom-right" v-if="!isFullscreen">
                 <Refresh class="tool-btn" v-tooltip="lang.ssRefresh" @click="handleRefreshPage" v-if="currentSlideHasIframe"/>
-                <UpTwo @click="handleHomeworkSubmit" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !isSubmitting" class="tool-btn upBtn" v-tooltip="lang.ssSubmitHW"/>
+                <!-- <UpTwo @click="handleHomeworkSubmit" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !isSubmitting" class="tool-btn upBtn" v-tooltip="lang.ssSubmitHW"/> -->
+                <svg @click="handleHomeworkSubmit" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !isSubmitting" class="tool-btn upBtn" v-tooltip="lang.ssSubmitHW" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
+                  <!-- Document body -->
+                  <path d="M15 10 L15 90 Q15 95 20 95 L80 95 Q85 95 85 90 L85 35 L60 10 Z"
+                        fill="none" stroke="currentColor" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/>
+                  <!-- Folded corner -->
+                  <path d="M60 10 L60 35 L85 35"
+                        fill="none" stroke="currentColor" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/>
+                  <!-- Upload arrow shaft -->
+                  <line x1="50" y1="72" x2="50" y2="45"
+                        stroke="currentColor" stroke-width="6" stroke-linecap="round"/>
+                  <!-- Arrow head -->
+                  <polyline points="37,57 50,43 63,57"
+                        fill="none" stroke="currentColor" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/>
+                  <!-- Bottom lines -->
+                  <line x1="36" y1="80" x2="64" y2="80"
+                        stroke="currentColor" stroke-width="6" stroke-linecap="round"/>
+                  <line x1="36" y1="88" x2="64" y2="88"
+                        stroke="currentColor" stroke-width="6" stroke-linecap="round"/>
+                </svg>
                 <IconLoading v-else-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo" class="tool-btn loading" v-tooltip="lang.ssSubmitting"></IconLoading>
                 <IconStopwatchStart v-if="props.type == '1' && courseDetail.userid == props.userid && isFollowModeActive" class="tool-btn" v-tooltip="lang.ssTimer" @click="timerlVisible = !timerlVisible"  />
                 <IconWrite v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="lang.ssPenTool" @click="writingBoardToolVisible = true"  />
@@ -2661,9 +2680,17 @@ const getCourseDetail = async () => {
             jsonObj.slides.forEach((slide: any, index: number) => {
               let slideContent = ''
               if (slide.elements) {
-                const textElements = slide.elements.filter((element: any) => element.type === 'text')
-                if (textElements.length > 0) {
-                  slideContent = textElements.map((element: any) => element.content).join(' ')
+                // 提取文本内容(包括普通文本和形状文本)
+                const allTextElements = slide.elements.filter((element: any) =>
+                  element.type === 'text' || (element.type === 'shape' && element.text?.content)
+                )
+                if (allTextElements.length > 0) {
+                  slideContent = allTextElements
+                    .map((element: any) => {
+                      const content = element.type === 'text' ? element.content : element.text.content
+                      return content.replace(/<[^>]*>/g, '')
+                    })
+                    .join(' ')
                 }
               }
               pptContent.push(`第${index + 1}页: ${slideContent || '内容为空'}`)
@@ -4966,8 +4993,10 @@ const clearTimerState = () => {
       
   }
   .upBtn{
-      border-bottom: 3px solid #fff;
-      padding-bottom: 3px;
+      // border-bottom: 3px solid #fff;
+      // padding-bottom: 3px;
+      width: 24px;
+      height: 24px;
 
       &:hover,
       &.active {

+ 1 - 1
src/views/components/ThumbnailSlide/index.vue

@@ -3,7 +3,7 @@
     class="thumbnail-slide"
     :style="{
       width: size + 'px',
-      height: size * 0.5625 + 'px',
+      height: size * viewportRatio + 'px',
     }"
   >
       <!-- viewportRatio -->

+ 5 - 5
src/views/lang/cn.json

@@ -786,7 +786,6 @@
   "ssStutterHint": "卡顿提示",
   "ssEvalDetails": "评估详情",
   "ssEvalDimensions": "评估维度",
-  "ssAccuracy": "准确度",
   "ssFluency": "流畅度",
   "ssCompleteness": "完整度",
   "ssRhythm": "节奏韵律",
@@ -811,13 +810,10 @@
   "ssRoundsUnit": "轮",
   "ssMinutesUnit": "分钟",
   "ssEnglishSpeakingTool": "英语口语",
-  "ssComingSoon": "即将推出",
   "ssNoRecommendations": "暂无推荐数据",
   "ssBatchPasteTitle": "批量粘贴",
   "ssBatchPasteVocab": "词汇(每行一个)",
   "ssBatchPasteSentences": "句型(每行一个)",
-  "ssConfirm": "确认",
-  "ssCancel": "取消",
   "ssFunctionDeveloping": "功能开发中",
   "ssSpkFilterAll": "全部",
   "ssSpkFilterSubmitted": "已完成",
@@ -849,5 +845,9 @@
   "ssSpkAIGenerating": "AI 生成中…",
   "ssSpkJustNow": "刚刚生成",
   "ssSpkSecondsAgoTpl": "{n} 秒前生成",
-  "ssSpkMinutesAgoTpl": "{n} 分钟前生成"
+  "ssSpkMinutesAgoTpl": "{n} 分钟前生成",
+  "ssSubmitStatus":"提交情况",
+  "ssSubStu2":"已提交学生",
+  "ssUnsubStu2":"未提交学生",
+  "ssViewSubmitStatus2":"点击查看提交情况"
 }

+ 6 - 6
src/views/lang/en.json

@@ -754,7 +754,6 @@
   "ssViewKeywordCloud": "Click to view keyword cloud",
   "ssKeyword": "Keyword",
   "ssKeywordCloud": "Word Cloud",
-  "ssGeneratingContent": "Generating Content",
   "ssEnglishSubject": "English",
   "ssSpeaking": "Speaking",
   "ssListening": "Listening",
@@ -786,7 +785,6 @@
   "ssStutterHint": "Stutter Hint",
   "ssEvalDetails": "Evaluation Details",
   "ssEvalDimensions": "Evaluation Dimensions",
-  "ssAccuracy": "Accuracy",
   "ssFluency": "Fluency",
   "ssCompleteness": "Completeness",
   "ssRhythm": "Rhythm",
@@ -811,13 +809,10 @@
   "ssRoundsUnit": "rounds",
   "ssMinutesUnit": "min",
   "ssEnglishSpeakingTool": "English Speaking",
-  "ssComingSoon": "Coming Soon",
   "ssNoRecommendations": "No recommendations available",
   "ssBatchPasteTitle": "Batch Paste",
   "ssBatchPasteVocab": "Vocabulary (one per line)",
   "ssBatchPasteSentences": "Sentences (one per line)",
-  "ssConfirm": "Confirm",
-  "ssCancel": "Cancel",
   "ssFunctionDeveloping": "Under Development",
   "ssSpkFilterAll": "All",
   "ssSpkFilterSubmitted": "Done",
@@ -849,5 +844,10 @@
   "ssSpkAIGenerating": "Generating…",
   "ssSpkJustNow": "Just now",
   "ssSpkSecondsAgoTpl": "{n}s ago",
-  "ssSpkMinutesAgoTpl": "{n}m ago"
+  "ssSpkMinutesAgoTpl": "{n}m ago",
+  "ssGeneratingContent": "Generating Content",
+  "ssSubmitStatus":"Submission Status",
+  "ssSubStu2":"Submitted Students",
+  "ssUnsubStu2":"Unsubmitted Students",
+  "ssViewSubmitStatus2":"Click to view submission status"
 }

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

@@ -754,7 +754,6 @@
   "ssViewKeywordCloud":"點擊查看詞雲",
   "ssKeyword":"關鍵詞",
   "ssKeywordCloud":"詞雲",
-  "ssGeneratingContent":"生成中",
   "ssEnglishSubject": "英語學科",
   "ssSpeaking": "口語",
   "ssListening": "聽力",
@@ -845,5 +844,10 @@
   "ssSpkAIGenerating": "AI 生成中…",
   "ssSpkJustNow": "剛剛生成",
   "ssSpkSecondsAgoTpl": "{n} 秒前生成",
-  "ssSpkMinutesAgoTpl": "{n} 分鐘前生成"
+  "ssSpkMinutesAgoTpl": "{n} 分鐘前生成",
+  "ssGeneratingContent":"生成中",
+  "ssSubmitStatus":"提交狀態",
+  "ssSubStu2":"已提交學生",
+  "ssUnsubStu2":"未提交學生",
+  "ssViewSubmitStatus2":"點擊查看提交狀態"
 }