1
0
ChengYuXuan 5 лет назад
Родитель
Сommit
09392fe2bc

+ 1 - 1
src/api/axios.config.ts

@@ -16,7 +16,7 @@ export default function axiosConfig() {
         return Promise.resolve(response)
       } else {
         console.log('reject')
-        if (responseData.code === 'A0223') {
+        if (responseData.code === 'A0223' || responseData.code === 'A0230') {
           jumpToLogin()
         }
         return Promise.reject(response)

+ 12 - 12
src/api/types.d.ts

@@ -17,13 +17,13 @@ export interface ErrorData extends AxiosResponseInfo {
   msg: string
 }
 
-export type PageSerializer<T = {}> = T & {
+export interface PageSerializer {
   totalNum: number
 }
 
 export type Pageable<T = {}> = T & {
-  pageNum?: number
-  pageSize?: number
+  pageNum: number
+  pageSize: number
 }
 //yyyy-MM-dd HH:mm:ss
 export type LocalDateTime = string
@@ -53,9 +53,8 @@ export interface CreateExamResponse {
   inviteCode: string
 }
 
-export interface ExamListSerializer {
+export type ExamListSerializer = PageSerializer & {
   examVOList: Array<ExamSerializer>
-  totalNum: number
 }
 
 export interface ResultSerializer {
@@ -111,15 +110,16 @@ export type ChoiceAnswer = string
 // }
 
 export interface CodeTuple {
-  lang?: string
-  code?: string
+  lang: string
+  code: string
 }
 
-// export interface CodeQuestionSerializer extends ExtendedQuestionSerializer {
-//   studentId?: number
-//   codeTuples?: Array<CodeTuple>
-//   lastCommitCodes?: Array<CodeTuple>
-// }
+export interface CodeQuestionSerializer {
+  id?: ID
+  stem: string
+  codeTuples?: Array<CodeTuple>
+  lastCommitCodes?: Array<CodeTuple>
+}
 
 export interface ExamQuestionSerializer {
   questionAndScore: QuestionAndScore

+ 6 - 2
src/mock/serializers.js

@@ -3,8 +3,10 @@ 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']),
-  END_TIME: () => randomArrayValue(['2021-03-01 00:00:00']),
+  END_TIME: () => randomArrayValue(['2021-04-01 00:00:00']),
   BUILD_STATUS: (status) =>
     status ||
     randomArrayValue([
@@ -46,7 +48,9 @@ const serializersPlugin = (mock) => ({
       'skills|1-3': ['@CWORD'],
       startTime: '@START_TIME',
       examName: '@CTITLE',
-      inviteCode: '@WORD(5)'
+      inviteCode: '@WORD(5)',
+      isPublic: '@BOOLEAN',
+      isJoined: '@BOOLEAN'
     }),
   EXAM_LIST_SERIALIZER: () =>
     mock({

+ 1 - 1
src/plugins/vuetify.ts

@@ -1,5 +1,5 @@
 import Vue from 'vue'
-import Vuetify from 'vuetify'
+import Vuetify from 'vuetify/lib' // TreeShaking
 import { vuetifyConfig } from '@/plugins/vuetify.config'
 Vue.use(Vuetify)
 

+ 7 - 13
src/views/Home.vue

@@ -1,17 +1,9 @@
 <template>
-  <v-app>
-    <!--    <router-link :to="{ name: 'student' }">-->
-    <!--      <v-btn color="primary">student</v-btn>-->
-    <!--    </router-link>-->
-    <!--    <router-link :to="{ name: 'teacher' }">-->
-    <!--      <v-btn color="primary">teacher</v-btn>-->
-    <!--    </router-link>-->
-  </v-app>
+  <v-app class="indigo lighten-5"> </v-app>
 </template>
 
 <script lang="ts">
 import Vue from 'vue'
-
 import { mapGetters, mapMutations } from 'vuex'
 import { getUserInfo } from '@/api/user'
 import { jumpToLogin } from '@/api/axios.config'
@@ -20,10 +12,12 @@ export default Vue.extend({
   data() {
     return {}
   },
-  methods: {
-    ...mapMutations(['setToken', 'setUser']),
+  computed: {
     ...mapGetters(['isStudent', 'isTeacher'])
   },
+  methods: {
+    ...mapMutations(['setToken', 'setUser'])
+  },
   // beforeRouteLeave(to, from, next) {
   //   const { id_token } = to.query
   //   if (id_token) {
@@ -36,12 +30,12 @@ export default Vue.extend({
     getUserInfo()
       .then(({ data }) => {
         this.setUser(data.result)
-        if (this.isStudent()) {
+        if (this.isStudent) {
           this.$router.push({
             name: 'student'
           })
         }
-        if (this.isTeacher()) {
+        if (this.isTeacher) {
           this.$router.push({
             name: 'teacher'
           })

+ 81 - 0
src/views/components/ExamQuestionsInfo.vue

@@ -0,0 +1,81 @@
+<template>
+  <div>
+    <div v-for="question in examQuestions" :key="question.id">
+      <v-divider class="my-5"></v-divider>
+      <v-row>
+        <v-col class="d-flex justify-center align-center" cols="1">
+          <v-chip label>
+            {{ examQuestions.indexOf(question) + 1 }}
+          </v-chip>
+        </v-col>
+        <v-col class="flex-grow-0 flex-shrink-0" cols="9">
+          <markdown-text
+            :raw="question.stem + mergeOptions(question.options)"
+            style="height: 100%;width: 100%"
+          ></markdown-text>
+        </v-col>
+        <v-col class="flex-shrink-1" cols="2">
+          <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">
+          <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-chip label color="success" class="ml-2">
+            {{ question.answer }}
+          </v-chip>
+        </v-col>
+      </v-row>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import Vue, { PropType } from 'vue'
+import { ExtendedQuestionSerializer, 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>>,
+      required: true
+    },
+    analysis: {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {
+    MarkdownText
+  },
+
+  data() {
+    return {}
+  },
+  methods: {
+    mergeOptions(options: OptionSerializer): string {
+      if (!options) {
+        return ''
+      }
+
+      let keys = Object.keys(options)
+      let values = Object.values(options)
+      let entries = []
+
+      for (let i = 0; i < keys.length; i++) {
+        entries.push(keys[i] + ' : ' + values[i])
+      }
+      return '\n' + entries.join(' ')
+    }
+  }
+})
+</script>
+
+<style scoped></style>

+ 0 - 0
src/components/PageHeader.vue → src/views/components/PageHeader.vue


+ 1 - 1
src/views/exam/BeforeExam.vue

@@ -41,7 +41,7 @@
 
 <script lang="ts">
 import Vue from 'vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
 import { ExamQuestionSerializer, ExamSerializer } from '@/api/types'
 import { getExamById, getExamQuestions } from '@/api/exam'
 import { TIME_FORMAT, timeLength } from '@/utils/common'

+ 15 - 15
src/views/exam/ExamPane.vue

@@ -165,20 +165,20 @@
           >
         </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-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>
         已到最后一题
@@ -202,7 +202,7 @@ import BlankPane from '@/views/exam/components/BlankPane.vue'
 import {
   ExamQuestionSerializer,
   ExamSerializer,
-  ID,
+  CodeQuestionSerializer,
   QuestionStemSerializer,
   QuestionType
 } from '@/api/types'

+ 1 - 7
src/views/exam/components/BlankPane.vue

@@ -52,10 +52,4 @@ export default Vue.extend({
 })
 </script>
 
-<style scoped>
-.questionCard {
-  margin: 5vh 10vw;
-  min-height: 90vh;
-  padding: 10px;
-}
-</style>
+<style scoped></style>

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

@@ -45,7 +45,7 @@
               <v-list-item-action>
                 <v-checkbox hide-details :input-value="active"> </v-checkbox>
               </v-list-item-action>
-              <v-list-item-content>
+              <v-list-item-content class="py-0">
                 <markdown-text :raw="choice.labelText"></markdown-text>
               </v-list-item-content>
             </template>

+ 1 - 1
src/views/student/Info.vue

@@ -20,7 +20,7 @@
 
 <script lang="ts">
 import Vue from 'vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
 
 export default Vue.extend({
   name: 'Info',

+ 13 - 37
src/views/student/Overview.vue

@@ -4,21 +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="onOpenInviteCodeDialog">-->
-            <!--              <v-icon>mdi-add</v-icon>-->
-            <!--              输入邀请码-->
-            <!--            </v-btn>-->
-          </v-col>
-          <!--          <v-col cols="3">-->
-          <!--            <v-select-->
-          <!--              :items="selectItems"-->
-          <!--              outlined-->
-          <!--              v-model="selected"-->
-          <!--              dense-->
-          <!--            ></v-select>-->
-          <!--          </v-col>-->
-          <!--          <v-col cols="1" class="ml-5">-->
           <v-btn
             color="secondary"
             fab
@@ -37,40 +22,27 @@
       v-for="item in examList"
       :key="item.examId"
       :exam-info="item"
-      @joinExam="fetchExams"
+      @joinExam="onJoinExam"
     >
     </student-exam-info-card>
     <v-pagination
       v-model="page"
-      :length="Math.floor(totalNum / pageSize) + 1"
+      :length="Math.ceil(totalNum / pageSize)"
       @input="changePage"
     >
     </v-pagination>
-
-    <v-dialog v-model="dialog" width="500">
-      <v-card>
-        <v-text-field
-          label="输入邀请码"
-          outlined
-          class="pa-5 darken-4"
-        ></v-text-field>
-      </v-card>
-      <v-btn color="primary" @click="onJoinExam">
-        确定
-      </v-btn>
-    </v-dialog>
-    <v-snackbar v-model="joinSuccess">
+    <v-snackbar top v-model="joinSuccess" color="success">
       加入评测成功
     </v-snackbar>
-    <v-snackbar v-model="joinFailed">
-      邀请码有误
+    <v-snackbar top v-model="joinFailed" color="error">
+      加入评测失败
     </v-snackbar>
   </v-container>
 </template>
 
 <script lang="ts">
 import Vue from 'vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
 import { getAllExamList } from '@/api/exam'
 import { ExamSerializer } from '@/api/types'
 import StudentExamInfoCard from '@/views/student/components/StudentExamInfoCard.vue'
@@ -116,9 +88,13 @@ export default Vue.extend({
     changePage() {
       this.fetchExams()
     },
-    onJoinExam() {}, // TODO 直接邀请码加入
-    onOpenInviteCodeDialog() {
-      this.dialog = true
+    onJoinExam(result: string) {
+      if (result === 'true') {
+        this.joinSuccess = true
+        this.fetchExams()
+      } else {
+        this.joinFailed = true
+      }
     },
     fetchExams() {
       this.fetching = true

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

@@ -42,7 +42,7 @@
     </student-exam-info-card>
     <v-pagination
       v-model="page"
-      :length="Math.floor(totalNum / pageSize) + 1"
+      :length="Math.ceil(totalNum / pageSize)"
       @input="changePage"
     >
     </v-pagination>
@@ -51,7 +51,7 @@
 
 <script lang="ts">
 import Vue from 'vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
 import { getExamsByStudentId } from '@/api/exam'
 import { ExamSerializer, ExamState } from '@/api/types'
 import StudentExamInfoCard from '@/views/student/components/StudentExamInfoCard.vue'

+ 8 - 25
src/views/student/components/StudentExamInfoCard.vue

@@ -78,11 +78,8 @@
       </v-col>
       <v-col>
         <v-card-text class="pa-0 pb-5"
-          >技能点:
-          <span v-for="skill in examInfo.skills" :key="skill"
-            >{{ skill }}
-          </span></v-card-text
-        >
+          >技能点: {{ examInfo.skills.join(' ') }}
+        </v-card-text>
       </v-col>
       <v-spacer></v-spacer>
     </v-row>
@@ -94,12 +91,6 @@
         </v-card-actions>
       </v-card>
     </v-dialog>
-    <v-snackbar top v-model="joinSuccess" timeout="2000" color="success">
-      加入评测成功
-    </v-snackbar>
-    <v-snackbar top v-model="joinFailed" timeout="2000" color="error">
-      加入评测失败,验证码错误
-    </v-snackbar>
   </v-card>
 </template>
 
@@ -115,9 +106,7 @@ export default Vue.extend({
     return {
       state: (null as unknown) as ExamStateText,
       inviteCode: '',
-      inputInviteCode: false,
-      joinSuccess: false,
-      joinFailed: false
+      inputInviteCode: false
     }
   },
   props: {
@@ -144,10 +133,9 @@ export default Vue.extend({
           const joinSuccess = data.result
           this.inputInviteCode = false
           if (joinSuccess) {
-            this.joinSuccess = true
-            this.$emit('joinExam')
+            this.$emit('joinExam', 'true')
           } else {
-            this.joinFailed = true
+            this.$emit('joinExam', 'false')
           }
         })
         .finally(() => {
@@ -161,10 +149,9 @@ export default Vue.extend({
         joinPublicExam(examId).then(({ data }) => {
           const joinSuccess = data.result
           if (joinSuccess) {
-            this.joinSuccess = true
-            this.$emit('joinExam')
+            this.$emit('joinExam', 'true')
           } else {
-            this.joinFailed = true
+            this.$emit('joinExam', 'false')
           }
         })
       } else {
@@ -208,8 +195,4 @@ export default Vue.extend({
 })
 </script>
 
-<style scoped>
-.router-link-active {
-  text-decoration: none;
-}
-</style>
+<style scoped></style>

+ 52 - 55
src/views/teacher/EditExam.vue

@@ -50,51 +50,44 @@
           </v-row>
         </v-card-text>
       </v-card>
-      <v-container class="grey lighten-5 pa-6 d-flex flex-column">
-        <div class="text-center display-1">样卷选择</div>
-        <v-tabs v-model="tab" v-if="examSamples[0]">
-          <v-tab v-for="i in examSamples.length" :key="i"> 样卷 {{ i }} </v-tab>
-        </v-tabs>
-        <v-tabs-items v-model="tab">
-          <v-tab-item v-for="sample in examSamples" :key="sample.id">
-            <div v-if="!fetching" style="min-height: 50vh">
-              <!--              <v-list-item-->
-              <!--                v-for="q in Object.entries(sample.questionAndScore)"-->
-              <!--                :key="q[0]"-->
-              <!--              >-->
-              <!--                题目ID: {{ q[0] }} 分值: {{ q[1] }}-->
-              <!--              </v-list-item>-->
-              <v-row
-                v-for="question in sampleQuestions[tab]"
-                :key="question.id"
-                class="d-flex"
-              >
-                <v-col class="flex-grow-0 flex-shrink-0" cols="10">
-                  <markdown-text
-                    :raw="question.stem"
-                    style="height: 100%;width: 100%"
-                  ></markdown-text>
-                </v-col>
-                <v-col class="flex-shrink-1">
-                  <v-card-text> 知识点: {{ question.keyPoints }} </v-card-text>
-                </v-col>
-              </v-row>
-            </div>
-            <v-row class="d-flex justify-center my-3">
-              <v-btn
-                class="primary align-self-center my-2 text-center"
-                @click="onSelectSample(sample.id)"
-              >
-                选择
-              </v-btn>
-            </v-row>
-          </v-tab-item>
-        </v-tabs-items>
-      </v-container>
+      <v-container> </v-container>
+      <v-toolbar flat
+        ><v-toolbar-title>
+          样卷选择
+        </v-toolbar-title>
+        <template v-slot:extension>
+          <v-tabs v-model="tab" v-if="examSamples[0]" align-with-title>
+            <v-tab v-for="i in examSamples.length" :key="i">
+              样卷 {{ i }}
+            </v-tab>
+          </v-tabs>
+        </template>
+      </v-toolbar>
+
+      <v-tabs-items v-model="tab">
+        <v-tab-item v-for="sample in examSamples" :key="sample.id">
+          <div v-if="!fetching" style="min-height: 50vh">
+            <exam-questions-info
+              :exam-questions="sampleQuestions[tab]"
+            ></exam-questions-info>
+          </div>
+          <v-row class="d-flex justify-center my-3">
+            <v-btn
+              class="primary align-self-center my-2 text-center"
+              @click="onSelectSample(sample.id)"
+            >
+              选择
+            </v-btn>
+          </v-row>
+        </v-tab-item>
+      </v-tabs-items>
     </v-container>
-    <v-snackbar v-model="success" absolute>
+    <v-snackbar v-model="success" color="success" top>
       设置样卷成功
     </v-snackbar>
+    <v-snackbar v-model="failed" color="error" top>
+      设置样卷失败
+    </v-snackbar>
   </v-container>
 </template>
 
@@ -114,14 +107,14 @@ import {
 } from '@/api/exam'
 import { getQuestionInfo, getSamplePaper } from '@/api/question'
 import { AxiosResponse } from 'axios'
-import MarkdownText from '@/views/exam/components/MarkdownText.vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
+import ExamQuestionsInfo from '@/views/components/ExamQuestionsInfo.vue'
 
 export default Vue.extend({
   name: 'EditExam',
   components: {
-    MarkdownText,
-    PageHeader
+    PageHeader,
+    ExamQuestionsInfo
   },
   data() {
     return {
@@ -132,6 +125,7 @@ export default Vue.extend({
       fetching: false,
       isSampleSet: false,
       success: false,
+      failed: false,
       breadcrumb: [
         {
           text: '主页',
@@ -183,15 +177,18 @@ export default Vue.extend({
       }
     },
     onSelectSample(sampleId: ID) {
-      setExamPaperFromSample(this.examInfo.examId ?? '', sampleId).then(() => {
-        this.success = true
-
-        setTimeout(() => {
-          this.$router.push({
-            name: 'teacher'
-          })
-        }, 1000)
-      })
+      setExamPaperFromSample(this.examInfo.examId ?? '', sampleId)
+        .then(({}) => {
+          this.success = true
+          setTimeout(() => {
+            this.$router.push({
+              name: 'teacher'
+            })
+          }, 1000)
+        })
+        .catch(() => {
+          this.failed = true
+        })
     }
   },
 

+ 3 - 3
src/views/teacher/ExamCreate.vue

@@ -172,7 +172,7 @@
 
 <script lang="ts">
 import Vue from 'vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
 import { createExam } from '@/api/exam'
 import { ExamSerializer, ID } from '@/api/types'
 import { mapGetters } from 'vuex'
@@ -256,6 +256,7 @@ export default Vue.extend({
     }
   },
   computed: {
+    ...mapGetters(['getId']),
     computedEndDate(): string {
       if (this.startTime === '') {
         return this.startDateFormatted
@@ -291,7 +292,6 @@ export default Vue.extend({
   },
 
   methods: {
-    ...mapGetters(['getId']),
     saveSelectedQuestions(questions: Array<ID>) {
       console.log(questions)
       this.selectedQuestions = questions
@@ -315,7 +315,7 @@ export default Vue.extend({
       console.log(this.valid)
 
       const obj = {
-        creatorId: this.getId(),
+        creatorId: this.getId,
         examName: this.examName,
         position: this.position,
         skills: this.skills,

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

@@ -45,7 +45,7 @@
     </teacher-exam-info-card>
     <v-pagination
       v-model="page"
-      :length="Math.floor(totalNum / pageSize) + 1"
+      :length="Math.ceil(totalNum / pageSize)"
       @input="changePage"
     >
     </v-pagination>
@@ -55,7 +55,7 @@
 
 <script lang="ts">
 import Vue from 'vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
 import TeacherExamInfoCard from '@/views/teacher/components/TeacherExamInfoCard.vue'
 import { ExamSerializer, ExamState } from '@/api/types'
 import { getExamsByTeacherId } from '@/api/exam'
@@ -91,8 +91,10 @@ export default Vue.extend({
       fetching: false
     }
   },
+  computed: {
+    ...mapGetters(['getId'])
+  },
   methods: {
-    ...mapGetters(['getId']),
     changePage() {
       this.fetchExams()
     },

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

@@ -6,7 +6,7 @@
 
 <script lang="ts">
 import Vue from 'vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
 
 export default Vue.extend({
   name: 'Overview',

+ 144 - 50
src/views/teacher/TeacherExamDetail.vue

@@ -1,6 +1,6 @@
 <template>
   <v-container>
-    <page-header title="评测信息" :breadcrumb="breadcrumb">
+    <page-header title="评测详情" :breadcrumb="breadcrumb">
       <template #extra>
         <v-row justify="end">
           <v-col cols="2">
@@ -14,71 +14,155 @@
         </v-row>
       </template>
     </page-header>
-    <v-container class="grey lighten-5 pa-6 d-flex flex-column">
-      <div class="text-center display-1">试卷</div>
-      <div v-if="!fetching">
-        <v-row
-          v-for="question in examQuestions"
-          :key="question.id"
-          class="d-flex"
-        >
-          <v-col class="flex-grow-0 flex-shrink-0" cols="10">
-            <markdown-text
-              :raw="question.stem"
-              style="height: 100%;width: 100%"
-            ></markdown-text>
-          </v-col>
-          <v-col class="flex-shrink-1">
-            <v-card-text> 知识点: {{ question.keyPoints }} </v-card-text>
-          </v-col>
-        </v-row>
-      </div>
-      <v-progress-circular
-        v-else
-        indeterminate
-        class="align-self-center"
-      ></v-progress-circular>
-    </v-container>
-    <v-container class="grey lighten-5 pa-6 d-flex flex-column mt-5">
-      <div class="text-center display-1">评测名单</div>
-      <!--      <v-list-item-group>-->
-      <!--        <v-list-item v-for="user in joinedUsers" :key="user.id">-->
-      <!--          <v-row justify="space-between">-->
-      <!--            <v-col>-->
-      <!--              {{ user.name }}-->
-      <!--            </v-col>-->
-      <!--            <v-col>-->
-      <!--              {{ user.email }}-->
-      <!--            </v-col>-->
-      <!--            <v-col>-->
-      <!--              {{ user.phone }}-->
-      <!--            </v-col>-->
-      <!--          </v-row>-->
-      <!--        </v-list-item>-->
-      <!--      </v-list-item-group>-->
-    </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>
+                  <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-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>
 </template>
 
 <script lang="ts">
 import Vue from 'vue'
-import { getExamQuestions } from '@/api/exam'
+import { getExamById, getExamQuestions } from '@/api/exam'
 import { AxiosResponse } from 'axios'
 import {
+  ExamSerializer,
   ExtendedQuestionSerializer,
+  LocalDateTime,
   ReceivedData,
   UserSerializer
 } from '@/api/types'
 import { getQuestionInfo } from '@/api/question'
-import MarkdownText from '@/views/exam/components/MarkdownText.vue'
-import PageHeader from '@/components/PageHeader.vue'
+import PageHeader from '@/views/components/PageHeader.vue'
+import ExamQuestionsInfo from '@/views/components/ExamQuestionsInfo.vue'
 import { getExamJoinedUsers } from '@/api/user'
+import { timeLength } from '@/utils/common'
 
 export default Vue.extend({
   name: 'TeacherExamDetail',
   components: {
-    MarkdownText,
-    PageHeader
+    PageHeader,
+    ExamQuestionsInfo
   },
   data() {
     return {
@@ -101,12 +185,22 @@ export default Vue.extend({
           disabled: true,
           href: '/'
         }
-      ]
+      ],
+      examInfo: (null as unknown) as ExamSerializer
+    }
+  },
+  methods: {
+    getExamLength(startTime: LocalDateTime, endTime: LocalDateTime): string {
+      return timeLength(startTime, endTime)
     }
   },
   mounted() {
     const examId = this.$route.params.examId
 
+    getExamById(examId).then(({ data }) => {
+      this.examInfo = data.result
+    })
+
     getExamQuestions(examId)
       .then(({ data }) => {
         this.fetching = true

+ 13 - 19
src/views/teacher/components/TeacherExamInfoCard.vue

@@ -44,34 +44,28 @@
         >
       </v-col>
     </v-row>
-    <v-row justify="space-between" dense>
+    <v-row justify="space-between" no-gutters>
       <v-col>
-        <v-card-text class="pa-0 pb-5"
-          >邀请码: {{ examInfo.inviteCode }}</v-card-text
-        >
-      </v-col>
-      <v-col>
-        <v-card-text class="pa-0 pb-5"
-          >评测时长:
-          {{ getExamLength(examInfo.startTime, examInfo.endTime) }}
-        </v-card-text>
-      </v-col>
-    </v-row>
-    <v-row no-gutters>
-      <v-col>
-        <v-card-text class="pa-0 pb-5"
+        <v-card-text class="pa-0 pb-4"
           >开始时间: {{ examInfo.startTime }}</v-card-text
         >
+        <v-card-text class="pa-0 pb-4"
+          >监考: {{ examInfo.antiCheating ? '是' : '否' }}
+        </v-card-text>
       </v-col>
       <v-col>
-        <v-card-text class="pa-0 pb-5"
+        <v-card-text class="pa-0 pb-4"
           >结束时间: {{ examInfo.endTime }}</v-card-text
         >
+        <v-card-text class="pa-0 pb-4" v-if="examInfo.inviteCode"
+          >邀请码: {{ examInfo.inviteCode }}</v-card-text
+        >
       </v-col>
       <v-col>
-        <v-card-text class="pa-0 pb-5" v-if="joinedUsers.length > 0"
-          >参加人数: {{ joinedUsers.length }}</v-card-text
-        >
+        <v-card-text class="pa-0 pb-4"
+          >评测时长:
+          {{ getExamLength(examInfo.startTime, examInfo.endTime) }}
+        </v-card-text>
       </v-col>
     </v-row>
     <v-dialog v-model="extendTimeDialog" max-width="500" attach="">