Procházet zdrojové kódy

feat: 添加知识点报告

ChengYuXuan před 5 roky
rodič
revize
415d8db049

+ 12 - 0
src/api/analysis.ts

@@ -0,0 +1,12 @@
+import { ANALYSIS_MODULE } from './prefix'
+import axios from 'axios'
+import { AnalysisInfo, ID, ReceivedData } from './types'
+
+export const getExamAnalysis = (examId: ID) => {
+  return axios.get<ReceivedData<AnalysisInfo>>(`${ANALYSIS_MODULE}/${examId}`, {
+    headers: {
+      Authorization:
+        'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJza3kiOiJXb0JpU2hlWWFvWGllQnVXYW5MZSEhIn0.QDF9l3beb2vodPMH87adsT4DZL60NU6Zf4uYAxelzg8'
+    }
+  })
+}

+ 4 - 0
src/api/prefix.ts

@@ -6,3 +6,7 @@ export const USER_MODULE = BASE_URL + '/user'
 export const JUDGE_MODULE = BASE_URL + '/judge'
 export const REPORT_MODULE = BASE_URL + '/report'
 export const RECORD_MODULE = BASE_URL + '/record'
+
+const ANALYSIS_URL = '/analysis_api'
+
+export const ANALYSIS_MODULE = ANALYSIS_URL + '/analysis'

+ 4 - 6
src/api/question.ts

@@ -36,17 +36,15 @@ export const getQuestionList = (
 ) => {
   let params = ''
   if (stem) {
-    params += `stem=${stem}`
+    params += `stem=${stem}&`
   }
   if (tags) {
-    params += `tags=${tags}`
+    params += `tags=${tags}&`
   }
   if (knowledgeName) {
-    params += `knowledgeName=${knowledgeName}`
-  }
-  if (params) {
-    params += '&'
+    params += `knowledgeName=${knowledgeName}&`
   }
+
   return axios.get<ReceivedData<QuestionListSerializer>>(
     `${QUESTION_MODULE}/question?${params}pageSize=${page.pageSize}&pageNum=${page.pageNum}`
   )

+ 11 - 0
src/api/types.d.ts

@@ -201,3 +201,14 @@ export interface CodeJudgeResult {
   timeused: number
   memoryused: number
 }
+
+// {"1":{AnalysisInfoItem1,AnalysisInfoItem2}, "all": {} , "avg" : {} }
+export interface AnalysisInfo {
+  all: AnalysisInfoItem
+  avg: AnalysisInfoItem
+  [key: string]: AnalysisInfoItem
+}
+// {"code":20,"Internet":25}
+export interface AnalysisInfoItem {
+  [key: string]: number
+}

+ 6 - 0
src/router/teacher.ts

@@ -51,6 +51,12 @@ const routes: Array<RouteConfig> = [
           import(
             /* webpackChunkName: "teacher" */ '@/views/teacher/TeacherExamDetail.vue'
           )
+      },
+      {
+        path: '/exam/:examId/report',
+        name: 'teacher-exam-report',
+        component: () =>
+          import(/*webpackChunkName: "teacher" */ '@/views/exam/ExamResult.vue')
       }
     ]
   }

+ 6 - 1
src/views/components/ExamQuestionsInfo.vue

@@ -32,7 +32,12 @@
           v-if="question.type !== 'short_answer'"
         >
           我的答案:
-          <v-chip label color="info" class="ml-2 mr-4">
+          <v-chip
+            label
+            color="info"
+            class="ml-2 mr-4"
+            v-if="submitAnswers.size > 0"
+          >
             {{ submitAnswers.get(question.id) }}
           </v-chip>
           正确答案:

+ 168 - 0
src/views/exam/ExamResult.vue

@@ -0,0 +1,168 @@
+<template>
+  <v-container>
+    <page-header title="评测报告" :breadcrumb="breadcrumb">
+      <template #extra>
+        <v-row no-gutters justify="end">
+          <router-link
+            to="/teacher/created-exams"
+            style="text-decoration: none"
+          >
+            <v-btn>返回</v-btn>
+          </router-link>
+        </v-row>
+      </template>
+    </page-header>
+    <v-container>
+      <v-row class="mb-5" style="height: 30vh">
+        <v-col cols="6">
+          <v-card style="height: 100%">
+            成绩分布
+          </v-card>
+        </v-col>
+        <v-col cols="6">
+          <v-card style="height: 100%">
+            <e-charts-component
+              :options="skillOption"
+              v-if="skillOption"
+            ></e-charts-component>
+          </v-card>
+        </v-col>
+      </v-row>
+      <v-row class="mb-5">
+        <v-col style="height: 50vh" cols="4">
+          <v-card style="height: 100%">
+            成绩排名
+          </v-card>
+        </v-col>
+      </v-row>
+    </v-container>
+  </v-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import PageHeader from '@/views/components/PageHeader.vue'
+import { TeacherQuestionSerializer, ReceivedData } from '@/api/types'
+import { getExamById, getExamQuestions } from '@/api/exam'
+import { AxiosResponse } from 'axios'
+import { getQuestionInfo } from '@/api/question'
+import EChartsComponent from '@/views/components/echarts/EChartsComponent.vue'
+import { BarChartOption } from '@/views/components/echarts/echarts.type'
+import { getPersonalReport } from '@/api/report'
+import dayjs from 'dayjs'
+import { TIME_FORMAT } from '@/utils/common'
+import { getExamAnalysis } from '@/api/analysis'
+import { mapGetters } from 'vuex'
+
+export default Vue.extend({
+  name: 'ExamResult',
+  components: {
+    PageHeader,
+    EChartsComponent
+  },
+  data() {
+    return {
+      fetching: false,
+      breadcrumb: [
+        {
+          text: '主页',
+          disabled: false,
+          href: '/teacher'
+        },
+        {
+          text: '评测管理',
+          disabled: false,
+          href: '/teacher/exams'
+        },
+        {
+          text: '成绩报告',
+          disabled: true,
+          href: '/'
+        }
+      ],
+      examQuestions: [] as Array<TeacherQuestionSerializer>,
+      skillOption: (null as unknown) as BarChartOption,
+      totalScore: 0,
+      examEnded: false,
+      loadingText: ''
+    }
+  },
+  mounted() {
+    const examId = this.$route.params.examId
+
+    getExamById(examId).then(({ data }) => {
+      const curTime = dayjs()
+      const examEndTime = dayjs(data.result.endTime, TIME_FORMAT)
+      if (curTime.isAfter(examEndTime.add(1, 'minute'))) {
+        this.examEnded = true
+      } else {
+        this.loadingText = '考试进行中,请在考试结束后查看成绩分布信息'
+      }
+    })
+
+    getExamQuestions(examId).then(({ data }) => {
+      this.fetching = true
+      this.totalScore = Object.values(data.result.questionAndScore).reduce(
+        (prev, cur) => {
+          return prev + cur
+        }
+      )
+
+      getExamAnalysis(examId).then(({ data }) => {
+        const analysisInfo = data.result
+        const infoBenchItem = analysisInfo.all
+        const avgPercentage = analysisInfo.avg
+
+        Object.entries(avgPercentage).map((val) => {
+          avgPercentage[val[0]] =
+            (avgPercentage[val[0]] / infoBenchItem[val[0]]) * 100
+        })
+
+        this.skillOption = {
+          title: {
+            text: '知识点掌握情况'
+          },
+          tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+              type: 'shadow'
+            }
+          },
+          legend: {
+            data: ['平均正确率']
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            containLabel: true
+          },
+          xAxis: [
+            {
+              type: 'category',
+              data: Object.keys(infoBenchItem),
+              axisLabel: {
+                interval: 0
+              }
+            }
+          ],
+          yAxis: [
+            {
+              type: 'value'
+            }
+          ],
+          series: [
+            {
+              name: '平均正确率',
+              type: 'bar',
+              data: Object.values(avgPercentage)
+            }
+          ]
+        }
+      })
+    })
+  }
+})
+</script>
+
+<style scoped></style>

+ 31 - 6
src/views/exam/PersonalResult.vue

@@ -77,6 +77,8 @@ import {
 import { getPersonalReport } from '@/api/report'
 import dayjs from 'dayjs'
 import { TIME_FORMAT } from '@/utils/common'
+import { getExamAnalysis } from '@/api/analysis'
+import { mapGetters } from 'vuex'
 
 export default Vue.extend({
   name: 'PersonalResult',
@@ -114,6 +116,9 @@ export default Vue.extend({
       loadingText: ''
     }
   },
+  methods: {
+    ...mapGetters(['getId'])
+  },
   mounted() {
     const examId = this.$route.params.examId
 
@@ -135,9 +140,18 @@ export default Vue.extend({
         }
       )
 
-      getPersonalReport(examId).then(({ data }) => {
-        let rank = data.result.rank
-        let score = data.result.score
+      getExamAnalysis(examId).then(({ data }) => {
+        const analysisInfo = data.result
+        const infoBenchItem = analysisInfo.all
+        const avgPercentage = analysisInfo.avg
+        const myInfo = analysisInfo[this.getId()]
+        const myPercentage = new Map()
+        Object.entries(myInfo).map((val) => {
+          const value = val[1] ?? 0
+          myPercentage.set(val[0], (value / infoBenchItem[val[0]]) * 100)
+          avgPercentage[val[0]] =
+            (avgPercentage[val[0]] / infoBenchItem[val[0]]) * 100
+        })
 
         this.skillOption = {
           title: {
@@ -161,7 +175,10 @@ export default Vue.extend({
           xAxis: [
             {
               type: 'category',
-              data: ['数据结构', '网络', '操作系统', '测试', '设计模式']
+              data: Object.keys(infoBenchItem),
+              axisLabel: {
+                interval: 0
+              }
             }
           ],
           yAxis: [
@@ -173,15 +190,20 @@ export default Vue.extend({
             {
               name: '个人正确率',
               type: 'bar',
-              data: [50, 33, 31, 34, 39]
+              data: Object.values(Object.fromEntries(myPercentage))
             },
             {
               name: '平均正确率',
               type: 'bar',
-              data: [12, 13, 10, 14, 90]
+              data: Object.values(avgPercentage)
             }
           ]
         }
+      })
+
+      getPersonalReport(examId).then(({ data }) => {
+        let rank = data.result.rank
+        let score = data.result.score
 
         this.gradeOption = {
           tooltip: {
@@ -246,6 +268,9 @@ export default Vue.extend({
 
               detail: {
                 formatter: function(value) {
+                  if (value === 0) {
+                    value = 1
+                  }
                   return '排名前 {value|' + value.toFixed(0) + '}{unit|%}'
                 },
                 rich: {

+ 1 - 1
src/views/teacher/ExamManage.vue

@@ -5,7 +5,7 @@
         <v-row no-gutters justify="end">
           <v-col cols="3">
             <router-link :to="{ name: 'create' }" style="text-decoration: none">
-              <v-btn color="info">
+              <v-btn color="primary">
                 创建评测
               </v-btn>
             </router-link>

+ 16 - 1
src/views/teacher/components/TeacherExamInfoCard.vue

@@ -22,7 +22,12 @@
         }}</v-chip>
       </v-col>
       <v-col class="mr-3 d-flex justify-end">
-        <v-btn v-if="state === '已结束'" color="secondary" class="mx-2">
+        <v-btn
+          v-if="state === '已结束'"
+          color="secondary"
+          class="mx-2"
+          @click="onOpenResult"
+        >
           结果报告
         </v-btn>
         <v-btn
@@ -176,6 +181,16 @@ export default Vue.extend({
         case '已结束':
           return 'secondary'
       }
+    },
+    onOpenResult() {
+      const examId = this.examInfo.examId?.toString() ?? ''
+
+      this.$router.push({
+        name: 'teacher-exam-report',
+        params: {
+          examId: examId
+        }
+      })
     }
   },
   mounted() {

+ 7 - 0
vue.config.js

@@ -10,6 +10,13 @@ module.exports = {
         target: 'http://47.111.95.164:8080/',
         ws: true,
         changeOrigin: true
+      },
+      '^/analysis_api': {
+        target: 'http://analysis.seec.seecoder.cn',
+        pathRewrite: {
+          '^/analysis_api': '/eval'
+        }
+        //token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJza3kiOiJXb0JpU2hlWWFvWGllQnVXYW5MZSEhIn0.QDF9l3beb2vodPMH87adsT4DZL60NU6Zf4uYAxelzg8
       }
     },
     port: 8000