Преглед изворни кода

学生端错题详情增加解答详细 我的作答组件

dengshaobo пре 1 месец
родитељ
комит
1e9d25a1fe

+ 118 - 0
src/views/targetImprove/components/AnswerAnalysis.vue

@@ -0,0 +1,118 @@
+<template>
+    <div class="answer_analysis">
+        <div class="knowledge_area">
+            <div class="knowledge_title">【知识点】</div>
+            <div class="knowledge_tags">
+                <span v-for="item in knowledgeList"  
+                    class="tag_item">{{item.knowName || item}}
+                </span>
+            </div>
+        </div>
+        <!-- 正确答案区域 -->
+        <div class="answer_area block_area">
+            <div class="title">【答案】</div>
+            <div  class="desc" v-html="answer"></div>
+        </div>
+        <!-- 详解区域 -->
+        <div class="analysis_area block_area">
+            <div class="title">【详解】</div>
+            <div class="detail_content desc" v-html="analysis"></div>
+        </div>
+    </div>
+</template>
+
+
+<script>
+
+export default ({
+    name:'AnswerAnalysis',
+    components: {},
+    props:{
+        knowledgeList:{  //知识点
+            type:Array,
+
+        },
+        answer:{  //答案
+            type:String,
+        },
+        analysis:{  //解析
+            type:String,
+        }
+    },
+    data(){
+        return {
+        }
+    },
+    methods: {},
+    computed:{},
+    mounted() {
+    },
+
+
+    
+})
+
+</script>
+
+
+<style lang="scss" scoped>
+
+  // 答案解析区域
+.answer_analysis{
+    border-top: 1px solid #DCDFE6;
+
+    padding: 16px;
+    display: flex;
+    flex-direction: column;
+    gap:16px;
+
+    // 知识点区域
+    .knowledge_area {
+        display: flex;
+        align-items: center;
+        gap:10px;
+        .knowledge_title {
+            width: 70px;
+            font-weight: 600;
+        }
+        .knowledge_tags {
+            flex: 1 1;
+            display: flex;
+            flex-wrap: wrap;
+            gap: 10px;
+            .tag_item {
+                box-sizing: border-box;
+                height: 28px;
+                line-height: 26px;
+                border-radius: 4px;
+                border: 1px solid #2E64FA;
+                padding: 0px 10px;
+                background-color: #f8f8f9;
+                color: #2E64FA;
+            }
+        }
+    }
+
+    // 答案解析 块
+    .block_area {
+        display: flex;
+        justify-content: flex-start;
+        align-items: flex-start;
+        gap:10px;
+        .title {
+            text-align: right;
+            width: 70px;
+            flex-shrink: 0;
+            font-weight: 600;
+            padding-top: 5px;
+        }
+        .desc{
+            flex: 1 1;
+            line-height: 24px;
+            padding: 0px;
+        }
+    }
+}
+
+
+</style>

+ 5 - 4
src/views/targetImprove/components/CommonTree.vue

@@ -190,9 +190,10 @@ export default {
     };
   },
   computed: {
-    targetCommon() {
-      return this.$store.state.target.targetCommon;
-    },
+    subjectName()
+    {
+      return this.$store.state.target.subjectName;
+    }
   },
   // ==================== 生命周期 ====================
   mounted() {
@@ -379,7 +380,7 @@ export default {
     HandleAiExplain(node, data) {
       let content =
         "科目:" +
-        this.targetCommon.subjectName +
+        this.subjectName +
         ",知识点:" +
         data.knowledgeName;
       this.openAiRobot(content);

+ 177 - 0
src/views/targetImprove/components/StudentAnswerDetail.vue

@@ -0,0 +1,177 @@
+<template>
+    <div class="stu_answer_detail">
+        <div class="title">【作 答】</div>
+        <div class="desc">
+            <el-radio-group 
+                v-if="answerType == 'online'"
+            >
+              <el-radio 
+                v-for="radioItem in optionList" 
+                    :key="radioItem" 
+                    :label="radioItem"
+                    :class="{ 
+                         'is-correct-radio is-checked':  answerStr.split('').includes(radioItem), 
+                        'is-wrong-radio is-checked': studentAnswer.split('').includes(radioItem) && !(answerStr.split('').includes(radioItem))
+                    }"
+                >{{ radioItem }}</el-radio>
+            </el-radio-group>
+
+            <ImageJumpPreview 
+                v-if="answerType == 'img' && answerImg?.length > 0"
+                :fileMessage="answerImg[currentNavPage - 1]" height="400px"
+                :filePadding="{padding:`0 ${imgPreviewPadding}`}"
+                :navBarWidth="{width:`calc(100% - ${imgPreviewPadding} + 20px)`}"
+                :navBarLeft="{left: `calc(${imgPreviewPadding} / 2 - 10px)`}"
+                :navTotalPage="answerImg.length"
+                :currentNavPage.sync="currentNavPage"
+            ></ImageJumpPreview>
+        </div>
+    </div>
+</template>   
+
+
+<script>
+import ImageJumpPreview from './ImageJumpPreview.vue'
+import {numToCapitalLetter} from '../utils/numberToExcelCapitalLetter.js'
+
+export default ({
+    name:'StudentAnswerDetail',
+    components: {
+        ImageJumpPreview,
+    },
+    props:{
+        answerType:{     //答题方式  //【线上答题(勾选选项)】online 和 【上传图片的方式】 img   
+            type:String
+        },
+
+        /** 【勾选选项】 online 用到的参数 */
+        answerOptionNum:{   // 选项数量(只有 props.answerType 为 online 的时候才会用到)
+            type:Number   
+        },
+        studentAnswer:{   //学生的答案
+            type:String,
+        },
+        rightAnswer:{  //正确答案
+            type:String,
+        },
+        /** 【拍照/传图】 img   用到的参数 */
+        answerImg:{
+            type:Array,  // {name:'',url:''}[]
+        }
+    },
+    data(){
+        return {
+            imgPreviewPadding:'70px',
+            currentNavPage:1,
+  
+            
+        }
+    },
+    computed:{
+        // 答案的选项列表
+        optionList(){
+            // 选项数量转换成大写字母列表
+            let options = []
+            for(let i=0;i<this.answerOptionNum;i++){
+                options[i]  = numToCapitalLetter(i)
+            }
+            return options
+        },
+
+        // 过滤掉HTML标签,只保留 A-G 大写字母后的正确答案
+        answerStr(){
+            let ans = this.rightAnswer || '';
+            console.log("打印正确答案",this.rightAnswer);
+    
+            // 1. 移除全部HTML标签
+            ans = ans.replace(/<[^>]*>/g, '');
+            // 2. 只保留 A‑G 大写英文字母,【这里必须是英文半角减号 A-G】
+            ans = ans.replace(/[^A-G]/gi, '');
+            ans = ans.toUpperCase().trim();
+            console.log("打印过滤后的正确答案",ans);
+            return ans;
+        }
+    },
+    methods: {},
+    mounted() {
+
+    },
+    
+})
+
+</script>
+
+
+<style lang="scss" scoped>
+ .stu_answer_detail{
+    padding: 16px;
+    display: flex;
+    border-top: 1px solid #DCDFE6;
+    align-items: center;
+    gap:10px;
+    .title {
+        text-align: right;
+        width: 70px;
+        flex-shrink: 0;
+        font-weight: 600;
+    }
+    .desc{
+        flex: 1 1;
+        // line-height: 24px;
+    }
+
+    
+    // 2. 错误选项样式(红色)
+    ::v-deep .is-wrong-radio {
+            position: relative !important;
+        // 圆圈边框
+        .el-radio__inner {
+            border-color: #F56C6C !important;
+        }
+        // 选中状态(圆点填充)
+        &.is-checked .el-radio__inner {
+            position: relative !important;
+            background-color: none;
+            &::after{
+                    width: 8px;
+                    height: 8px;
+                    border-radius: 100%;
+                    background-color: #F56C6C !important;
+                    content: "";
+                    position: absolute;
+                    left: 50%;
+                    top: 50%;
+                    transform: translate(-50%, -50%) scale(1) !important;
+                    transition: transform 0.15s ease-in;
+            }
+        }
+    }
+
+    // 正确选项样式 蓝色
+    ::v-deep .is-correct-radio {
+            position: relative !important;
+        // 圆圈边框
+        .el-radio__inner {
+            border-color: #2E64FA !important;
+        }
+        // 选中状态(圆点填充)
+        &.is-checked .el-radio__inner {
+            position: relative !important;
+            background-color: none;
+
+            &::after{
+                    width: 8px;
+                    height: 8px;
+                    border-radius: 100%;
+                    background-color: #2E64FA!important;
+                    content: "";
+                    position: absolute;
+                    left: 50%;
+                    top: 50%;
+                    transform: translate(-50%, -50%) scale(1) !important;
+                    transition: transform 0.15s ease-in;
+            }
+        }
+    }
+}
+</style>

+ 259 - 20
src/views/targetImprove/consolidate/index.vue

@@ -65,16 +65,79 @@
         </div>
         <div class="page_jg_20">
         </div>
+        <div class="question_list hide_scrollbar" v-loading="loading" element-loading-text="加载中……" element-loading-spinner="el-icon-loading" element-loading-background="#ffffff">
+          <div class="question_list_item" v-for="(item, index) in questionList" :key="index">
+            
+            <div class="item_top">
+              <QuestionItem :question="item" :index="GetQuestionIndex(index)">
+                <template slot="header_right">
+                    <div class="item_right_info" >
+                      <span class="info_score" v-if="item.answerScore">满分:{{item.answerScore || 0}}</span>
+                      <span class="info_rate" v-if="item.answerScore">得分:{{item.score || 0 }}</span>
+                      <span class="info_rate">得分率:{{item.scoreRate }}%</span>
+                    </div>
+                </template>
+              </QuestionItem>
+            </div>
+            <div class="item_bottom">
 
-        <div class="question_item" v-for="(item, index) in questionList" :key="index">
-          <QuestionItem :question="item" :index="index+1"></QuestionItem>
-        </div>
-        <div class="question_no_data" v-if="questionList.length==0">
-          <div class="no_data_content">
-            <div class="no_data_text">暂无数据</div>
+              <div class="item_bottom_left">
+                <!-- 更新:{{item.createTime}} -->
+              </div>
+
+              <!-- ---------------- 试题按钮操作区 ---------------- -->
+              <div class="item_bottom_right">
+                <span class="action_btn ai_lecture_btn"  @click="OperationTrigger(item,'ai')">
+                    <img :src="aiIcon" alt="">
+                    AI讲题
+                </span>
+                <span class="action_btn" :class="{'analysis_btn':item.operationType=='analysis'}" @click="OperationTrigger(item,'analysis')">
+                    <img :src="item.operationType=='analysis' ? answerIconLight :answerIcon" alt="">
+                    解析答案
+                </span>
+                <span class="action_btn" :class="{'answer_detail_btn':item.operationType=='stuAnswerDetail'}" @click="OperationTrigger(item,'stuAnswerDetail')">
+                    <img :src="item.operationType=='stuAnswerDetail' ? deteaiIconLight :deteaiIcon" alt="">
+                    我的作答
+                </span>
+              </div>
+            </div>
+
+            <!-- 答案解析区 -->
+            <div class="item_answer"  v-if="item?.operationType == 'analysis'">
+              <!-- 答案解析区域 -->
+              <AnswerAnalysis 
+                :knowledgeList="item.knowledgeList"
+                :answer="item.answer"
+                :analysis="item.parse"/>
+            </div>
+            <!-- 学生作答区域 -->
+            <div class="item_answer"  v-if="item?.operationType == 'stuAnswerDetail'">
+              <!-- 学生作答区域 -->
+              <StudentAnswerDetail v-if="item?.operationType == 'stuAnswerDetail'" 
+                        :studentAnswer="item.studentAnswer" 
+                        :answerType="item.isObjective ? (item.isJudge ? 'img' : 'online') : 'img'"
+                        :answerOptionNum="item.optionsNum"
+                        :rightAnswer="item.answer"
+                        :answerImg="(item.studentAnswer?.split(',')?.map(img=>({name:img,url:img})) || [])"
+                    />
+            </div>
+          </div>
+          <div class="question_no_data" v-if="questionList.length==0">
+            <div class="no_data_content">
+              <div class="no_data_text">暂无数据</div>
+            </div>
           </div>
         </div>
-
+        <div class="page_pagination">
+          <el-pagination
+            @current-change="ChangePage"
+            background
+            layout="prev, pager, next"
+            :current-page="pageInfo.pageNum"
+            :page-size="pageInfo.pageSize"
+            :total="pageInfo.total">
+          </el-pagination>
+        </div>
         <div class="page_jg_20">
 
         </div>
@@ -86,16 +149,34 @@
 import TargetSubject from "@/views/targetImprove/components/TargetSubject.vue"//顶部筛选科目组件
 import QuestionItem from "@/views/targetImprove/components/QuestionItem.vue"//题目公共组件
 import CommonTree from '@//views/targetImprove/components/CommonTree.vue';//树形组件
+import AnswerAnalysis from '@/views/targetImprove/components/AnswerAnalysis';//答案解析组件
+import StudentAnswerDetail from '@/views/targetImprove/components/StudentAnswerDetail.vue'//学生作答组件
+
+import aiIcon from '@/assets/knowledgeGraph/aiIcon.png'
+import answerIcon from '@/assets/practiceResult/broken_line.svg'
+import deteaiIcon from '@/assets/practiceResult/todo_list.png'
+
+import answerIconLight from '@/assets/practiceResult/broken_line_light.png'
+import deteaiIconLight from '@/assets/practiceResult/todo_list_light.svg'
 import { mapState } from 'vuex'
 export default {
   components: {
     TargetSubject,
     QuestionItem,
-    CommonTree
+    CommonTree,
+    AnswerAnalysis,
+    StudentAnswerDetail
 
   },
+  inject: ['openAiRobot'],//注入Ai机器人组件调用方法
   data() {
     return {
+      // 图标数据
+      aiIcon:aiIcon,
+      answerIcon:answerIcon,
+      deteaiIcon:deteaiIcon,
+      answerIconLight:answerIconLight,
+      deteaiIconLight:deteaiIconLight,
       questionList: [],//错题列表
       courseList:[],//科目列表
       grades:[],//选择的年纪
@@ -118,7 +199,13 @@ export default {
       startTime:'',//开始时间
       endTime:'',//结束时间
       mockTreeData:[],//知识点树形数据
-
+      pageInfo: {
+        pageNum: 1,
+        pageSize: 10,
+        total: 0
+      },
+      loading:false,//加载中
+      showExportDialog:false,//导出错题本pdf弹窗
     }
   },
   computed:{
@@ -126,6 +213,10 @@ export default {
     gradeList()
     {
       return this.$store.state.target.gradesList;
+    },
+    subjectName()
+    {
+      return this.$store.state.target.subjectName;
     }
   },
   watch:{
@@ -150,13 +241,62 @@ export default {
   },
   methods: { 
 
+    /** type:  ai(ai讲题) analysis(解析答案) stuAnswer(学生答案) */
+    OperationTrigger(row,type)
+    {
+      console.log("打印当前行数据",row);
+      if(row.operationType == type){
+          row.operationType = ''
+      }else{
+          row.operationType = type
+      }
+      if(type == 'ai'){
+          console.log("打开AI解读");
+          console.log("打印当前行数据",row);
+          let knowledgeName = row.knowledgeList.map(kno=>{return '【'+kno.knowName+'】'}).join('');
+          let content = "科目:"+this.subjectName+",【试题】:"+row.title+",【知识点】:"+knowledgeName+"【参考答案】:"+row.answer+",【试题解析】:"+row.parse;
+          let roleString=`你现在扮演洋葱学园的理科讲题老师,做纯文本试题讲解,不生成动画脚本,面向初高中学生。
+          核心风格:启发式讲题,先引导思考,不直接抛出答案;语言口语化,像课堂老师说话,逻辑层层递进,把“为什么”讲透,不只复述解析文字。
+
+          严格执行输出结构:
+          1. 引入:我们一起来看这道题,点明本题考察的知识点。
+          2. 分析题干:梳理题目给出的已知条件,标出题目里容易忽略、容易踩坑的地方,引导学生先暂停思考。
+          3. 知识回顾:结合提供的知识点,用通俗语言回顾核心原理,拒绝晦涩定义,讲明白解决这道题要用到什么知识。
+          4. 逐步推理:依据我提供的试题解析和答案,一步一步推导,每一步解释这么做的理由;重点点明学生高频出错的地方。
+          5. 给出结果:给出本题参考答案。
+          6. 方法总结:归纳这类题型通用解题思路,点明易错陷阱,告诉学生以后碰到同类题目该从哪里下手。
+          7. 变式小练习:出一道同考点、同解题逻辑的简短变式题,只出题目,不给出答案,供学生巩固练习。
+          【数学公式输出规则】所有数学公式,行内公式使用$公式$,独立块公式使用$$公式$$,禁止使用[]包裹公式。
+
+
+          接下来我会输入:
+          【试题】
+          【知识点】
+          【试题解析】
+          【参考答案】
+          请按照以上规则输出完整文本讲解
+          `;//角色定义提示词
+          this.openAiRobot(content,roleString);
+      }
+    },
+
+    //题目编号索引
+    GetQuestionIndex(index)
+    {
+      return (this.pageInfo.pageNum-1)*10+index+1;
+    },
+
+    ChangePage()
+    {
+
+    },
 
      /**
      * 【事件回传】复选框勾选变化(多选)
      * @param {Array} checkedKeys - 所有勾选节点的 key 数组(勾选父节点时包含其全部子节点 key)
      */
-    HandleTestNodeCheck(checkedKeys) {
-      console.log('%c[树形组件] node-check 勾选节点 key 数组: ', 'background:#3BA272;color:#fff;padding:4px 8px;border-radius:4px;', checkedKeys);
+    HandleTestNodeCheck(checkedKeys)
+    {
       this.knowledgeIds = checkedKeys;
       this.GetQuestionList();//获取错题列表
     },
@@ -229,7 +369,15 @@ export default {
         console.log('打印错题列表', res)
         if(res.code == 200)
         {
-          this.questionList = res.data.records || [];
+          this.questionList = (res.data.records || []).map(item => {
+            return {
+              ...item,       // 保留原有全部字段
+              operationType:'',  //新增字段 题目的操作类型(包含 'ai'(ai讲题) 'analysis'(解析答案) 'stuAnswer'(学生答案))
+
+              showAnswer: false  // 新增字段 是否显示答案解析
+            }
+          })
+          this.pageInfo.total=Number(res.data.total);
         }
         else
         {
@@ -282,7 +430,7 @@ export default {
           }
           else
           {
-            
+
             this.GetQuestionList();//获取错题列表
           }
         
@@ -374,17 +522,108 @@ export default {
     overflow-y: auto;
     box-sizing: border-box;
     border-radius: 10px 10px 10px 10px;
+    .question_list
+    {
+
+      height: calc(100% - 107px);
+      overflow-y: auto;
+
+
+    }
   }
 }
 
-.question_item
+.question_list_item
 {
-
-  background: #FFFFFF;
-  border-radius: 10px 10px 10px 10px;
-  border: 1px solid #DCDFE6;
-  padding: 16px;
-  box-sizing: border-box;
+  width:100%;
+  border:1px solid #DCDFE6;
+  border-radius: 10px;
+  background-color: #ffffff;
   margin-bottom: 20px;
+  box-sizing: border-box;
+  .item_top
+  {
+    width:auto;
+    padding: 16px;
+    box-sizing: border-box;
+  }
+  .item_bottom
+  {
+    width:100%;
+    height: 48px;
+    line-height: 48px;
+    padding: 10px 16px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: #F8F8F9;
+    border-radius: 0px 0px 10px 10px;
+    border-top: 1px solid #DCDFE6;
+    box-sizing: border-box;
+    .item_bottom_left
+    {
+      font-weight: 400;
+      font-size: 14px;
+      color: #666;
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .item_bottom_right
+    {
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+      gap: 20px;
+
+
+      .action_btn {
+          cursor: pointer;
+          display: flex;
+          align-items: center;
+          gap:8px;
+          line-height: 16px;
+          img{
+              width: 16px;
+              height: 16px;
+          }
+          &.ai_lecture_btn {
+              color: #4F3EFF;
+          }
+          &.answer_detail_btn,
+          &.analysis_btn {
+              color:#2E64FA;
+          }
+      }
+
+      // 收藏 取消
+      :deep() {
+        .el-button--text {
+          color:#333;
+        }
+      }
+
+
+      img
+      {
+        width: 16px;
+        height: 16px;
+      }
+
+      
+    }
+  }
+
+  //答案解析区域
+  .item_answer
+  {
+    width:100%;
+    background: #F8F8F9;
+    height:auto;
+    border-radius:0 0 10px 10px;
+    // border-top: 1px solid #DCDFE6;
+  }
 }
 </style>