Просмотр исходного кода

feat(投票功能): 添加投票类型78的支持和相关功能

- 在selectUserDialog组件中添加scoped样式和滚动支持
- 在index.vue中扩展toolType判断和键盘导航支持
- 在answerTheResult组件中添加投票类型78的显示逻辑
- 在choiceQuestionDetailDialog组件中实现投票类型的详细展示和AI分析功能
lsc 22 часов назад
Родитель
Сommit
28584a9674

+ 8 - 3
src/views/Student/components/answerTheResult.vue

@@ -16,6 +16,11 @@
 				<span v-if="choiceQuestionListData[workIndex]">{{choiceQuestionListData[workIndex].accuracyRate}}%({{choiceQuestionListData[workIndex].yes}}/{{choiceQuestionListData[workIndex].all}})</span>
 				<span v-if="choiceQuestionListData[workIndex]">{{choiceQuestionListData[workIndex].accuracyRate}}%({{choiceQuestionListData[workIndex].yes}}/{{choiceQuestionListData[workIndex].all}})</span>
 			</div>
 			</div>
 
 
+			<div class="atr_d_msg" v-if="workDetail && workDetail.type === '78'">
+				<div>{{ lang.ssAccuracy }}</div>
+				<span v-if="choiceQuestionListData[workIndex]">{{choiceQuestionListData[workIndex].accuracyRate}}%({{choiceQuestionListData[workIndex].yes}}/{{choiceQuestionListData[workIndex].all}})</span>
+			</div>
+
 			<div class="atr_d_msg" v-if="choiceQuestionAnswer">
 			<div class="atr_d_msg" v-if="choiceQuestionAnswer">
 				<div>{{ lang.ssCorrectAns }}</div>
 				<div>{{ lang.ssCorrectAns }}</div>
 				<span style="color: #03ae2b">{{ choiceQuestionAnswer }}</span>
 				<span style="color: #03ae2b">{{ choiceQuestionAnswer }}</span>
@@ -74,7 +79,7 @@
 			class="atr_type45Area"
 			class="atr_type45Area"
 			v-if="
 			v-if="
 				workDetail &&
 				workDetail &&
-				workDetail.type === '45' &&
+				(workDetail.type === '45' || workDetail.type === '78') &&
 				choiceQuestionListData[workIndex] &&
 				choiceQuestionListData[workIndex] &&
 				choiceQuestionListData[workIndex].choiceUser
 				choiceQuestionListData[workIndex].choiceUser
 			"
 			"
@@ -244,7 +249,7 @@ const getWorkDetail = async () => {
 // 选择题题目数组
 // 选择题题目数组
 const choiceQuestionList = () => {
 const choiceQuestionList = () => {
   let _result: any[] = []
   let _result: any[] = []
-  if (workDetail.value && workDetail.value.type === '45') {
+  if (workDetail.value && (workDetail.value.type === '45' || workDetail.value.type === '78')) {
     const _workData = workDetail.value.json.testJson
     const _workData = workDetail.value.json.testJson
     _workData.forEach((item: any, index: number) => {
     _workData.forEach((item: any, index: number) => {
       // 修复 props.workArray 可能为 null 的问题
       // 修复 props.workArray 可能为 null 的问题
@@ -400,7 +405,7 @@ const changeShow = (op: any, idx: number) => {
 
 
 const lookDetail = () => {
 const lookDetail = () => {
   console.log(props.toolType)
   console.log(props.toolType)
-  if ([45, 15, 72, 73].includes(props.toolType)) {
+  if ([45, 15, 72, 73, 78].includes(props.toolType)) {
     emit('openChoiceQuestionDetail', props.slideIndex)
     emit('openChoiceQuestionDetail', props.slideIndex)
   }
   }
 }
 }

+ 181 - 7
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -101,6 +101,99 @@
         </div>
         </div>
       </div>
       </div>
 
 
+      <!-- 投票 -->
+      <div class="c_t45" v-if="workDetail && workDetail.type === '78' && props.showData">
+        <div class="c_t45_title">
+          <div v-if="props.showData.choiceQuestionListData[props.showData.workIndex]">{{
+            props.showData.choiceQuestionListData[props.showData.workIndex]
+              .teststitle
+          }}</div>
+          <div class="c_t45_msg">
+            <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
+                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.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>-->
+        </div>
+        <img class="c_t45_img" :src="props.showData.choiceQuestionListData[props.showData.workIndex]
+          .timuList[0].src
+          " v-if="props.showData.choiceQuestionListData[props.showData.workIndex] &&
+            props.showData.choiceQuestionListData[props.showData.workIndex]
+              .timuList.length > 0
+          " @click="previewImageToolRef.previewImage(props.showData.choiceQuestionListData[props.showData.workIndex]
+            .timuList[0].src)" />
+        <!-- <span class="c_t45_type" v-if="
+          props.showData.choiceQuestionListData[props.showData.workIndex]
+            .type === '1'
+        ">{{ lang.ssSingleSel }}</span>
+        <span class="c_t45_type" v-if="
+          props.showData.choiceQuestionListData[props.showData.workIndex]
+            .type === '2'
+        ">{{ lang.ssMultiOpt }}</span> -->
+        <!-- <span class="c_t45_type">{{ lang.ssChoiceQuestion }}</span> -->
+        <div class="c_t45_echarts" :style="{
+          width: slideWidth - 40 + 'px',
+        }">
+          <div id="echartsArea1" ref="echartsArea1"></div>
+        </div>
+        <div class="aiAnalysis" v-if="props.workArray.length > 0">
+          <div class="ai_header">
+            <div class="ai_title">
+              <svg viewBox="0 0 1024 1024" width="200" height="200">
+                <path
+                  d="M512 170.666667C323.477333 170.666667 170.666667 323.477333 170.666667 512s152.810667 341.333333 341.333333 341.333333 341.333333-152.810667 341.333333-341.333333S700.522667 170.666667 512 170.666667zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z">
+                </path>
+                <path
+                  d="M693.013333 330.986667a42.666667 42.666667 0 0 1 10.304 43.648l-75.413333 226.282666a42.666667 42.666667 0 0 1-26.986667 26.986667l-226.282666 75.413333a42.666667 42.666667 0 0 1-53.973334-53.973333l75.434667-226.261333a42.666667 42.666667 0 0 1 26.986667-26.986667l226.282666-75.413333a42.666667 42.666667 0 0 1 43.648 10.304z m-222.72 139.306666l-41.685333 125.098667 125.077333-41.706667 41.706667-125.077333-125.077333 41.706667z">
+                </path>
+              </svg>{{ lang.ssAnalysis }}
+            </div>
+            <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
+              @click="aiAnalysisRefresh78()">
+              {{ lang.ssAIGenerate }}
+              <svg viewBox="0 0 1024 1024" width="200" height="200">
+                <path
+                  d="M875 483c-33.4 0-60.5 27.1-60.5 60.5v0.1C814.4 710.3 678.8 846 512 846S209.5 710.3 209.5 543.5 345.2 241 512 241c36.8 0 71.7 7.6 104.4 19.7-32 3-57.4 29.1-57.4 61.9 0 34.8 28.2 63 63 63h201.9c34.8 0 63-28.2 63-63V120c0-34.8-28.2-63-63-63s-63 28.2-63 63v81.4C691 150.5 605.2 120 512 120 278.1 120 88.5 309.6 88.5 543.5S278.1 967 512 967s423.5-189.6 423.5-423.5c0-33.4-27.1-60.5-60.5-60.5z">
+                </path>
+              </svg>
+
+            </div>
+          </div>
+          <div class="ai_content" v-if="currentAnalysis && currentAnalysis.json">
+            {{ currentAnalysis.json.text }}
+          </div>
+          <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="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}
+          </div>
+        </div>
+        <div class="cq_changeBtn" v-if="props.showData.choiceQuestionListData.length > 1">
+          <div :class="{ cq_cb_disabled: props.showData.workIndex <= 0 }" @click="changeWorkIndex(0)">
+            <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
+              <path
+                d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
+                fill=""></path>
+            </svg>
+          </div>
+          <span>{{ props.showData.workIndex + 1 }}/{{ props.showData.choiceQuestionListData.length }}</span>
+          <div :class="{ cq_cb_disabled: props.showData.workIndex >= props.showData.choiceQuestionListData.length - 1 }"
+            @click="changeWorkIndex(1)">
+            <svg style="transform: rotate(90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
+              <path
+                d="M512 330.666667c14.933333 0 29.866667 4.266667 40.533333 14.933333l277.33333399 234.666667c27.733333 23.466667 29.866667 64 8.53333301 89.6-23.466667 27.733333-64 29.866667-89.6 8.53333299L512 477.866667l-236.8 200.53333299c-27.733333 23.466667-68.266667 19.19999999-89.6-8.53333299-23.466667-27.733333-19.19999999-68.266667 8.53333301-89.6l277.33333399-234.666667c10.666667-10.666667 25.6-14.933333 40.533333-14.933333z"
+                fill=""></path>
+            </svg>
+          </div>
+        </div>
+      </div>
+
       <!-- 问答题 -->
       <!-- 问答题 -->
       <div class="c_t15" v-if="workDetail && workDetail.type === '15' && props.showData">
       <div class="c_t15" v-if="workDetail && workDetail.type === '15' && props.showData">
         <div class="c_t15_title">{{ workDetail.json.answerQ }}</div>
         <div class="c_t15_title">{{ workDetail.json.answerQ }}</div>
@@ -395,7 +488,7 @@ const processWorkContent = async (content: string, toolType: number): Promise<an
   }
   }
 
 
   try {
   try {
-    if ([45, 15].includes(toolType)) {
+    if ([45, 15, 78].includes(toolType)) {
       return JSON.parse(decodeURIComponent(contentToParse))
       return JSON.parse(decodeURIComponent(contentToParse))
     }
     }
     else if (toolType === 72) {
     else if (toolType === 72) {
@@ -537,7 +630,7 @@ const handleChartResize = () => {
           if (
           if (
             props.showData &&
             props.showData &&
             props.showData.workDetail &&
             props.showData.workDetail &&
-            props.showData.workDetail.type === '45'
+            (props.showData.workDetail.type === '45' || props.showData.workDetail.type === '78')
           ) {
           ) {
             setEchartsArea1()
             setEchartsArea1()
           }
           }
@@ -766,7 +859,7 @@ watch(
     if (
     if (
       newVal &&
       newVal &&
       newVal.choiceQuestionListData[newVal.workIndex] &&
       newVal.choiceQuestionListData[newVal.workIndex] &&
-      props.showData.workDetail.type === '45'
+      (props.showData.workDetail.type === '45' || props.showData.workDetail.type === '78')
     ) {
     ) {
       if (
       if (
         oldVal &&
         oldVal &&
@@ -804,7 +897,7 @@ watch(
     if (
     if (
       (newVal || newVal === 0) &&
       (newVal || newVal === 0) &&
       props.showData.workDetail &&
       props.showData.workDetail &&
-      props.showData.workDetail.type === '45'
+      (props.showData.workDetail.type === '45' || props.showData.workDetail.type === '78')
     ) {
     ) {
       if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
       if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
         setEchartsArea1()
         setEchartsArea1()
@@ -827,7 +920,7 @@ watch(
     if (
     if (
       (newVal || newVal === 0) &&
       (newVal || newVal === 0) &&
       props.showData.workDetail &&
       props.showData.workDetail &&
-      props.showData.workDetail.type === '45'
+      (props.showData.workDetail.type === '45' || props.showData.workDetail.type === '78')
     ) {
     ) {
       setEchartsArea1()
       setEchartsArea1()
     }
     }
@@ -845,7 +938,7 @@ watch(
       newVal &&
       newVal &&
       props.showData &&
       props.showData &&
       props.showData.workDetail &&
       props.showData.workDetail &&
-      props.showData.workDetail.type === '45'
+      (props.showData.workDetail.type === '45' || props.showData.workDetail.type === '78')
     ) {
     ) {
       setEchartsArea1()
       setEchartsArea1()
     }
     }
@@ -863,7 +956,7 @@ watch(
       newVal &&
       newVal &&
       props.showData &&
       props.showData &&
       props.showData.workDetail &&
       props.showData.workDetail &&
-      props.showData.workDetail.type === '45'
+      (props.showData.workDetail.type === '45' || props.showData.workDetail.type === '78')
     ) {
     ) {
       handleChartResize()
       handleChartResize()
     }
     }
@@ -959,6 +1052,87 @@ const aiAnalysisRefresh45 = () => {
   })
   })
 }
 }
 
 
+// ai生成投票分析 投票
+const aiAnalysisRefresh78 = () => {
+  const _work = props.showData.choiceQuestionListData[props.showData.workIndex]
+  const msg = `# CONTEXT #
+你是K-12阶段的AI教育课堂分析助手,基于上传的课件、逐字稿,以及当页的学生答题数据(选择题/问答题/智能体对话/投票)进行智能分析。
+
+# OBJECTIVE #
+输出当前学生答题的分析报告:整体表现、核心发现(共性问题/误区)、教学优化(改进方向),为教师提供教学策略的建议和支持。
+
+#INPUT#
+课程数据:
+- 课程名称:${props.courseDetail.title}
+- 课程学科:${props.courseDetail.name}
+当前页面答题数据(投票):【分析重点】
+- 投票题目:${_work.teststitle}
+- 选项数据:${JSON.stringify(_work.choiceUser)}
+- 题目图片:${_work.timuList.lenght ? _work.timuList[0].src : ''}
+- 未提交学生:${JSON.stringify(props.showData.unsubmittedStudents.map((item: any) => item.name))}
+
+# ANALYSIS RULES #
+1. **问题定位**:明确指出哪个知识点/概念/步骤掌握不佳
+2. **建议具体**:给出可执行的教学动作(如“增加XX实例演练”“补充XX对比图”)
+
+# RESPONSE #
+采用段落叙述,数据量化呈现,突出可操作建议,严格控制输出为80词内。采用三段式论述:
+1. 整体表现-1句话
+2. 核心发现-1-2句,指出共性问题+典型案例
+3. 改进建议-1句话,提出具体教学动作
+
+# EXAMPLES #
+样例:
+选择题正确率62%,核心概念“机器学习三步骤”(输入数据→训练模型→预测结果)掌握尚可,但“训练与预测区分”混淆率达38%;建议强化训练vs预测对比教学。`
+
+  if (!currentAnalysis.value) {
+    aiAnalysisData.value.push({
+      pid: props.workId + (props.cid ? ',' + props.cid : ''),
+      index: props.showData.workIndex,
+      loading: true,
+      generatingContent: true,
+      json: { text: '', echartsData: '' },
+      noEnd: true,
+      update_at: '',
+      create_at: '',
+    })
+  }
+  else {
+    aiAnalysisData.value.find((item: any) => {
+      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 + (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 + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+      }).json.text = event.data
+
+      aiAnalysisData.value.find((item: any) => {
+        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 + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+      }).json.text = event.data
+      aiAnalysisData.value.find((item: any) => {
+        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 + (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()
+    }
+  }).catch(err => {
+    console.log('err', err)
+  })
+}
+
 // 问答题
 // 问答题
 const aiAnalysisRefresh15 = () => {
 const aiAnalysisRefresh15 = () => {
   let flag = 0
   let flag = 0

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

@@ -55,7 +55,7 @@ defineExpose({
 })
 })
 </script>
 </script>
 
 
-<style lang="scss">
+<style lang="scss" scoped>
 .modalArea{
 .modalArea{
   &>.modal-content {
   &>.modal-content {
     border-radius: 1rem;
     border-radius: 1rem;
@@ -101,6 +101,9 @@ defineExpose({
   display: flex;
   display: flex;
   gap: .8rem;
   gap: .8rem;
   flex-wrap: wrap;
   flex-wrap: wrap;
+
+  max-height: calc(100vh - 8.6rem);
+  overflow: auto;
   &>div{
   &>div{
     padding: .25rem .5rem;
     padding: .25rem .5rem;
     font-weight: 500;
     font-weight: 500;

+ 3 - 1
src/views/Student/index.vue

@@ -1222,7 +1222,7 @@ const getWorkId = () => {
     typeof element === 'object' &&
     typeof element === 'object' &&
     ('toolType' in element) &&
     ('toolType' in element) &&
     (element as any).toolType !== undefined &&
     (element as any).toolType !== undefined &&
-    ((element as any).toolType === 45 || (element as any).toolType === 15 || (element as any).toolType === 73 || (element as any).toolType === 72)
+    ((element as any).toolType === 45 || (element as any).toolType === 15 || (element as any).toolType === 73 || (element as any).toolType === 72 || (element as any).toolType === 78)
   ) {
   ) {
     // 提取链接中的id参数
     // 提取链接中的id参数
     const url = (element as any).url
     const url = (element as any).url
@@ -2546,6 +2546,7 @@ const handleKeydown = (e: KeyboardEvent) => {
         previousSlide()
         previousSlide()
       }
       }
       break
       break
+    case 'PageUp':
     case 'ArrowUp':
     case 'ArrowUp':
       e.preventDefault()
       e.preventDefault()
       if (!isFollowModeActive.value || props.type == '1') {
       if (!isFollowModeActive.value || props.type == '1') {
@@ -2558,6 +2559,7 @@ const handleKeydown = (e: KeyboardEvent) => {
         nextSlide()
         nextSlide()
       }
       }
       break
       break
+    case 'PageDown':
     case 'ArrowDown':
     case 'ArrowDown':
     case ' ':
     case ' ':
       e.preventDefault()
       e.preventDefault()