Bläddra i källkod

考试结果引入echarts

ChengYuXuan 5 år sedan
förälder
incheckning
e157e31ab1

+ 2 - 0
package.json

@@ -14,6 +14,7 @@
     "core-js": "^3.6.5",
     "dayjs": "^1.10.4",
     "echarts": "^5.0.1",
+    "lodash.throttle": "^4.1.1",
     "marked": "^1.2.7",
     "monaco-editor": "^0.21.2",
     "monaco-editor-webpack-plugin": "^2.1.0",
@@ -26,6 +27,7 @@
   },
   "devDependencies": {
     "@koa/cors": "^3.1.0",
+    "@types/lodash.throttle": "^4.1.6",
     "@types/marked": "^1.2.1",
     "@types/ramda": "^0.27.34",
     "@typescript-eslint/eslint-plugin": "^2.33.0",

+ 3 - 3
src/plugins/vuetify.config.ts

@@ -8,13 +8,13 @@ export const vuetifyConfig: Partial<UserVuetifyPreset> = {
     themes: {
       // default theme
       light: {
-        primary: '#9575CD',
+        primary: '#7E57C2',
         secondary: '#424242',
         accent: '#82B1FF',
         error: '#EF5350',
         info: '#42A5F5',
-        success: '#66BB6A',
-        warning: '#FFCA28'
+        success: '#66BB6A'
+        // warning: '#FFCA28'
       }
     }
   },

+ 3 - 1
src/router/student.ts

@@ -37,7 +37,9 @@ const routes: Array<RouteConfig> = [
         path: 'result/:examId',
         name: 'result',
         component: () =>
-          import(/*webpackChunkName: "exam"*/ '@/views/exam/PersonalResult.vue')
+          import(
+            /*webpackChunkName: "student"*/ '@/views/exam/PersonalResult.vue'
+          )
       }
     ]
   }

+ 5 - 3
src/views/components/ExamQuestionsInfo.vue

@@ -18,19 +18,21 @@
           <v-card-text> 知识点: {{ question.keyPoints }} </v-card-text>
         </v-col>
       </v-row>
-      <v-row class="pl-10" v-if="analysis">
-        <v-col class="grey lighten-3" cols="11">
+      <v-row class="px-10 d-block" v-if="analysis">
+        <v-col class="grey lighten-3 py-1">
           <markdown-text
             :raw="question.analysis"
             style="height: 100%;width: 100%"
           ></markdown-text>
         </v-col>
-        <v-col class="d-flex justify-center align-center" cols="1">
+        <v-col class="d-flex align-center justify-end">
+          答案:
           <v-chip label color="success" class="ml-2">
             {{ question.answer }}
           </v-chip>
         </v-col>
       </v-row>
+      <v-row class="pl-10"> </v-row>
     </div>
   </div>
 </template>

+ 60 - 0
src/views/components/echarts/EChartsComponent.vue

@@ -0,0 +1,60 @@
+<template>
+  <div style="height: 100%;width: 100%"></div>
+</template>
+
+<script lang="ts">
+import Vue, { PropType } from 'vue'
+import { EChartsType } from 'echarts/core'
+import throttle from 'lodash.throttle'
+import {
+  BarChartOption,
+  PieChartOption
+} from '@/views/components/echarts/echarts.type'
+import * as echarts from 'echarts/core'
+import { BarChart, PieChart } from 'echarts/charts'
+import {
+  GridComponent,
+  LegendComponent,
+  TitleComponent,
+  TooltipComponent
+} from 'echarts/components'
+import { CanvasRenderer } from 'echarts/renderers'
+
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  LegendComponent,
+  BarChart,
+  PieChart,
+  CanvasRenderer
+])
+
+export default Vue.extend({
+  name: 'EChartsComponent',
+  props: {
+    options: Object as PropType<BarChartOption | PieChartOption>
+  },
+  data() {
+    return {
+      chart: (null as unknown) as EChartsType,
+      resizeHandler: (null as unknown) as Function
+    }
+  },
+  mounted() {
+    this.chart = echarts.init(this.$el as HTMLElement)
+    this.chart.setOption(this.options)
+
+    this.resizeHandler = throttle(() => {
+      this.chart.resize()
+    }, 300)
+    window.addEventListener('resize', this.resizeHandler as any)
+  },
+  destroyed() {
+    window.removeEventListener('resize', this.resizeHandler as any)
+    this.chart.dispose()
+  }
+})
+</script>
+
+<style scoped></style>

+ 23 - 0
src/views/components/echarts/echarts.type.d.ts

@@ -0,0 +1,23 @@
+import * as echarts from 'echarts/core'
+import { BarSeriesOption, PieSeriesOption } from 'echarts/charts'
+import {
+  GridComponentOption,
+  LegendComponentOption,
+  TitleComponentOption,
+  TooltipComponentOption
+} from 'echarts/components'
+
+export type BarChartOption = echarts.ComposeOption<
+  | BarSeriesOption
+  | TitleComponentOption
+  | GridComponentOption
+  | TooltipComponentOption
+  | LegendComponentOption
+>
+
+export type PieChartOption = echarts.ComposeOption<
+  | PieSeriesOption
+  | TitleComponentOption
+  | TooltipComponentOption
+  | LegendComponentOption
+>

+ 2 - 7
src/views/exam/ExamPane.vue

@@ -1,15 +1,10 @@
 <template>
   <div>
-    <v-navigation-drawer
-      app
-      permanent
-      right
-      class="elevation-5"
-      v-if="examInfo"
-    >
+    <v-navigation-drawer app permanent right class="elevation-5">
       <div
         class="flex-column d-flex justify-space-between"
         style="height: 100vh"
+        v-if="examInfo"
       >
         <div class="flex-shrink-0">
           <div class="pa-4">

+ 75 - 25
src/views/exam/PersonalResult.vue

@@ -13,32 +13,67 @@
     </page-header>
     <v-row class="mb-5">
       <v-col class="d-flex flex-column justify-space-between">
-        <v-card> <div class="rank" ref="grade"></div></v-card>
+        <v-card class="rank">
+          <e-charts-component
+            :options="gradeOption"
+            v-if="skillOption"
+          ></e-charts-component>
+        </v-card>
         <v-card class="rank" ref="rank"> 排名情况</v-card>
       </v-col>
-      <v-spacer></v-spacer>
-      <v-col cols="6">
-        <v-card class="cv"> 个人简历</v-card>
+      <v-col>
+        <v-card class="skill">
+          <e-charts-component
+            :options="skillOption"
+            v-if="skillOption"
+          ></e-charts-component>
+        </v-card>
       </v-col>
     </v-row>
-
-    <v-card>
-      <div class="skill" ref="skill"></div>
-    </v-card>
+    <v-expansion-panels :value="0">
+      <v-expansion-panel>
+        <v-expansion-panel-header>
+          <div class="text-center display-1">试题解析</div>
+        </v-expansion-panel-header>
+        <v-expansion-panel-content class="mb-5">
+          <exam-questions-info
+            v-if="!fetching"
+            :exam-questions="examQuestions"
+            :analysis="true"
+          ></exam-questions-info>
+          <div v-else class="d-flex justify-center">
+            <v-progress-circular indeterminate></v-progress-circular>
+          </div>
+        </v-expansion-panel-content>
+      </v-expansion-panel>
+    </v-expansion-panels>
   </v-container>
 </template>
 
 <script lang="ts">
 import Vue from 'vue'
-import PageHeader from '@/components/PageHeader.vue'
-import * as echarts from 'echarts'
+import PageHeader from '@/views/components/PageHeader.vue'
+import ExamQuestionsInfo from '@/views/components/ExamQuestionsInfo.vue'
+import { ExtendedQuestionSerializer, ReceivedData } from '@/api/types'
+import { getExamQuestions } from '@/api/exam'
+import { AxiosResponse } from 'axios'
+import { getQuestionInfo } from '@/api/question'
+import EChartsComponent from '@/views/components/echarts/EChartsComponent.vue'
+import {
+  PieChartOption,
+  BarChartOption
+} from '@/views/components/echarts/echarts.type'
+
 export default Vue.extend({
   name: 'PersonalResult',
   components: {
-    PageHeader
+    PageHeader,
+    ExamQuestionsInfo,
+    EChartsComponent
   },
   data() {
     return {
+      fetching: false,
       breadcrumb: [
         {
           text: '主页',
@@ -55,21 +90,41 @@ export default Vue.extend({
           disabled: true,
           href: '/'
         }
-      ]
+      ],
+      examQuestions: [] as Array<ExtendedQuestionSerializer>,
+      skillOption: (null as unknown) as BarChartOption,
+      gradeOption: (null as unknown) as PieChartOption
     }
   },
   mounted() {
-    const skillRefs = this.$refs.skill
-    const skillChart = echarts.init(skillRefs as HTMLElement)
-    const skillChartOption = {
+    const examId = this.$route.params.examId
+    getExamQuestions(examId).then(({ data }) => {
+      this.fetching = true
+
+      let questionsPromises: Promise<
+        AxiosResponse<ReceivedData<ExtendedQuestionSerializer, 0>>
+      >[] = []
+      let questionsIds = Object.entries(data.result.questionAndScore)
+      questionsIds.forEach((v, ind) => {
+        questionsPromises[ind] = getQuestionInfo(v[0])
+      })
+
+      Promise.all(questionsPromises).then((reps) => {
+        this.examQuestions = reps.map((value) => {
+          return value.data.result
+        })
+        this.fetching = false
+      })
+    })
+
+    this.skillOption = {
       title: {
         text: '知识点掌握情况'
       },
       tooltip: {
         trigger: 'axis',
         axisPointer: {
-          // 坐标轴指示器,坐标轴触发有效
-          type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          type: 'shadow'
         }
       },
       legend: {
@@ -105,11 +160,8 @@ export default Vue.extend({
         }
       ]
     }
-    skillChart.setOption(skillChartOption)
 
-    const gradeRefs = this.$refs.grade
-    const gradeChart = echarts.init(gradeRefs as HTMLElement)
-    const gradeChartOptions = {
+    this.gradeOption = {
       tooltip: {
         trigger: 'item',
         formatter: '{a}{b}: {c} ({d}%)'
@@ -127,7 +179,7 @@ export default Vue.extend({
         {
           name: '',
           type: 'pie',
-          radius: ['50%', '70%'],
+          radius: ['30%', '70%'],
           avoidLabelOverlap: false,
           label: {
             show: false
@@ -149,8 +201,6 @@ export default Vue.extend({
         }
       ]
     }
-
-    gradeChart.setOption(gradeChartOptions)
   }
 })
 </script>
@@ -167,7 +217,7 @@ export default Vue.extend({
 }
 
 .skill {
-  /*width: 800px;*/
+  width: 800px;
   height: 35vh;
 }
 </style>

+ 0 - 1
src/views/student/Overview.vue

@@ -98,7 +98,6 @@ export default Vue.extend({
     },
     fetchExams() {
       this.fetching = true
-      this.examList = []
 
       getAllExamList({ pageNum: this.page, pageSize: this.pageSize })
         .then(({ data }) => {

+ 6 - 8
src/views/student/StudentExamList.vue

@@ -4,12 +4,6 @@
       <template #extra>
         <v-row class=" my-0 py-0" no-gutters justify="end">
           <v-spacer></v-spacer>
-          <!--          <v-col cols="3">-->
-          <!--            <v-btn color="info" @click="onJoinExam">-->
-          <!--              参加评测-->
-          <!--            </v-btn>-->
-          <!--          </v-col>-->
-
           <v-col cols="3">
             <v-select
               :items="selectItems"
@@ -87,6 +81,12 @@ export default Vue.extend({
       fetching: false
     }
   },
+  watch: {
+    selected() {
+      this.page = 1
+      this.fetchExams()
+    }
+  },
   methods: {
     changePage() {
       this.fetchExams()
@@ -98,7 +98,6 @@ export default Vue.extend({
     onJoinExam() {},
     fetchExams() {
       this.fetching = true
-      this.examList = []
 
       getExamsByStudentId(
         { pageNum: this.page, pageSize: this.pageSize },
@@ -110,7 +109,6 @@ export default Vue.extend({
           this.examList.forEach((val, index) => {
             this.examList[index].isJoined = true // 该变量仅在/exam/list 中有意义
           })
-          this.$forceUpdate()
         })
         .catch((err) => {
           console.log(err)

+ 6 - 0
src/views/student/components/StudentExamInfoCard.vue

@@ -121,6 +121,12 @@ export default Vue.extend({
       this.examInfo.endTime
     )
   },
+  updated() {
+    this.state = computeExamStateText(
+      this.examInfo.startTime,
+      this.examInfo.endTime
+    )
+  },
   methods: {
     getExamLength(startTime: LocalDateTime, endTime: LocalDateTime): string {
       return timeLength(startTime, endTime)

+ 26 - 52
src/views/teacher/ExamCreate.vue

@@ -65,7 +65,7 @@
           >
             <template v-slot:activator="{ on, attrs }">
               <v-text-field
-                v-model="startDateFormatted"
+                v-model="startDate"
                 label="开始日期"
                 prepend-icon="mdi-calendar"
                 readonly
@@ -159,9 +159,6 @@
         >
           创建评测
         </v-btn>
-        <!--        <v-btn color="primary" class="ma-4" @click="selectQuestion = true">-->
-        <!--          手动选择试题-->
-        <!--        </v-btn>-->
       </v-form>
     </v-container>
     <v-snackbar>
@@ -176,7 +173,7 @@ import PageHeader from '@/views/components/PageHeader.vue'
 import { createExam } from '@/api/exam'
 import { ExamSerializer, ID } from '@/api/types'
 import { mapGetters } from 'vuex'
-import dayjs from 'dayjs'
+import dayjs, { Dayjs } from 'dayjs'
 import { TIME_FORMAT } from '@/utils/common'
 
 export default Vue.extend({
@@ -226,14 +223,8 @@ export default Vue.extend({
       skills: [],
       difficulty: 50,
       startDate: '',
-      startDateFormatted: '',
       startTime: '',
-      endDate: '',
-      endDateFormatted: '',
-      endTime: '',
       comment: '',
-      selectedQuestions: [] as Array<ID>,
-      selectQuestion: false,
       antiCheating: false,
       isPrivate: false,
       valid: true,
@@ -251,28 +242,39 @@ export default Vue.extend({
         '3 小时'
       ],
       examLength: 0,
-      minStartTime: '',
-      minStartDate: ''
+      minStartDate: '',
+      nowDateTimeFormatted: '',
+      nowDatetime: (null as unknown) as Dayjs
     }
   },
   computed: {
     ...mapGetters(['getId']),
+    minStartTime(): string {
+      const nowDate = this.nowDateTimeFormatted.substr(0, 10)
+      const min = this.nowDatetime.add(5, 'minute').format('HH:mm') // bad
+
+      if (this.startDate === nowDate) {
+        return min
+      }
+      return ''
+    },
+
     computedEndDate(): string {
       if (this.startTime === '') {
-        return this.startDateFormatted
+        return this.startDate
       }
-      return this.computedExamEndTime.substr(0, 10)
+      return this.computedExamEndDateTime.substr(0, 10)
     },
     computedEndTime(): string {
       if (this.startTime === '') {
         return ''
       }
 
-      return this.computedExamEndTime.substr(11, 5)
+      return this.computedExamEndDateTime.substr(11, 5)
     },
-    computedExamEndTime(): string {
+    computedExamEndDateTime(): string {
       const startTime = dayjs(
-        this.startDateFormatted + ' ' + this.startTime + ':00',
+        this.startDate + ' ' + this.startTime + ':00',
         TIME_FORMAT
       )
 
@@ -285,43 +287,16 @@ export default Vue.extend({
     }
   },
 
-  watch: {
-    startDate(val) {
-      this.startDateFormatted = this.formatDate(this.startDate)
-    }
-  },
-
   methods: {
-    saveSelectedQuestions(questions: Array<ID>) {
-      console.log(questions)
-      this.selectedQuestions = questions
-    },
-    formatDate(date: string): string {
-      if (!date) return ''
-
-      const [year, month, day] = date.split('-')
-      return `${year}-${month}-${day}`
-    },
-    parseDate(date: string): string {
-      if (!date) return ''
-
-      const [month, day, year] = date.split('-')
-      return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
-    },
     onCreateExam() {
-      // if (!this.$refs.form.validate()) {
-      //   return
-      // }
-      console.log(this.valid)
-
       const obj = {
         creatorId: this.getId,
         examName: this.examName,
         position: this.position,
         skills: this.skills,
         difficulty: this.difficulty,
-        startTime: this.startDateFormatted + ' ' + this.startTime + ':00',
-        endTime: this.computedExamEndTime,
+        startTime: this.startDate + ' ' + this.startTime + ':00',
+        endTime: this.computedExamEndDateTime,
         comment: this.comment,
         antiCheating: this.antiCheating,
         isPublic: !this.isPrivate
@@ -340,11 +315,10 @@ export default Vue.extend({
     }
   },
   mounted() {
-    const now = dayjs()
-    this.startDateFormatted = now.format(TIME_FORMAT).substr(0, 10)
-
-    this.minStartTime = now.add(5, 'minute').format('HH:mm') // bad
-    this.minStartDate = this.startDateFormatted
+    this.nowDatetime = dayjs()
+    this.nowDateTimeFormatted = this.nowDatetime.format(TIME_FORMAT)
+    this.startDate = this.nowDateTimeFormatted.substr(0, 10)
+    this.minStartDate = this.startDate
   }
 })
 </script>

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

@@ -94,6 +94,12 @@ export default Vue.extend({
   computed: {
     ...mapGetters(['getId'])
   },
+  watch: {
+    selected() {
+      this.page = 1
+      this.fetchExams()
+    }
+  },
   methods: {
     changePage() {
       this.fetchExams()
@@ -107,7 +113,6 @@ export default Vue.extend({
     },
     fetchExams() {
       this.fetching = true
-      this.examList = []
 
       getExamsByTeacherId(
         { pageNum: this.page, pageSize: this.pageSize },

+ 6 - 0
src/views/teacher/components/TeacherExamInfoCard.vue

@@ -189,6 +189,12 @@ export default Vue.extend({
         this.joinedUsers = data.result
       })
     }
+  },
+  updated() {
+    this.state = computeExamStateText(
+      this.examInfo.startTime,
+      this.examInfo.endTime
+    )
   }
 })
 </script>

+ 3 - 0
vue.config.js

@@ -29,4 +29,7 @@ module.exports = {
       })
     ]
   }
+  // css: {
+  //   extract: { ignoreOrder: true }
+  // }
 }

+ 17 - 0
yarn.lock

@@ -1036,6 +1036,18 @@
   resolved "https://registry.npm.taobao.org/@types/json-schema/download/@types/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
   integrity sha1-9MfsQ+gbMZqYFRFQMXCfJph4kfA=
 
+"@types/lodash.throttle@^4.1.6":
+  version "4.1.6"
+  resolved "https://registry.npm.taobao.org/@types/lodash.throttle/download/@types/lodash.throttle-4.1.6.tgz#f5ba2c22244ee42ff6c2c49e614401a870c1009c"
+  integrity sha1-9bosIiRO5C/2wsSeYUQBqHDBAJw=
+  dependencies:
+    "@types/lodash" "*"
+
+"@types/lodash@*":
+  version "4.14.168"
+  resolved "https://registry.npm.taobao.org/@types/lodash/download/@types/lodash-4.14.168.tgz?cache=0&sync_timestamp=1610980821904&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Flodash%2Fdownload%2F%40types%2Flodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
+  integrity sha1-/iRjLnm3rePxMoka//hsql5c4Ag=
+
 "@types/marked@^1.2.1":
   version "1.2.1"
   resolved "https://registry.npm.taobao.org/@types/marked/download/@types/marked-1.2.1.tgz#9864ecc10d0edb72e7be4e94acb5fcc607c15eed"
@@ -5849,6 +5861,11 @@ lodash.memoize@^4.1.2:
   resolved "https://registry.npm.taobao.org/lodash.memoize/download/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
 
+lodash.throttle@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.npm.taobao.org/lodash.throttle/download/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
+  integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
+
 lodash.transform@^4.6.0:
   version "4.6.0"
   resolved "https://registry.npm.taobao.org/lodash.transform/download/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0"