Ver Fonte

代码取题判题,个人成绩报告

ChengYuXuan há 5 anos atrás
pai
commit
4bab9b4051

+ 7 - 1
src/api/exam.ts

@@ -31,12 +31,18 @@ export const extendExamTime = (examId: ID, endTime: LocalDateTime) => {
     `${EXAM_MODULE}/${examId}/time?endTime=${endTime}`
   )
 }
-
+// 所有题目
 export const getExamQuestions = (examId: ID) => {
   return axios.get<ReceivedData<ExamQuestionSerializer>>(
     `${EXAM_MODULE}/${examId}/questions`
   )
 }
+// 代码题的 ID
+export const getExamCodeQuestions = (examId: ID) => {
+  return axios.get<ReceivedData<Array<ID>>>(
+    `${EXAM_MODULE}/${examId}/questions/code`
+  )
+}
 
 export const joinExamByInviteCode = (inviteCode: string, examId: ID) => {
   return axios.put<ReceivedData<boolean>>(

+ 12 - 1
src/api/judge.ts

@@ -1,6 +1,6 @@
 import axios from 'axios'
 import { JUDGE_MODULE } from '@/api/prefix'
-import { ID, ReceivedData } from '@/api/types'
+import { CodeSubmitSerializer, ID, ReceivedData } from '@/api/types'
 
 export const objectiveJudge = (examId: ID, records: object) => {
   return axios.post<ReceivedData>(
@@ -8,3 +8,14 @@ export const objectiveJudge = (examId: ID, records: object) => {
     { records: records }
   )
 }
+
+export const codeJudge = (
+  examId: ID,
+  questionId: ID,
+  codeSubmit: CodeSubmitSerializer
+) => {
+  return axios.post<ReceivedData>(
+    `${JUDGE_MODULE}/code?examId=${examId}&questionId=${questionId}`,
+    codeSubmit
+  )
+}

+ 2 - 0
src/api/prefix.ts

@@ -4,3 +4,5 @@ export const EXAM_MODULE = BASE_URL + '/exam'
 export const QUESTION_MODULE = BASE_URL + '/question'
 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'

+ 4 - 6
src/api/question.ts

@@ -1,23 +1,21 @@
 import axios from 'axios'
 import { QUESTION_MODULE } from './prefix'
 import {
-  ExamQuestionSerializer,
   ID,
-  Pageable,
-  ExtendedQuestionSerializer,
-  QuestionStemSerializer,
+  TeacherQuestionSerializer,
+  QuestionSerializer,
   ReceivedData,
   SampleSerializer
 } from '@/api/types'
 
 export const getQuestionInfo = (id: ID) => {
-  return axios.get<ReceivedData<ExtendedQuestionSerializer>>(
+  return axios.get<ReceivedData<TeacherQuestionSerializer>>(
     `${QUESTION_MODULE}/${id}`
   )
 }
 
 export const getQuestionStem = (id: ID) => {
-  return axios.get<ReceivedData<QuestionStemSerializer>>(
+  return axios.get<ReceivedData<QuestionSerializer>>(
     `${QUESTION_MODULE}/${id}/stem`
   )
 }

+ 9 - 0
src/api/record.ts

@@ -0,0 +1,9 @@
+import axios from 'axios'
+import { RECORD_MODULE } from '@/api/prefix'
+import { ExamQuestionSubmitRecord, ID, ReceivedData } from '@/api/types'
+
+const getExamQuestionSubmitRecord = (examId: ID, questionId: ID) => {
+  return axios.get<ReceivedData<ExamQuestionSubmitRecord>>(
+    `${RECORD_MODULE}?examId=${examId}&questionId=${questionId}`
+  )
+}

+ 9 - 0
src/api/report.ts

@@ -0,0 +1,9 @@
+import axios from 'axios'
+import { REPORT_MODULE } from '@/api/prefix'
+import { ID, PersonalReportSerializer, ReceivedData } from '@/api/types'
+
+export const getPersonalReport = (examId: ID) => {
+  return axios.get<ReceivedData<PersonalReportSerializer>>(
+    `${REPORT_MODULE}?examId=${examId}`
+  )
+}

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

@@ -85,17 +85,31 @@ export interface OptionLabelTextPair {
   labelText?: string
 }
 
-export interface QuestionStemSerializer {
+export interface QuestionSerializer {
   id: number
   stem: string
   type: QuestionType
+  // 客观题
+  options?: OptionSerializer
+  keyPoints?: string
+  tags?: Array<string>
+  knowledgeId?: Array<string>
+}
+
+export interface ObjectiveQuestionSerializer extends QuestionSerializer {
   options: OptionSerializer
   keyPoints: string
   tags: Array<string>
   knowledgeId: Array<string>
 }
 
-export interface ExtendedQuestionSerializer extends QuestionStemSerializer {
+export interface CodeQuestionSerializer extends QuestionSerializer {
+  inputOutputMapping: Map<string, string>
+  memoryLimit: number
+  millisecondLimit: number
+}
+// TODO 代码题的格式
+export interface TeacherQuestionSerializer extends QuestionSerializer {
   answer?: string
   analysis?: string
   lastModifiedTime?: LocalDateTime
@@ -103,7 +117,7 @@ export interface ExtendedQuestionSerializer extends QuestionStemSerializer {
 
 export type ChoiceAnswer = string
 
-// export interface ChoiceQuestionSerializer extends ExtendedQuestionSerializer {
+// export interface ChoiceQuestionSerializer extends TeacherQuestionSerializer {
 //   studentId?: number
 //   single?: boolean
 //   lastCommitAnswer?: ChoiceAnswer
@@ -114,16 +128,8 @@ export interface CodeTuple {
   code: string
 }
 
-export interface CodeQuestionSerializer {
-  id?: ID
-  stem: string
-  codeTuples?: Array<CodeTuple>
-  lastCommitCodes?: Array<CodeTuple>
-}
-
 export interface ExamQuestionSerializer {
   questionAndScore: QuestionAndScore
-  // codeQuestions?: Array<CodeQuestionSerializer>
 }
 
 export interface SampleSerializer {
@@ -147,3 +153,23 @@ export interface Record {
   endAt: LocalDateTime
   startAt: LocalDateTime
 }
+
+export interface CodeSubmitSerializer {
+  code: string
+  language: string
+}
+
+export interface PersonalReportSerializer {
+  id: ID
+  score: number
+  rank: number
+  submitTime: LocalDateTime
+}
+
+export interface ExamQuestionSubmitRecord {
+  id: ID
+  questionId: ID
+  actualScore: number
+  submitAnswer: string
+  submitTime: LocalDateTime
+}

+ 5 - 0
src/mock/api/exam/_examId/questions/code/get.json

@@ -0,0 +1,5 @@
+{
+  "code": "00000",
+  "result|3": ["test-1","test-2","test-3"],
+  "msg": ""
+}

+ 5 - 0
src/mock/api/judge/code/post.json

@@ -0,0 +1,5 @@
+{
+  "code": "00000",
+  "result": "TODO",
+  "msg": ""
+}

+ 5 - 0
src/mock/api/record/get.json

@@ -0,0 +1,5 @@
+{
+  "result": ["@EXAM_QUESTION_SUBMIT_RECORD"],
+  "code": "00000",
+  "msg": ""
+}

+ 5 - 0
src/mock/api/report/get.json

@@ -0,0 +1,5 @@
+{
+  "code": "00000",
+  "result": "@STUDENT_EXAM_REPORT",
+  "msg": ""
+}

+ 10 - 3
src/mock/serializers.js

@@ -3,9 +3,9 @@ const { randomArrayValue } = require('./utils')
 const basicExtendsTypes = () => ({
   // LOCAL_DATE_TIME: () => Math.floor(new Date().getTime() / 1000),
   USER_ROLE: (role) => role || randomArrayValue(['STUDENT', 'TEACHER']),
-  // START_TIME: () =>
-  //   randomArrayValue(['2021-02-01 00:00:00', '2021-03-01 00:00:00']),
-  START_TIME: () => randomArrayValue(['2021-02-01 00:00:00']),
+  START_TIME: () =>
+    randomArrayValue(['2021-02-01 00:00:00', '2021-03-01 00:00:00']),
+  // START_TIME: () => randomArrayValue(['2021-02-01 00:00:00']),
   END_TIME: () => randomArrayValue(['2021-04-01 00:00:00']),
   BUILD_STATUS: (status) =>
     status ||
@@ -203,6 +203,13 @@ const serializersPlugin = (mock) => ({
   EXAM_USERS_SERIALIZER: () =>
     mock({
       'users|10-100': ['@USER_SERIALIZER']
+    }),
+  EXAM_QUESTION_SUBMIT_RECORD: () =>
+    mock({
+      'id|1-1000': 1,
+      'questionId|1-1000': 1,
+      'actualScore|1-10': 1,
+      submitAnswer: '@character("ABCD")'
     })
 })
 

+ 7 - 4
src/views/components/ExamQuestionsInfo.vue

@@ -15,10 +15,13 @@
           ></markdown-text>
         </v-col>
         <v-col class="flex-shrink-1" cols="2">
-          <v-card-text> 知识点: {{ question.keyPoints }} </v-card-text>
+          <v-card-text v-if="question.keyPoints">
+            知识点: {{ question.keyPoints }}
+          </v-card-text>
+          <v-card-text v-else class="text-end">编程题</v-card-text>
         </v-col>
       </v-row>
-      <v-row class="px-10 d-block" v-if="analysis">
+      <v-row class="px-10 d-block" v-if="analysis && question.type !== 'code'">
         <v-col class="grey lighten-3 py-1">
           <markdown-text
             :raw="question.analysis"
@@ -39,14 +42,14 @@
 
 <script lang="ts">
 import Vue, { PropType } from 'vue'
-import { ExtendedQuestionSerializer, OptionSerializer } from '@/api/types'
+import { TeacherQuestionSerializer, OptionSerializer } from '@/api/types'
 import MarkdownText from '@/views/exam/components/MarkdownText.vue'
 
 export default Vue.extend({
   name: 'ExamQuestionsInfo',
   props: {
     examQuestions: {
-      type: Array as PropType<Array<ExtendedQuestionSerializer>>,
+      type: Array as PropType<Array<TeacherQuestionSerializer>>,
       required: true
     },
     analysis: {

+ 7 - 3
src/views/components/echarts/EChartsComponent.vue

@@ -8,10 +8,11 @@ import { EChartsType } from 'echarts/core'
 import throttle from 'lodash.throttle'
 import {
   BarChartOption,
-  PieChartOption
+  PieChartOption,
+  GaugeChartOption
 } from '@/views/components/echarts/echarts.type'
 import * as echarts from 'echarts/core'
-import { BarChart, PieChart } from 'echarts/charts'
+import { BarChart, GaugeChart, PieChart } from 'echarts/charts'
 import {
   GridComponent,
   LegendComponent,
@@ -27,13 +28,16 @@ echarts.use([
   LegendComponent,
   BarChart,
   PieChart,
+  GaugeChart,
   CanvasRenderer
 ])
 
 export default Vue.extend({
   name: 'EChartsComponent',
   props: {
-    options: Object as PropType<BarChartOption | PieChartOption>
+    options: Object as PropType<
+      BarChartOption | PieChartOption | GaugeChartOption
+    >
   },
   data() {
     return {

+ 7 - 1
src/views/components/echarts/echarts.type.d.ts

@@ -1,5 +1,9 @@
 import * as echarts from 'echarts/core'
-import { BarSeriesOption, PieSeriesOption } from 'echarts/charts'
+import {
+  BarSeriesOption,
+  PieSeriesOption,
+  GaugeSeriesOption
+} from 'echarts/charts'
 import {
   GridComponentOption,
   LegendComponentOption,
@@ -21,3 +25,5 @@ export type PieChartOption = echarts.ComposeOption<
   | TooltipComponentOption
   | LegendComponentOption
 >
+
+export type GaugeChartOption = echarts.ComposeOption<GaugeSeriesOption>

+ 37 - 33
src/views/exam/BeforeExam.vue

@@ -2,40 +2,40 @@
   <v-container>
     <page-header title="考前确认" :breadcrumb="breadcrumb">
       <template #extra>
-        <v-row justify="end">
-          <v-spacer></v-spacer>
-          <v-col cols="3" v-if="canEnter">
-            <router-link
-              :to="{ name: 'exam-pane' }"
-              style="text-decoration: none;"
-            >
-              <v-btn color="success">进入评测</v-btn>
-            </router-link>
-          </v-col>
-          <v-col cols="2">
-            <router-link to="/student/exams" style="text-decoration: none">
-              <v-btn>返回</v-btn>
-            </router-link>
-          </v-col>
+        <v-row justify="end" no-gutters>
+          <router-link
+            :to="{ name: 'exam-pane' }"
+            style="text-decoration: none;"
+          >
+            <v-btn color="success" class="mr-2">进入评测</v-btn>
+          </router-link>
+          <router-link to="/student/exams" style="text-decoration: none">
+            <v-btn class="ml-2">返回</v-btn>
+          </router-link>
         </v-row>
       </template>
     </page-header>
-    <v-card class="mb-5" hover style="cursor: default" v-if="examInfo">
-      <v-card-title class="text-h4">{{ examInfo.examName }}</v-card-title>
-      <v-card-subtitle class="text-h5 black--text">评测信息</v-card-subtitle>
-      <v-card-text class="text-body-1 black--text"
-        >开始时间: {{ examInfo.startTime }} 结束时间:
-        {{ examInfo.endTime }} 评测时长:{{ examLength }}
-      </v-card-text>
+    <v-container>
+      <v-card class="mb-5" hover style="cursor: default" v-if="examInfo">
+        <v-card-title class="text-h4">{{ examInfo.examName }}</v-card-title>
+        <v-card-subtitle class="text-h5 black--text">评测信息</v-card-subtitle>
+        <v-card-text class="text-body-1 black--text"
+          >开始时间: {{ examInfo.startTime }} 结束时间:
+          {{ examInfo.endTime }} 评测时长:{{ examLength }}
+        </v-card-text>
 
-      <v-card-subtitle class="text-h5 black--text">评测规则</v-card-subtitle>
-      <v-card-text>{{ examInfo.comment }}</v-card-text>
-      <v-card-subtitle class="text-h5 black--text">评测内容</v-card-subtitle>
-      <v-card-text class="text-body-1 black--text" v-if="examQuestions">
-        客观题 {{ Object.keys(examQuestions.questionAndScore).length }} 道
-        编程题 {{}} 道
-      </v-card-text>
-    </v-card>
+        <v-card-subtitle class="text-h5 black--text">评测规则</v-card-subtitle>
+        <v-card-text>{{ examInfo.comment }}</v-card-text>
+        <v-card-subtitle class="text-h5 black--text">评测内容</v-card-subtitle>
+        <v-card-text
+          class="text-body-1 black--text"
+          v-if="examQuestionsNum > 0"
+        >
+          客观题 {{ examQuestionsNum - codeQuestionsNum }} 道 编程题
+          {{ codeQuestionsNum }} 道
+        </v-card-text>
+      </v-card>
+    </v-container>
   </v-container>
 </template>
 
@@ -43,7 +43,7 @@
 import Vue from 'vue'
 import PageHeader from '@/views/components/PageHeader.vue'
 import { ExamQuestionSerializer, ExamSerializer } from '@/api/types'
-import { getExamById, getExamQuestions } from '@/api/exam'
+import { getExamById, getExamCodeQuestions, getExamQuestions } from '@/api/exam'
 import { TIME_FORMAT, timeLength } from '@/utils/common'
 import dayjs from 'dayjs'
 
@@ -74,7 +74,8 @@ export default Vue.extend({
       ],
       examInfo: (null as unknown) as ExamSerializer,
       examLength: '',
-      examQuestions: (null as unknown) as ExamQuestionSerializer,
+      examQuestionsNum: 0,
+      codeQuestionsNum: 0,
       canEnter: false
     }
   },
@@ -83,7 +84,10 @@ export default Vue.extend({
 
     getExamQuestions(examId)
       .then(({ data }) => {
-        this.examQuestions = data.result
+        this.examQuestionsNum = Object.keys(data.result.questionAndScore).length
+        getExamCodeQuestions(examId).then(({ data }) => {
+          this.codeQuestionsNum = data.result.length
+        })
       })
       .catch((err) => {
         console.log(err)

+ 122 - 71
src/views/exam/ExamPane.vue

@@ -24,6 +24,7 @@
             :currentTime="currentTime"
             :endTime="endTime"
             @timesUp="onTimesUp"
+            @endTimeChanged="endTimeChanged = true"
           ></count-down>
         </div>
 
@@ -36,22 +37,45 @@
               <h2>客观题</h2>
             </div>
             <v-progress-linear
-              :value="(currentQuestionIndex / totalQuestionNum) * 100"
-              :buffer-value="
-                ((currentQuestionIndex + 1) / totalQuestionNum) * 100
-              "
+              :value="((currentQuestionIndex + 1) / objectiveQuestionNum) * 100"
+              :buffer-value="100"
               class="my-3"
             ></v-progress-linear>
             <div class="d-flex justify-space-around">
-              <v-chip label>
-                {{ currentQuestionIndex + 1 }} / {{ totalQuestionNum }}</v-chip
+              <v-chip
+                label
+                v-if="currentQuestionIndex < codeQuestionStartIndex"
+              >
+                {{ currentQuestionIndex + 1 }} /
+                {{ objectiveQuestionNum }}</v-chip
               >
+              <v-chip label v-else>
+                {{ objectiveQuestionNum }} / {{ objectiveQuestionNum }}
+              </v-chip>
             </div>
           </div>
           <div class="justify-space-between">
             <div class="d-flex justify-center">
               <h2>代码题</h2>
             </div>
+            <v-progress-linear
+              :value="
+                (currentQuestionIndex -
+                  codeQuestionStartIndex / codeQuestionNum) *
+                  100
+              "
+              :buffer-value="100"
+              class="my-3"
+            ></v-progress-linear>
+            <div
+              class="d-flex justify-space-around"
+              v-if="currentQuestionIndex >= codeQuestionStartIndex"
+            >
+              <v-chip label>
+                {{ currentQuestionIndex - codeQuestionStartIndex + 1 }} /
+                {{ codeQuestionNum }}</v-chip
+              >
+            </div>
           </div>
         </div>
         <div style="height: 20vh;width: 80%" class="align-self-center">
@@ -114,7 +138,27 @@
     </v-navigation-drawer>
     <v-main class="indigo lighten-5 pa-0">
       <v-container fluid class="pa-2">
-        <v-card class="questionCard">
+        <v-window
+          :value="currentQuestionIndex - codeQuestionStartIndex"
+          v-if="isCodeQuestion"
+        >
+          <v-window-item v-for="i in codeQuestionNum" :key="i">
+            <code-pane
+              :question="codeQuestions[i - 1]"
+              :total-question-num="codeQuestionNum"
+              @nextQuestion="nextQuestion"
+              @prevQuestion="prevQuestion"
+            >
+              <template #index>
+                <v-chip class="align-self-center"
+                  >{{ currentQuestionIndex - objectiveQuestionNum + 1 }} /
+                  {{ codeQuestionNum }}</v-chip
+                >
+              </template>
+            </code-pane>
+          </v-window-item>
+        </v-window>
+        <v-card class="questionCard" v-else>
           <div v-if="!fetchingStem">
             <blank-pane
               v-if="questionType() === 'blank'"
@@ -124,7 +168,7 @@
               <template #index>
                 <v-chip class="align-self-center"
                   >{{ currentQuestionIndex + 1 }} /
-                  {{ totalQuestionNum }}</v-chip
+                  {{ objectiveQuestionNum }}</v-chip
                 >
               </template>
             </blank-pane>
@@ -138,7 +182,7 @@
               <template #index>
                 <v-chip class="align-self-center"
                   >{{ currentQuestionIndex + 1 }} /
-                  {{ totalQuestionNum }}</v-chip
+                  {{ objectiveQuestionNum }}</v-chip
                 >
               </template>
             </choice-pane>
@@ -153,41 +197,21 @@
             >
           </div>
         </v-card>
-
-        <code-pane v-if="isCodeQuestion && questionType() === 'code'">
-          <v-chip class="align-self-center"
-            >{{ currentQuestionIndex + 1 }} / {{ totalQuestionNum }}</v-chip
-          >
-        </code-pane>
-        <!--   先用v-if(codeIndex只在初始为负数),再用v-show。只用v-show的时候,会出现第一个代码题editor无法渲染的问题,原因未知-->
-        <!--                <v-window-->
-        <!--                  :value="codeIndex"-->
-        <!--                  v-if="codeIndex >= 0"-->
-        <!--                  v-show="!isChoiceProblem"-->
-        <!--                >-->
-        <!--                  <v-window-item v-for="i in codeQuestionNum" :key="i">-->
-        <!--        <code-pane-->
-        <!--          :question="codeQuestions[i - 1]"-->
-        <!--          :total-question-num="curTypeQuestionNum"-->
-        <!--          @nextQuestion="nextQuestion"-->
-        <!--          @submitCode="saveSubmittedCode"-->
-        <!--        ></code-pane>-->
-        <!--                  </v-window-item>-->
-        <!--                </v-window>-->
       </v-container>
-      <v-snackbar v-model="finished" absolute>
-        已到最后一题
-      </v-snackbar>
-      <v-snackbar v-model="submitting" absolute centered color="info">
-        <span v-if="examTimesUp">评测结束</span> 提交试卷中...
-      </v-snackbar>
-      <v-snackbar v-model="examAvailableDialog" centered absolute color="error">
-        评测不可用
-      </v-snackbar>
-      <v-snackbar v-model="submittedError" absolute>
-        提交失败,请重试
-      </v-snackbar>
     </v-main>
+    <v-snackbar v-model="finished" absolute>
+      已到最后一题
+    </v-snackbar>
+    <v-snackbar v-model="submitting" absolute centered color="info">
+      <span v-if="examTimesUp">评测结束</span> 提交试卷中...
+    </v-snackbar>
+    <v-snackbar v-model="examAvailableDialog" centered absolute color="error">
+      评测不可用
+    </v-snackbar>
+    <v-snackbar v-model="submittedError" absolute>
+      提交失败,请重试
+    </v-snackbar>
+    <v-snackbar v-model="endTimeChanged" absolute>评测已延长</v-snackbar>
   </div>
 </template>
 
@@ -201,13 +225,13 @@ import {
   ExamQuestionSerializer,
   ExamSerializer,
   CodeQuestionSerializer,
-  QuestionStemSerializer,
+  QuestionSerializer,
   QuestionType,
   Record,
   LocalDateTime,
   ID
 } from '@/api/types'
-import { getExamById, getExamQuestions } from '@/api/exam'
+import { getExamById, getExamQuestions, getExamCodeQuestions } from '@/api/exam'
 import { getQuestionStem } from '@/api/question'
 import { objectiveJudge } from '@/api/judge'
 import dayjs from 'dayjs'
@@ -231,41 +255,33 @@ export default Vue.extend({
       examInfo: (null as unknown) as ExamSerializer,
       endTime: 0,
       examQuestions: (null as unknown) as ExamQuestionSerializer,
-
-      currentQuestion: (null as unknown) as QuestionStemSerializer,
+      currentQuestion: (null as unknown) as QuestionSerializer,
       currentQuestionIndex: -1,
       objectiveAnswers: {} as Map<ID, Record>,
       questionIds: [] as Array<string>,
       questionStartTime: dayjs().format(TIME_FORMAT) as LocalDateTime,
-      examId: 0,
-      // codeQuestions: [] as Array<CodeQuestionSerializer>,
-      // codeIndex: -1
+      examId: 0 as ID,
+      codeQuestions: [] as Array<CodeQuestionSerializer>,
       examAvailableDialog: false,
       submitting: false,
       finished: false,
       answered: false,
-      examTimesUp: false
+      examTimesUp: false,
+      codeQuestionIds: [] as ID[],
+      codeQuestionStartIndex: 0,
+      objectiveQuestionNum: 0,
+      codeQuestionNum: 0,
+      endTimeChanged: false
     }
   },
   computed: {
     isCodeQuestion(): boolean {
-      return false
-    },
-    // codeQuestionNum(): number {
-    //   return this.codeQuestions.length
-    // },
-    objectiveQuestionNum(): number {
-      if (this.examQuestions) {
-        return Object.keys(this.examQuestions.questionAndScore).length
-      }
-      return 0
+      return this.currentQuestionIndex >= this.codeQuestionStartIndex
     },
     totalQuestionNum(): number {
-      return this.objectiveQuestionNum
-      // return this.objectiveQuestionNum + this.codeQuestionNum
+      return this.objectiveQuestionNum + this.codeQuestionNum
     }
   },
-  watch: {},
   methods: {
     questionType(): QuestionType {
       return this.currentQuestion.type
@@ -334,7 +350,22 @@ export default Vue.extend({
           this.submittedError = true
         })
     },
+    // 仅限编程题
+    prevQuestion() {
+      if (this.currentQuestionIndex > this.codeQuestionStartIndex) {
+        this.currentQuestionIndex--
+      }
+    },
     nextQuestion() {
+      if (this.currentQuestionIndex >= this.codeQuestionStartIndex) {
+        if (this.currentQuestionIndex < this.totalQuestionNum - 1) {
+          this.currentQuestionIndex++
+        } else {
+          this.finished = true
+        }
+        return
+      }
+
       if (this.currentQuestionIndex < this.totalQuestionNum - 1) {
         if (
           !this.objectiveAnswers.get(
@@ -352,7 +383,7 @@ export default Vue.extend({
       }
     },
     fetchData() {
-      this.examId = parseInt(this.$route.params.examId)
+      this.examId = this.$route.params.examId
 
       getExamById(this.examId).then(({ data }) => {
         this.examInfo = data.result
@@ -366,16 +397,36 @@ export default Vue.extend({
             this.$router.push({ name: 'Home' })
           }, 1000)
         }
-      })
+        getExamQuestions(this.examId).then(({ data }) => {
+          this.examQuestions = data.result
+          this.questionIds = Object.keys(this.examQuestions.questionAndScore)
 
-      getExamQuestions(this.examId).then(({ data }) => {
-        this.examQuestions = data.result
-        this.questionIds = Object.keys(this.examQuestions.questionAndScore)
+          getExamCodeQuestions(this.examId).then(({ data }) => {
+            this.codeQuestionIds = data.result
+            this.questionIds = this.questionIds.filter((value) => {
+              return !this.codeQuestionIds.find((v) => v === value)
+            })
 
-        this.currentQuestionIndex = 0
-        this.fetchQuestionStem(0)
+            this.codeQuestionStartIndex = this.questionIds.length
+            this.objectiveQuestionNum = this.questionIds.length
+            this.codeQuestionNum = this.codeQuestionIds.length
+
+            let codeQuestionsPromises = this.codeQuestionIds.map((val) => {
+              return getQuestionStem(val)
+            })
 
-        this.objectiveAnswers = new Map<ID, Record>()
+            Promise.all(codeQuestionsPromises).then((reps) => {
+              this.codeQuestions = reps.map((value) => {
+                return value.data.result as CodeQuestionSerializer
+              })
+            })
+          })
+
+          this.currentQuestionIndex = 0
+          this.fetchQuestionStem(0)
+
+          this.objectiveAnswers = new Map<ID, Record>()
+        })
       })
     }
   },

+ 114 - 67
src/views/exam/PersonalResult.vue

@@ -2,51 +2,58 @@
   <v-container>
     <page-header title="评测报告" :breadcrumb="breadcrumb">
       <template #extra>
-        <v-row justify="end">
-          <v-col cols="2">
-            <router-link to="/student/exams" style="text-decoration: none">
-              <v-btn>返回</v-btn>
-            </router-link>
-          </v-col>
+        <v-row no-gutters justify="end">
+          <router-link to="/student/exams" style="text-decoration: none">
+            <v-btn>返回</v-btn>
+          </router-link>
         </v-row>
       </template>
     </page-header>
-    <v-row class="mb-5">
-      <v-col class="d-flex flex-column justify-space-between">
-        <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-col>
-        <v-card class="skill">
-          <e-charts-component
-            :options="skillOption"
-            v-if="skillOption"
-          ></e-charts-component>
-        </v-card>
-      </v-col>
-    </v-row>
-    <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>
+      <v-row class="mb-5" style="height: 30vh">
+        <v-col cols="3">
+          <v-card style="height: 100%">
+            <e-charts-component
+              :options="gradeOption"
+              v-if="gradeOption"
+            ></e-charts-component>
+          </v-card>
+        </v-col>
+        <v-col cols="3">
+          <v-card style="height: 100%">
+            <e-charts-component
+              :options="rankOption"
+              v-if="rankOption"
+            ></e-charts-component>
+          </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-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>
   </v-container>
 </template>
 
@@ -54,15 +61,17 @@
 import Vue from 'vue'
 import PageHeader from '@/views/components/PageHeader.vue'
 import ExamQuestionsInfo from '@/views/components/ExamQuestionsInfo.vue'
-import { ExtendedQuestionSerializer, ReceivedData } from '@/api/types'
+import { TeacherQuestionSerializer, 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
+  BarChartOption,
+  GaugeChartOption
 } from '@/views/components/echarts/echarts.type'
+import { getPersonalReport } from '@/api/report'
 
 export default Vue.extend({
   name: 'PersonalResult',
@@ -91,18 +100,26 @@ export default Vue.extend({
           href: '/'
         }
       ],
-      examQuestions: [] as Array<ExtendedQuestionSerializer>,
+      examQuestions: [] as Array<TeacherQuestionSerializer>,
       skillOption: (null as unknown) as BarChartOption,
-      gradeOption: (null as unknown) as PieChartOption
+      gradeOption: (null as unknown) as PieChartOption,
+      rankOption: (null as unknown) as GaugeChartOption
     }
   },
   mounted() {
     const examId = this.$route.params.examId
+    let rank = 0
+    let score = 0
+    getPersonalReport(examId).then(({ data }) => {
+      rank = data.result.rank
+      score = data.result.score
+    })
+
     getExamQuestions(examId).then(({ data }) => {
       this.fetching = true
 
       let questionsPromises: Promise<
-        AxiosResponse<ReceivedData<ExtendedQuestionSerializer, 0>>
+        AxiosResponse<ReceivedData<TeacherQuestionSerializer, 0>>
       >[] = []
       let questionsIds = Object.entries(data.result.questionAndScore)
       questionsIds.forEach((v, ind) => {
@@ -167,7 +184,7 @@ export default Vue.extend({
         formatter: '{a}{b}: {c} ({d}%)'
       },
       title: {
-        text: '正确率',
+        text: '得分   ',
         left: 'right'
       },
       legend: {
@@ -179,7 +196,7 @@ export default Vue.extend({
         {
           name: '',
           type: 'pie',
-          radius: ['30%', '70%'],
+          radius: ['50%', '70%'],
           avoidLabelOverlap: false,
           label: {
             show: false
@@ -195,8 +212,53 @@ export default Vue.extend({
             show: false
           },
           data: [
-            { value: 20, name: '错误' },
-            { value: 80, name: '正确' }
+            { value: 100 - score, name: '错误' },
+            { value: score, name: '正确' }
+          ]
+        }
+      ]
+    }
+
+    this.rankOption = {
+      series: [
+        {
+          type: 'gauge',
+          startAngle: 180,
+          endAngle: 0,
+          min: 100,
+          max: 0,
+          splitNumber: 5,
+          axisLabel: {
+            distance: 15,
+            fontSize: 10
+          },
+
+          progress: {
+            show: true,
+            roundCap: true,
+            width: 10
+          },
+
+          detail: {
+            formatter: function(value) {
+              return '排名前 {value|' + value.toFixed(0) + '}{unit|%}'
+            },
+            rich: {
+              value: {
+                fontSize: 20,
+                fontWeight: 'bolder',
+                color: '#777'
+              },
+              unit: {
+                fontSize: 20,
+                color: '#999'
+              }
+            }
+          },
+          data: [
+            {
+              value: rank
+            }
           ]
         }
       ]
@@ -205,19 +267,4 @@ export default Vue.extend({
 })
 </script>
 
-<style scoped>
-.rank {
-  width: 20vw;
-  height: 15vh;
-}
-
-.cv {
-  /*width: 30vw;*/
-  height: 40vh;
-}
-
-.skill {
-  width: 800px;
-  height: 35vh;
-}
-</style>
+<style scoped></style>

+ 2 - 3
src/views/exam/components/BlankPane.vue

@@ -21,8 +21,7 @@
 <script lang="ts">
 import Vue, { PropType } from 'vue'
 import MarkdownText from '@/views/exam/components/MarkdownText.vue'
-import { QuestionStemSerializer, OptionSerializer } from '@/api/types'
-import { clone } from 'ramda'
+import { QuestionSerializer } from '@/api/types'
 export default Vue.extend({
   name: 'BlankPane',
   components: {
@@ -30,7 +29,7 @@ export default Vue.extend({
   },
   props: {
     question: {
-      type: Object as PropType<QuestionStemSerializer>,
+      type: Object as PropType<QuestionSerializer>,
       required: true
     }
   },

+ 4 - 4
src/views/exam/components/ChoicePane.vue

@@ -60,7 +60,7 @@
 <script lang="ts">
 import Vue, { PropType } from 'vue'
 import MarkdownText from '@/views/exam/components/MarkdownText.vue'
-import { QuestionStemSerializer, OptionLabelTextPair } from '@/api/types'
+import { QuestionSerializer, OptionLabelTextPair } from '@/api/types'
 
 import { clone } from 'ramda'
 export default Vue.extend({
@@ -70,7 +70,7 @@ export default Vue.extend({
   },
   props: {
     question: {
-      type: Object as PropType<QuestionStemSerializer>,
+      type: Object as PropType<QuestionSerializer>,
       required: true
     }
   },
@@ -106,8 +106,8 @@ export default Vue.extend({
   },
   methods: {
     init() {
-      let keys = Object.keys(this.question.options)
-      let values = Object.values(this.question.options)
+      let keys = Object.keys(this.question.options ?? {})
+      let values = Object.values(this.question.options ?? {})
       this.options = []
 
       for (let i = 0; i < keys.length; i++) {

+ 277 - 267
src/views/exam/components/CodePane.vue

@@ -1,159 +1,161 @@
 <template>
-  <v-card class="d-flex flex-column questionCard pa-0">
-    <!--    <div class="my-1 d-flex flex-grow-1 pa-1">-->
-    <!--      <div class="flex-grow-1 d-flex flex-column justify-space-between">-->
-    <!--        <div>-->
-    <!--          <v-tabs v-model="tab" class="pa-0 ma-0 elevation-1" grow>-->
-    <!--            <v-tab>题目描述</v-tab>-->
-    <!--            <v-tab>提交记录</v-tab>-->
-    <!--          </v-tabs>-->
-    <!--          <v-tabs-items v-model="tab">-->
-    <!--            <v-tab-item>-->
-    <!--              <div class="overflow-y-auto" style="height: 80vh;max-width: 30vw">-->
-    <!--                <markdown-text-->
-    <!--                  :raw="question.stem"-->
-    <!--                  style="height: 100%"-->
-    <!--                ></markdown-text>-->
-    <!--              </div>-->
-    <!--            </v-tab-item>-->
-    <!--            <v-tab-item>-->
-    <!--              <div class="d-flex flex-column justify-space-between pa-2">-->
-    <!--                <v-card-->
-    <!--                  flat-->
-    <!--                  class="d-flex flex-column justify-space-between mb-5"-->
-    <!--                >-->
-    <!--                  <div class="d-inline mb-2">-->
-    <!--                    执行结果:-->
-    <!--                    <v-chip-->
-    <!--                      :color="statusToColor(currentCommit.status)"-->
-    <!--                      outlined-->
-    <!--                      >{{ currentCommit.status }}</v-chip-->
-    <!--                    >-->
-    <!--                  </div>-->
-    <!--                  <v-card-text-->
-    <!--                    style="background-color: darkgray; height: 30vh"-->
-    <!--                    class="rounded ma-0 mb-5 overflow-y-auto pa-2"-->
-    <!--                    v-if="-->
-    <!--                      currentCommit && currentCommit.status === '编译或运行失败'-->
-    <!--                    "-->
-    <!--                  >-->
-    <!--                    TODO-->
-    <!--                  </v-card-text>-->
-    <!--                </v-card>-->
-    <!--                &lt;!&ndash;                <v-simple-table fixed-header height="40vh" class="flex-grow-1">&ndash;&gt;-->
-    <!--                <v-simple-table-->
-    <!--                  fixed-header-->
-    <!--                  :height="-->
-    <!--                    currentCommit && currentCommit.status === '编译或运行失败'-->
-    <!--                      ? 35 + 'vh'-->
-    <!--                      : 70 + 'vh'-->
-    <!--                  "-->
-    <!--                >-->
-    <!--                  <thead>-->
-    <!--                    <tr>-->
-    <!--                      <th>提交时间</th>-->
-    <!--                      <th>状态</th>-->
-    <!--                      <th>成绩</th>-->
-    <!--                      <th>操作</th>-->
-    <!--                    </tr>-->
-    <!--                  </thead>-->
-    <!--                  <tbody>-->
-    <!--                    <tr v-for="item in commits" :key="item.id">-->
-    <!--                      <td>{{ item.submitTime }}</td>-->
-    <!--                      <td>-->
-    <!--                        <v-chip-->
-    <!--                          :color="statusToColor(item.status)"-->
-    <!--                          small-->
-    <!--                          outlined-->
-    <!--                        >-->
-    <!--                          {{ item.status }}-->
-    <!--                        </v-chip>-->
-    <!--                      </td>-->
-    <!--                      <td>{{ item.score }}</td>-->
-    <!--                      <td>-->
-    <!--                        <v-btn small>详情</v-btn>-->
-    <!--                      </td>-->
-    <!--                    </tr>-->
-    <!--                  </tbody>-->
-    <!--                </v-simple-table>-->
-    <!--              </div>-->
-    <!--            </v-tab-item>-->
-    <!--          </v-tabs-items>-->
-    <!--        </div>-->
-
-    <!--        <div style="height: 32px" class="d-flex justify-space-between">-->
-    <!--          <span>-->
-    <!--            <slot name="index"></slot>-->
-    <!--            <v-card-title> 代码题</v-card-title>-->
-    <!--          </span>-->
-    <!--          <v-btn color="info" small @click="nextQuestion">下一题</v-btn>-->
-    <!--        </div>-->
-    <!--      </div>-->
-    <!--      <v-divider vertical class="mx-1"></v-divider>-->
-    <!--      <div style="width: 50vw; height: 100%" class="d-flex flex-column">-->
-    <!--        <div-->
-    <!--          style="height: 48px"-->
-    <!--          class="d-flex pr-5 justify-space-between elevation-1 mb-1"-->
-    <!--        >-->
-    <!--          <div class="d-flex ml-5">-->
-    <!--            <div class="d-flex">-->
-    <!--              <v-select-->
-    <!--                :items="languages"-->
-    <!--                v-model="currentLang"-->
-    <!--                dense-->
-    <!--                style="width: 120px"-->
-    <!--                class=" ma-0 pa-0"-->
-    <!--                solo-->
-    <!--              >-->
-    <!--              </v-select>-->
-    <!--            </div>-->
-    <!--            <v-tooltip top>-->
-    <!--              <template v-slot:activator="{ on, attrs }">-->
-    <!--                <v-btn-->
-    <!--                  fab-->
-    <!--                  x-small-->
-    <!--                  tile-->
-    <!--                  class="mx-2 mb-2 pa-0 elevation-1 align-self-center"-->
-    <!--                  v-bind="attrs"-->
-    <!--                  v-on="on"-->
-    <!--                  @click="resetLastCommit"-->
-    <!--                >-->
-    <!--                  <v-icon>mdi-arrow-left</v-icon>-->
-    <!--                </v-btn>-->
-    <!--              </template>-->
-    <!--              <span>恢复到上次提交的代码</span>-->
-    <!--            </v-tooltip>-->
-    <!--            <v-tooltip top>-->
-    <!--              <template v-slot:activator="{ on, attrs }">-->
-    <!--                <v-btn-->
-    <!--                  fab-->
-    <!--                  x-small-->
-    <!--                  tile-->
-    <!--                  class="mx-2 mb-2 pa-0 elevation-1 align-self-center"-->
-    <!--                  v-bind="attrs"-->
-    <!--                  v-on="on"-->
-    <!--                  @click="resetOriginCode"-->
-    <!--                >-->
-    <!--                  <v-icon>mdi-refresh</v-icon>-->
-    <!--                </v-btn>-->
-    <!--              </template>-->
-    <!--              <span>重置代码至初始状态</span>-->
-    <!--            </v-tooltip>-->
-    <!--          </div>-->
-    <!--          <v-btn-->
-    <!--            color="primary"-->
-    <!--            class="mx-2 mb-2 align-self-center"-->
-    <!--            small-->
-    <!--            v-on:click="submitCode"-->
-    <!--            >保存测试</v-btn-->
-    <!--          >-->
-    <!--        </div>-->
-    <!--        <code-editor-->
-    <!--          :language="currentLang"-->
-    <!--          v-model="currentCode"-->
-    <!--        ></code-editor>-->
-    <!--      </div>-->
-    <!--    </div>-->
+  <v-card class="d-flex flex-column code-question pa-0">
+    <div class="my-1 d-flex flex-grow-1 pa-1">
+      <div class="flex-grow-1 d-flex flex-column justify-space-between">
+        <div>
+          <v-tabs v-model="tab" class="pa-0 ma-0 elevation-1" grow>
+            <v-tab>题目描述</v-tab>
+            <v-tab>提交记录</v-tab>
+          </v-tabs>
+          <v-tabs-items v-model="tab">
+            <v-tab-item>
+              <div
+                class="overflow-y-auto px-2"
+                style="height: 85vh;max-width: 30vw"
+              >
+                <markdown-text
+                  :raw="question.stem"
+                  style="height: 100%;margin-top:0"
+                ></markdown-text>
+              </div>
+            </v-tab-item>
+            <!--            <v-tab-item>-->
+            <!--              <div class="d-flex flex-column justify-space-between pa-2">-->
+            <!--                <v-card-->
+            <!--                  flat-->
+            <!--                  class="d-flex flex-column justify-space-between mb-5"-->
+            <!--                >-->
+            <!--                  <div class="d-inline mb-2">-->
+            <!--                    执行结果:-->
+            <!--                    <v-chip-->
+            <!--                      :color="statusToColor(currentCommit.status)"-->
+            <!--                      outlined-->
+            <!--                      >{{ currentCommit.status }}</v-chip-->
+            <!--                    >-->
+            <!--                  </div>-->
+            <!--                  <v-card-text-->
+            <!--                    style="background-color: darkgray; height: 30vh"-->
+            <!--                    class="rounded ma-0 mb-5 overflow-y-auto pa-2"-->
+            <!--                    v-if="-->
+            <!--                      currentCommit && currentCommit.status === '编译或运行失败'-->
+            <!--                    "-->
+            <!--                  >-->
+            <!--                    TODO-->
+            <!--                  </v-card-text>-->
+            <!--                </v-card>-->
+            <!--                <v-simple-table-->
+            <!--                  fixed-header-->
+            <!--                  :height="-->
+            <!--                    currentCommit && currentCommit.status === '编译或运行失败'-->
+            <!--                      ? 35 + 'vh'-->
+            <!--                      : 70 + 'vh'-->
+            <!--                  "-->
+            <!--                >-->
+            <!--                  <thead>-->
+            <!--                    <tr>-->
+            <!--                      <th>提交时间</th>-->
+            <!--                      <th>状态</th>-->
+            <!--                      <th>成绩</th>-->
+            <!--                      <th>操作</th>-->
+            <!--                    </tr>-->
+            <!--                  </thead>-->
+            <!--                  <tbody>-->
+            <!--                    <tr v-for="item in commits" :key="item.id">-->
+            <!--                      <td>{{ item.submitTime }}</td>-->
+            <!--                      <td>-->
+            <!--                        <v-chip-->
+            <!--                          :color="statusToColor(item.status)"-->
+            <!--                          small-->
+            <!--                          outlined-->
+            <!--                        >-->
+            <!--                          {{ item.status }}-->
+            <!--                        </v-chip>-->
+            <!--                      </td>-->
+            <!--                      <td>{{ item.score }}</td>-->
+            <!--                      <td>-->
+            <!--                        <v-btn small>详情</v-btn>-->
+            <!--                      </td>-->
+            <!--                    </tr>-->
+            <!--                  </tbody>-->
+            <!--                </v-simple-table>-->
+            <!--              </div>-->
+            <!--            </v-tab-item>-->
+          </v-tabs-items>
+        </div>
+        <div style="height: 32px" class="d-flex justify-space-between px-2">
+          <span>
+            <span class="mx-2"> 代码题</span>
+            <slot name="index"></slot>
+          </span>
+          <v-btn color="info" small @click="prevQuestion">上一题</v-btn>
+          <v-btn color="info" small @click="nextQuestion">下一题</v-btn>
+        </div>
+      </div>
+      <v-divider vertical class="mx-1"></v-divider>
+      <div style="width: 50vw; height: 100%" class="d-flex flex-column">
+        <div
+          style="height: 48px"
+          class="d-flex pr-5 justify-space-between elevation-1 mb-1"
+        >
+          <div class="d-flex ml-5 mt-1">
+            <div class="d-flex">
+              <v-select
+                :items="languages"
+                v-model="currentLang"
+                dense
+                style="width: 120px"
+                class="ma-0 pa-0"
+                solo
+              >
+              </v-select>
+            </div>
+            <v-tooltip top>
+              <template v-slot:activator="{ on, attrs }">
+                <v-btn
+                  fab
+                  x-small
+                  tile
+                  class="mx-2 mb-2 pa-0 elevation-1 align-self-center"
+                  v-bind="attrs"
+                  v-on="on"
+                  @click="resetLastCommit"
+                >
+                  <v-icon>mdi-arrow-left</v-icon>
+                </v-btn>
+              </template>
+              <span>恢复到上次提交的代码</span>
+            </v-tooltip>
+            <v-tooltip top>
+              <template v-slot:activator="{ on, attrs }">
+                <v-btn
+                  fab
+                  x-small
+                  tile
+                  class="mx-2 mb-2 pa-0 elevation-1 align-self-center"
+                  v-bind="attrs"
+                  v-on="on"
+                  @click="resetOriginCode"
+                >
+                  <v-icon>mdi-refresh</v-icon>
+                </v-btn>
+              </template>
+              <span>重置代码至初始状态</span>
+            </v-tooltip>
+          </div>
+          <v-btn
+            color="primary"
+            class="mx-2 mb-2 align-self-center"
+            small
+            v-on:click="submitCode"
+            >保存测试</v-btn
+          >
+        </div>
+        <code-editor
+          :language="currentLang"
+          v-model="currentCode"
+        ></code-editor>
+      </div>
+    </div>
   </v-card>
 </template>
 
@@ -162,7 +164,12 @@ import Vue, { PropType } from 'vue'
 import CodeEditor from '@/views/exam/components/CodeEditor.vue'
 import MarkdownText from '@/views/exam/components/MarkdownText.vue'
 import { clone } from 'ramda'
-import { CodeTuple } from '@/api/types'
+import {
+  CodeQuestionSerializer,
+  CodeSubmitSerializer,
+  CodeTuple
+} from '@/api/types'
+import { codeJudge } from '@/api/judge'
 
 interface CommitInfo {
   id?: string
@@ -174,120 +181,123 @@ interface CommitInfo {
 }
 
 export default Vue.extend({
-  name: 'CodePane'
-  // components: {
-  //   CodeEditor,
-  //   MarkdownText
-  // },
-  // props: {
-  //   question: {
-  //     required: true,
-  //     type: Object as PropType<CodeQuestionSerializer>
-  //   },
-  //   totalQuestionNum: {
-  //     required: true,
-  //     type: Number
-  //   }
-  // },
-  // data() {
-  //   return {
-  //     tab: 0,
-  //     commits: [] as CommitInfo[],
-  //     languages: ['cpp', 'java', 'python'],
-  //     currentLang: '',
-  //     currentCode: '',
-  //     codeCache: [] as CodeTuple[], // 切换语言时,将当前编辑的保存到cache中,下次切换回来可继续编辑
-  //     currentCommit: (null as unknown) as CommitInfo,
-  //     statusTags: [
-  //       '所有用例通过',
-  //       '有用例未通过',
-  //       '编译或运行失败',
-  //       '运行超时',
-  //       '等待运行'
-  //     ],
-  //     statusColors: ['success', 'warning', 'error', 'accent', 'info']
-  //   }
-  // },
-  // watch: {
-  //   currentLang(val, oldVal) {
-  //     if (oldVal !== '' && val !== oldVal) {
-  //       const newCode =
-  //         this.codeCache.find((value) => {
-  //           return value.lang === val
-  //         })?.code ?? ''
-  //
-  //       const oldCodeTuple =
-  //         this.codeCache.find((value) => {
-  //           return value.lang === oldVal
-  //         }) ?? {}
-  //
-  //       oldCodeTuple.code = this.currentCode
-  //       this.currentCode = newCode
-  //     }
-  //   }
-  // },
-  // created() {
-  //   this.languages =
-  //     this.question?.codeTuples?.map((value) => {
-  //       return value.lang ?? ''
-  //     }) ?? []
-  //   this.currentLang = this.languages[0]
-  //
-  //   this.currentCode =
-  //     this.question?.codeTuples?.find((value, index) => {
-  //       return index === 0
-  //     })?.code ?? ''
-  //
-  //   this.codeCache = clone(this.question.codeTuples) ?? []
-  //
-  //   for (let i = 0; i < 10; i++) {
-  //     const r = Math.floor(Math.random() * 4 + 1)
-  //     this.commits.push({
-  //       id: i.toString(),
-  //       submitTime: '2020-12-11 20:02',
-  //       status: this.statusTags[r],
-  //       score: 6 * i
-  //     })
-  //   }
-  //   this.currentCommit = this.getCurrentCommit()
-  // },
-  // methods: {
-  //   submitCode(): void {
-  //     this.tab = 1
-  //     this.$emit('submitCode', this.currentCode, this.currentLang)
-  //   },
-  //   getCurrentCommit(): CommitInfo {
-  //     return this.commits[0]
-  //   },
-  //   statusToColor(status: string): string {
-  //     return this.statusColors[this.statusTags.indexOf(status)]
-  //   },
-  //   resetLastCommit() {
-  //     const lastCommitCodes = this.question.lastCommitCodes
-  //     const lastCommitTuple = lastCommitCodes?.find((val) => {
-  //       return val.lang === this.currentLang
-  //     })
-  //
-  //     this.currentCode = lastCommitTuple?.code ?? ''
-  //   },
-  //   resetOriginCode() {
-  //     const originCodes = this.question.codeTuples ?? []
-  //     const originTuple =
-  //       originCodes.find((val) => {
-  //         return val.lang === this.currentLang
-  //       }) ?? {}
-  //     this.currentCode = originTuple.code ?? ''
-  //   },
-  //   nextQuestion() {
-  //     this.$emit('nextQuestion')
-  //   }
-  // }
+  name: 'CodePane',
+  components: {
+    CodeEditor,
+    MarkdownText
+  },
+  props: {
+    question: {
+      required: true,
+      type: Object as PropType<CodeQuestionSerializer>
+    },
+    totalQuestionNum: {
+      required: true,
+      type: Number
+    }
+  },
+  data() {
+    return {
+      tab: 0,
+      commits: [] as CommitInfo[],
+      languages: ['cpp', 'java', 'python'],
+      currentLang: '',
+      currentCode: '',
+      codeCache: [] as CodeTuple[], // 切换语言时,将当前编辑的保存到cache中,下次切换回来可继续编辑
+      // currentCommit: (null as unknown) as CommitInfo,
+      statusTags: [
+        '所有用例通过',
+        '有用例未通过',
+        '编译或运行失败',
+        '运行超时',
+        '等待运行'
+      ],
+      statusColors: ['success', 'warning', 'error', 'accent', 'info']
+    }
+  },
+  watch: {
+    currentLang(val, oldVal) {
+      if (oldVal !== '' && val !== oldVal) {
+        const newCode =
+          this.codeCache.find((value) => {
+            return value.lang === val
+          })?.code ?? ''
+
+        this.codeCache.forEach((val) => {
+          if (val.lang === oldVal) {
+            val.code = this.currentCode
+          }
+        })
+
+        this.currentCode = newCode
+      }
+    }
+  },
+  created() {
+    this.currentLang = this.languages[0]
+
+    this.currentCode = ''
+
+    this.codeCache = this.languages.map((val) => {
+      return { lang: val, code: '' }
+    })
+    // TODO this is test
+    for (let i = 0; i < 10; i++) {
+      const r = Math.floor(Math.random() * 4 + 1)
+      this.commits.push({
+        id: i.toString(),
+        submitTime: '2020-12-11 20:02',
+        status: this.statusTags[r],
+        score: 6 * i
+      })
+    }
+  },
+  methods: {
+    submitCode(): void {
+      this.tab = 1
+      const examId = this.$route.params.examId
+      const codeToSubmit: CodeSubmitSerializer = {
+        code: this.currentCode,
+        language: this.currentLang
+      }
+      codeJudge(examId, this.question.id, codeToSubmit).then(({ data }) => {
+        console.log(data.result)
+      })
+    },
+    // getCurrentCommit(): CommitInfo {
+    //   return this.commits[0]
+    // },
+    statusToColor(status: string): string {
+      return this.statusColors[this.statusTags.indexOf(status)]
+    },
+    resetLastCommit() {
+      // const lastCommitCodes = this.question.lastCommitCodes
+      // const lastCommitTuple = lastCommitCodes?.find((val) => {
+      //   return val.lang === this.currentLang
+      // })
+      //
+      // this.currentCode = lastCommitTuple?.code ?? ''
+    },
+    resetOriginCode() {
+      // const originCodes = this.question.codeTuples ?? []
+      // const originTuple =
+      //   originCodes.find((val) => {
+      //     return val.lang === this.currentLang
+      //   }) ?? {}
+      // this.currentCode = originTuple.code ?? ''
+    },
+    prevQuestion() {
+      this.$emit('prevQuestion')
+    },
+    nextQuestion() {
+      this.$emit('nextQuestion')
+    }
+  }
 })
 </script>
 
 <style scoped>
-.questionCard {
-  margin: 0.5vh;
-  height: 92vh;
+.code-question {
+  height: 98vh;
 }
 </style>

+ 1 - 2
src/views/exam/components/CountDown.vue

@@ -1,7 +1,6 @@
 <template>
   <span>
     <v-chip label> 离结束还有:{{ content }} </v-chip>
-    <v-snackbar v-model="endTimeChanged">评测已延长</v-snackbar>
   </span>
 </template>
 <script>
@@ -71,7 +70,7 @@ export default {
                       .format(TIME_FORMAT)
                       .substr(14, 2) + '分钟'
                   self.end = newEnd
-                  self.endTimeChanged = true
+                  self.$emit('endTimeChanged')
                 }
               })
               .catch()

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

@@ -2,8 +2,7 @@
   <v-container>
     <page-header title="评测列表" :breadcrumb="breadcrumb">
       <template #extra>
-        <v-row class=" my-0 py-0" no-gutters justify="end">
-          <v-spacer></v-spacer>
+        <v-row no-gutters justify="end">
           <v-btn
             color="secondary"
             fab
@@ -13,7 +12,6 @@
           >
             <v-icon dark>mdi-cached</v-icon>
           </v-btn>
-          <!--          </v-col>-->
         </v-row>
       </template>
     </page-header>

+ 1 - 2
src/views/student/StudentExamList.vue

@@ -2,8 +2,7 @@
   <v-container>
     <page-header :title="title" :breadcrumb="breadcrumb">
       <template #extra>
-        <v-row class=" my-0 py-0" no-gutters justify="end">
-          <v-spacer></v-spacer>
+        <v-row no-gutters justify="end">
           <v-col cols="3">
             <v-select
               :items="selectItems"

+ 10 - 15
src/views/teacher/EditExam.vue

@@ -2,15 +2,13 @@
   <v-container>
     <page-header title="评测设置" :breadcrumb="breadcrumb">
       <template #extra>
-        <v-row justify="end">
-          <v-col cols="2">
-            <router-link
-              to="/teacher/created-exams"
-              style="text-decoration: none"
-            >
-              <v-btn>返回</v-btn>
-            </router-link>
-          </v-col>
+        <v-row justify="end" no-gutters>
+          <router-link
+            to="/teacher/created-exams"
+            style="text-decoration: none"
+          >
+            <v-btn>返回</v-btn>
+          </router-link>
         </v-row>
       </template>
     </page-header>
@@ -95,7 +93,7 @@
 import Vue from 'vue'
 import {
   ExamSerializer,
-  ExtendedQuestionSerializer,
+  TeacherQuestionSerializer,
   ID,
   ReceivedData,
   SampleSerializer
@@ -121,7 +119,7 @@ export default Vue.extend({
       examInfo: (null as unknown) as ExamSerializer,
       tab: 0,
       examSamples: [] as Array<SampleSerializer>,
-      sampleQuestions: [] as Array<Array<ExtendedQuestionSerializer>>,
+      sampleQuestions: [] as Array<Array<TeacherQuestionSerializer>>,
       fetching: false,
       isSampleSet: false,
       success: false,
@@ -158,7 +156,7 @@ export default Vue.extend({
         this.fetching = true
 
         let questionsPromises: Promise<
-          AxiosResponse<ReceivedData<ExtendedQuestionSerializer, 0>>
+          AxiosResponse<ReceivedData<TeacherQuestionSerializer, 0>>
         >[] = []
         let questionsIds = Object.entries(
           this.examSamples[val].questionAndScore
@@ -209,9 +207,6 @@ export default Vue.extend({
 
     getSamplePaper().then(({ data }) => {
       this.examSamples = data.result
-      // let tmp = clone(data.result[0])
-      // tmp.id = 2
-      // this.examSamples.push(tmp)
       this.fetchQuestionInfo(0)
     })
   }

+ 7 - 9
src/views/teacher/ExamCreate.vue

@@ -2,15 +2,13 @@
   <v-container>
     <page-header title="创建评测" :breadcrumb="breadcrumb">
       <template #extra>
-        <v-row justify="end">
-          <v-col cols="2">
-            <router-link
-              to="/teacher/created-exams"
-              style="text-decoration: none"
-            >
-              <v-btn>返回</v-btn>
-            </router-link>
-          </v-col>
+        <v-row justify="end" no-gutters>
+          <router-link
+            to="/teacher/created-exams"
+            style="text-decoration: none"
+          >
+            <v-btn>返回</v-btn>
+          </router-link>
         </v-row>
       </template>
     </page-header>

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

@@ -2,11 +2,9 @@
   <v-container>
     <page-header title="评测管理" :breadcrumb="breadcrumb">
       <template #extra>
-        <v-row class=" my-0 py-0" no-gutters justify="end">
-          <v-spacer></v-spacer>
+        <v-row no-gutters justify="end">
           <v-col cols="3">
             <router-link :to="{ name: 'create' }" style="text-decoration: none">
-              <!--            <router-link :to="{ name: 'teacher-home' }">-->
               <v-btn color="info">
                 创建评测
               </v-btn>

+ 128 - 128
src/views/teacher/TeacherExamDetail.vue

@@ -2,142 +2,142 @@
   <v-container>
     <page-header title="评测详情" :breadcrumb="breadcrumb">
       <template #extra>
-        <v-row justify="end">
-          <v-col cols="2">
-            <router-link
-              to="/teacher/created-exams"
-              style="text-decoration: none"
-            >
-              <v-btn>返回</v-btn>
-            </router-link>
-          </v-col>
+        <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-expansion-panels :value="[0]" multiple>
-      <v-expansion-panel>
-        <v-expansion-panel-header>
-          <div class="text-center display-1">评测信息</div>
-        </v-expansion-panel-header>
-        <v-expansion-panel-content>
-          <div class="px-3 mb-5" v-if="examInfo">
-            <v-card-title>评测「{{ examInfo.examName }}」</v-card-title>
-            <v-card-text>
-              <v-row no-gutters align-content="start">
-                <v-col>
-                  <v-card-text class="pa-0 pb-3"
-                    >评测邀请码:{{
-                      examInfo.inviteCode ? examInfo.inviteCode : '无'
-                    }}</v-card-text
-                  >
-                </v-col>
-                <v-col>
-                  <v-card-text class="pa-0 pb-5"
-                    >当前难度值:{{ examInfo.difficulty }}</v-card-text
-                  >
-                </v-col>
-              </v-row>
-              <v-row no-gutters align-content="start">
-                <v-col>
-                  <v-card-text class="pa-0 pb-3"
-                    >开始时间:{{ examInfo.startTime }}</v-card-text
-                  >
-                </v-col>
-                <v-col>
-                  <v-card-text class="pa-0 pb-5"
-                    >结束时间:{{ examInfo.endTime }}</v-card-text
-                  >
-                </v-col>
-              </v-row>
-              <v-row no-gutters align-content="start">
-                <v-col>
-                  <v-card-text class="pa-0 pb-3"
-                    >考试时长:{{
-                      getExamLength(examInfo.startTime, examInfo.endTime)
-                    }}</v-card-text
-                  >
-                </v-col>
-              </v-row>
-              <v-row no-gutters align-content="start">
-                <v-col>
-                  <v-card-text class="pa-0 pb-3"
-                    >职位:{{ examInfo.position }}</v-card-text
-                  >
-                </v-col>
-                <v-col>
-                  <v-card-text class="pa-0 pb-5"
-                    >技能点:{{ examInfo.skills.join(' ') }}</v-card-text
-                  >
-                </v-col>
-              </v-row>
-              <v-row no-gutters align-content="start">
-                <v-col>
-                  <v-card-text class="pa-0 pb-3"
-                    >备注:{{
-                      examInfo.comment ? examInfo.comment : '无'
-                    }}</v-card-text
-                  >
-                </v-col>
-              </v-row>
-            </v-card-text>
-          </div>
-        </v-expansion-panel-content>
-      </v-expansion-panel>
-      <v-expansion-panel>
-        <v-expansion-panel-header>
-          <div class="text-center display-1">试卷题目</div>
-        </v-expansion-panel-header>
-        <v-expansion-panel-content>
-          <div v-if="!fetching">
-            <exam-questions-info
-              :exam-questions="examQuestions"
-            ></exam-questions-info>
-          </div>
-          <v-progress-circular
-            v-else
-            indeterminate
-            class="align-self-center"
-          ></v-progress-circular
-        ></v-expansion-panel-content>
-      </v-expansion-panel>
-      <v-expansion-panel>
-        <v-expansion-panel-header>
-          <div class="text-center display-1">评测名单</div>
-        </v-expansion-panel-header>
-        <v-expansion-panel-content>
-          <v-list-item-group>
-            <v-row class="px-4 align-center">
-              <v-col cols="1">
-                序号
-              </v-col>
-              <v-col> 用户名 </v-col>
-              <v-col> 邮箱 </v-col>
-              <v-col> 成绩 </v-col>
-            </v-row>
-            <v-list-item v-for="user in joinedUsers" :key="user.id">
-              <v-list-item-content>
-                <v-row class="align-center">
-                  <v-col cols="1">
-                    <v-chip label>
-                      {{ joinedUsers.indexOf(user) + 1 }}
-                    </v-chip>
+    <v-container>
+      <v-expansion-panels :value="[0]" multiple>
+        <v-expansion-panel>
+          <v-expansion-panel-header>
+            <div class="text-center display-1">评测信息</div>
+          </v-expansion-panel-header>
+          <v-expansion-panel-content>
+            <div class="px-3 mb-5" v-if="examInfo">
+              <v-card-title>评测「{{ examInfo.examName }}」</v-card-title>
+              <v-card-text>
+                <v-row no-gutters align-content="start">
+                  <v-col>
+                    <v-card-text class="pa-0 pb-3"
+                      >评测邀请码:{{
+                        examInfo.inviteCode ? examInfo.inviteCode : '无'
+                      }}</v-card-text
+                    >
                   </v-col>
                   <v-col>
-                    {{ user.name }}
+                    <v-card-text class="pa-0 pb-5"
+                      >当前难度值:{{ examInfo.difficulty }}</v-card-text
+                    >
                   </v-col>
+                </v-row>
+                <v-row no-gutters align-content="start">
+                  <v-col>
+                    <v-card-text class="pa-0 pb-3"
+                      >开始时间:{{ examInfo.startTime }}</v-card-text
+                    >
+                  </v-col>
+                  <v-col>
+                    <v-card-text class="pa-0 pb-5"
+                      >结束时间:{{ examInfo.endTime }}</v-card-text
+                    >
+                  </v-col>
+                </v-row>
+                <v-row no-gutters align-content="start">
+                  <v-col>
+                    <v-card-text class="pa-0 pb-3"
+                      >考试时长:{{
+                        getExamLength(examInfo.startTime, examInfo.endTime)
+                      }}</v-card-text
+                    >
+                  </v-col>
+                </v-row>
+                <v-row no-gutters align-content="start">
                   <v-col>
-                    {{ user.email }}
+                    <v-card-text class="pa-0 pb-3"
+                      >职位:{{ examInfo.position }}</v-card-text
+                    >
                   </v-col>
                   <v-col>
-                    无
+                    <v-card-text class="pa-0 pb-5"
+                      >技能点:{{ examInfo.skills.join(' ') }}</v-card-text
+                    >
                   </v-col>
                 </v-row>
-              </v-list-item-content>
-            </v-list-item>
-          </v-list-item-group>
-        </v-expansion-panel-content>
-      </v-expansion-panel>
-    </v-expansion-panels>
+                <v-row no-gutters align-content="start">
+                  <v-col>
+                    <v-card-text class="pa-0 pb-3"
+                      >备注:{{
+                        examInfo.comment ? examInfo.comment : '无'
+                      }}</v-card-text
+                    >
+                  </v-col>
+                </v-row>
+              </v-card-text>
+            </div>
+          </v-expansion-panel-content>
+        </v-expansion-panel>
+        <v-expansion-panel>
+          <v-expansion-panel-header>
+            <div class="text-center display-1">试卷题目</div>
+          </v-expansion-panel-header>
+          <v-expansion-panel-content>
+            <div v-if="!fetching">
+              <exam-questions-info
+                :exam-questions="examQuestions"
+              ></exam-questions-info>
+            </div>
+            <v-progress-circular
+              v-else
+              indeterminate
+              class="align-self-center"
+            ></v-progress-circular
+          ></v-expansion-panel-content>
+        </v-expansion-panel>
+        <v-expansion-panel>
+          <v-expansion-panel-header>
+            <div class="text-center display-1">评测名单</div>
+          </v-expansion-panel-header>
+          <v-expansion-panel-content>
+            <v-list-item-group>
+              <v-row class="px-4 align-center">
+                <v-col cols="1">
+                  序号
+                </v-col>
+                <v-col> 用户名 </v-col>
+                <v-col> 邮箱 </v-col>
+                <v-col> 成绩 </v-col>
+              </v-row>
+              <v-list-item v-for="user in joinedUsers" :key="user.id">
+                <v-list-item-content>
+                  <v-row class="align-center">
+                    <v-col cols="1">
+                      <v-chip label>
+                        {{ joinedUsers.indexOf(user) + 1 }}
+                      </v-chip>
+                    </v-col>
+                    <v-col>
+                      {{ user.name }}
+                    </v-col>
+                    <v-col>
+                      {{ user.email }}
+                    </v-col>
+                    <v-col>
+                      无
+                    </v-col>
+                  </v-row>
+                </v-list-item-content>
+              </v-list-item>
+            </v-list-item-group>
+          </v-expansion-panel-content>
+        </v-expansion-panel>
+      </v-expansion-panels>
+    </v-container>
   </v-container>
 </template>
 
@@ -147,7 +147,7 @@ import { getExamById, getExamQuestions } from '@/api/exam'
 import { AxiosResponse } from 'axios'
 import {
   ExamSerializer,
-  ExtendedQuestionSerializer,
+  TeacherQuestionSerializer,
   LocalDateTime,
   ReceivedData,
   UserSerializer
@@ -167,7 +167,7 @@ export default Vue.extend({
   data() {
     return {
       fetching: false,
-      examQuestions: [] as Array<ExtendedQuestionSerializer>,
+      examQuestions: [] as Array<TeacherQuestionSerializer>,
       joinedUsers: [] as Array<UserSerializer>,
       breadcrumb: [
         {
@@ -206,7 +206,7 @@ export default Vue.extend({
         this.fetching = true
 
         let questionsPromises: Promise<
-          AxiosResponse<ReceivedData<ExtendedQuestionSerializer, 0>>
+          AxiosResponse<ReceivedData<TeacherQuestionSerializer, 0>>
         >[] = []
         let questionsIds = Object.entries(data.result.questionAndScore)
         questionsIds.forEach((v, ind) => {

+ 3 - 4
src/views/teacher/components/QuestionList.vue

@@ -68,17 +68,16 @@
 
 <script lang="ts">
 import Vue, { PropType } from 'vue'
-import { QuestionStemSerializer, ID, Pageable } from '@/api/types'
-import { getExamQuestions } from '@/api/exam'
+import { QuestionSerializer, ID } from '@/api/types'
 
 export default Vue.extend({
   name: 'QuestionList',
   props: {},
   data() {
     return {
-      questions: [] as Array<QuestionStemSerializer>,
+      questions: [] as Array<QuestionSerializer>,
       selectedId: [] as Array<ID>,
-      selectedQuestions: [] as Array<QuestionStemSerializer>,
+      selectedQuestions: [] as Array<QuestionSerializer>,
       headers: [
         { text: '技能点', value: 'skillId' },
         { text: '知识点', value: 'knowledgeId' },

+ 1 - 1
vue.config.js

@@ -7,7 +7,7 @@ module.exports = {
     proxy: {
       '^/api': {
         // target: 'http://localhost:5000/',
-        target: 'http://101.37.175.237:8080/',
+        target: 'http://47.111.95.164:8080/',
         ws: true,
         changeOrigin: true
       }