Ver código fonte

接口对接,完成客观题答题

ChengYuXuan 5 anos atrás
pai
commit
ce60cded4b
36 arquivos alterados com 800 adições e 521 exclusões
  1. 4 2
      src/api/axios.config.ts
  2. 14 13
      src/api/exam.ts
  3. 41 29
      src/api/types.d.ts
  4. 1 1
      src/mock/api/exam/_examId/get.json
  5. 3 3
      src/mock/api/exam/_examId/question/get.json
  6. 1 1
      src/mock/api/exam/_examId/time/put.json
  7. 1 1
      src/mock/api/exam/invite/code/put.json
  8. 2 3
      src/mock/api/exam/list/get.json
  9. 1 1
      src/mock/api/exam/post.json
  10. 1 1
      src/mock/api/exam/question/sample/_examId/post.json
  11. 2 2
      src/mock/api/exam/student/exams/get.json
  12. 2 2
      src/mock/api/exam/teacher/exams/get.json
  13. 1 1
      src/mock/api/judge/objective/post.json
  14. 1 1
      src/mock/api/question/_id/get.json
  15. 1 1
      src/mock/api/question/_id/stem/get.json
  16. 1 1
      src/mock/api/question/sample/get.json
  17. 3 1
      src/mock/api/user/_examId/get.json
  18. 1 1
      src/mock/api/user/get.json
  19. 28 8
      src/mock/serializers.js
  20. 1 0
      src/router/student.ts
  21. 3 2
      src/store/index.ts
  22. 23 0
      src/utils/common.ts
  23. 40 15
      src/views/exam/ExamDetails.vue
  24. 196 256
      src/views/exam/ExamPane.vue
  25. 61 0
      src/views/exam/components/BlankPane.vue
  26. 83 77
      src/views/exam/components/ChoicePane.vue
  27. 5 2
      src/views/exam/components/CodePane.vue
  28. 121 7
      src/views/student/Overview.vue
  29. 36 20
      src/views/student/StudentExamList.vue
  30. 26 6
      src/views/student/StudentHome.vue
  31. 54 28
      src/views/student/components/StudentExamInfoCard.vue
  32. 13 9
      src/views/teacher/ExamCreate.vue
  33. 19 13
      src/views/teacher/ExamManage.vue
  34. 5 9
      src/views/teacher/components/QuestionList.vue
  35. 4 4
      src/views/teacher/components/TeacherExamInfoCard.vue
  36. 1 0
      vue.config.js

+ 4 - 2
src/api/axios.config.ts

@@ -12,9 +12,10 @@ export default function axiosConfig() {
 
       const responseData = response.data
 
-      if (responseData.code === 0) {
+      if (responseData.code === '00000') {
         return Promise.resolve(response)
       } else {
+        console.log('reject')
         return Promise.reject(response)
       }
     },
@@ -57,7 +58,8 @@ export default function axiosConfig() {
   )
 }
 
-const portal = process.env.portalUrl
+// const portal = process.env.portalUrl
+const portal = 'http://p.seecoder.cn'
 
 export const getLoginUrl = () => {
   const location = window.location

+ 14 - 13
src/api/exam.ts

@@ -4,21 +4,22 @@ import {
   ExamSerializer,
   ReceivedData,
   Pageable,
-  ExamStatus,
   ID,
   ExamQuestionSerializer,
-  LocalDateTime
+  LocalDateTime,
+  ExamState,
+  PageSerializer,
+  ExamListSerializer
 } from '@/api/types'
 
 export const createExam = (exam: ExamSerializer) => {
   return axios.post<ReceivedData>(`${EXAM_MODULE}`, exam)
 }
 
-// TODO pageable URL
-export const getAllExamList = (params?: Pageable) => {
-  return axios.get<ReceivedData<Array<ExamSerializer>>>(`${EXAM_MODULE}/list`, {
-    params
-  })
+export const getAllExamList = (page: Pageable) => {
+  return axios.get<ReceivedData<ExamListSerializer>>(
+    `${EXAM_MODULE}/list?pageSize=${page.pageSize}&pageNum=${page.pageNum}`
+  )
 }
 
 export const getExamById = (examId: ID) => {
@@ -49,14 +50,14 @@ export const setExamPaperFromSample = (examId: ID, sampleExamPaperId: ID) => {
   )
 }
 
-export const getExamsByStudentId = (page: Pageable) => {
-  return axios.get<ReceivedData<Array<ExamSerializer>>>(
-    `${EXAM_MODULE}/student/exams?pageSize=${page.pageSize}&pageNum=${page.pageNum}`
+export const getExamsByStudentId = (page: Pageable, state: ExamState) => {
+  return axios.get<ReceivedData<ExamListSerializer>>(
+    `${EXAM_MODULE}/student/exams?pageSize=${page.pageSize}&pageNum=${page.pageNum}&state=${state}`
   )
 }
 
-export const getExamsByTeacherId = (page: Pageable) => {
-  return axios.get<ReceivedData<Array<ExamSerializer>>>(
-    `${EXAM_MODULE}/teacher/exams?pageSize=${page.pageSize}&pageNum=${page.pageNum}`
+export const getExamsByTeacherId = (page: Pageable, state: ExamState) => {
+  return axios.get<ReceivedData<ExamListSerializer>>(
+    `${EXAM_MODULE}/teacher/exams?pageSize=${page.pageSize}&pageNum=${page.pageNum}&state=${state}`
   )
 }

+ 41 - 29
src/api/types.d.ts

@@ -1,6 +1,9 @@
 export type ID = number | string
 
 export interface AxiosResponseInfo {}
+// 0 1 2 3
+export type ExamStateText = '所有评测' | '未开始' | '进行中' | '已结束'
+export type ExamState = 0 | 1 | 2 | 3
 
 export interface ReceivedData<R = {}, E = 0> extends AxiosResponseInfo {
   code: E | 0 | 401
@@ -15,9 +18,8 @@ export interface ErrorData extends AxiosResponseInfo {
   msg: string
 }
 
-export interface PageSerializer {
-  pageNum?: number
-  pageSize?: number
+export type PageSerializer<T = {}> = T & {
+  totalNum: number
 }
 
 export type Pageable<T = {}> = T & {
@@ -30,18 +32,24 @@ export type LocalDateTime = string
 export type UserRole = 'STUDENT' | 'TEACHER'
 
 export interface ExamSerializer {
-  antiCheating?: boolean
-  comment?: string
-  creatorId?: number
-  difficulty?: number
-  endTime?: LocalDateTime
-  examId?: number
-  position?: string
-  skills?: Array<string>
-  startTime?: LocalDateTime
-  examName?: string
+  antiCheating: boolean
+  comment: string
+  creatorId: ID
+  difficulty: number
+  endTime: LocalDateTime
+  position: string
+  skills: Array<string>
+  startTime: LocalDateTime
+  examName: string
   // 上边是创建考试所需的信息
+  examId?: number
   inviteCode?: string
+  joined?: boolean
+}
+
+export interface ExamListSerializer {
+  examVOList: Array<ExamSerializer>
+  totalNum: number
 }
 
 export interface ResultSerializer {
@@ -66,14 +74,19 @@ export interface ResultSerializer {
 
 export type OptionSerializer = Map<string, string>
 
+export interface OptionLabelTextPair {
+  label: string
+  text: string
+}
+
 export interface QuestionStemSerializer {
-  id?: number
-  stem?: string
-  type?: QuestionType
-  options?: OptionSerializer
-  keyPoints?: string
-  tags?: Array<string>
-  knowledgeId?: Array<string>
+  id: number
+  stem: string
+  type: QuestionType
+  options: OptionSerializer
+  keyPoints: string
+  tags: Array<string>
+  knowledgeId: Array<string>
 }
 
 export interface ExtendedQuestionSerializer extends QuestionStemSerializer {
@@ -102,19 +115,18 @@ export interface CodeTuple {
 // }
 
 export interface ExamQuestionSerializer {
-  id?: number
-  name?: string
-  questionAndScore?: QuestionAndScore
+  id: number
+  name: string
+  questionAndScore: QuestionAndScore
   // codeQuestions?: Array<CodeQuestionSerializer>
 }
 
 export interface UserSerializer {
-  id?: number
-  username?: string
-  email?: string
-  phone?: string
-  role?: UserRole
-  createdAt?: LocalDateTime
+  id: number
+  name: string
+  email: string
+  phone: string
+  role: UserRole
 }
 
 export type QuestionAndScore = Map<string, number>

+ 1 - 1
src/mock/api/exam/_examId/get.json

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

+ 3 - 3
src/mock/api/exam/_examId/question/get.json

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

+ 1 - 1
src/mock/api/exam/_examId/time/put.json

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

+ 1 - 1
src/mock/api/exam/invite/code/put.json

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

+ 2 - 3
src/mock/api/exam/list/get.json

@@ -1,6 +1,5 @@
 {
-  "code": 0,
-  "result|5-10": ["@EXAM_SERIALIZER"],
-  "page": "@PAGE_SERIALIZER",
+  "code": "00000",
+  "result": "@EXAM_LIST_SERIALIZER",
   "msg": ""
 }

+ 1 - 1
src/mock/api/exam/post.json

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

+ 1 - 1
src/mock/api/exam/question/sample/_examId/post.json

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

+ 2 - 2
src/mock/api/exam/student/exams/get.json

@@ -1,5 +1,5 @@
 {
-  "code": "0",
-  "result|1-10": ["@EXAM_SERIALIZER"],
+  "code": "00000",
+  "result": "@EXAM_LIST_SERIALIZER",
   "msg": ""
 }

+ 2 - 2
src/mock/api/exam/teacher/exams/get.json

@@ -1,5 +1,5 @@
 {
-  "code": "0",
-  "result|1-10": ["@EXAM_SERIALIZER"],
+  "code": "00000",
+  "result": "@EXAM_LIST_SERIALIZER",
   "msg": ""
 }

+ 1 - 1
src/mock/api/judge/objective/post.json

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

+ 1 - 1
src/mock/api/question/_id/get.json

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

+ 1 - 1
src/mock/api/question/_id/stem/get.json

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

+ 1 - 1
src/mock/api/question/sample/get.json

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

+ 3 - 1
src/mock/api/user/_examId/get.json

@@ -1,3 +1,5 @@
 {
-  "students|10-50": ["@USER_SERIALIZER"]
+  "code": "00000",
+  "result": "@EXAM_USERS_SERIALIZER",
+  "msg": "00000"
 }

+ 1 - 1
src/mock/api/user/get.json

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

+ 28 - 8
src/mock/serializers.js

@@ -18,15 +18,16 @@ const basicExtendsTypes = () => ({
   EXAM_STATUS: (examStatus) =>
     // examStatus || randomArrayValue(['READY', 'AVAILABLE', 'FINISHED', 'CLOSED'])
     examStatus || randomArrayValue(['READY', 'AVAILABLE', 'FINISHED']),
-  QUESTION_TYPE: () =>
-    randomArrayValue(['choice', 'multi_choice', 'blank', 'code'])
+  OBJECTIVE_QUESTION_TYPE: () =>
+    // randomArrayValue(['choice', 'multi_choice', 'blank', 'code']) TODO code
+    randomArrayValue(['choice', 'multi_choice', 'blank'])
 })
 
 const serializersPlugin = (mock) => ({
   USER_SERIALIZER: () =>
     mock({
       'id|1-1000': 1,
-      username: '@CWORD',
+      name: '@CNAME',
       email: '@EMAIL',
       'phone|10000000000-20000000000': 1,
       role: '@USER_ROLE'
@@ -45,6 +46,11 @@ const serializersPlugin = (mock) => ({
       examName: '@CTITLE',
       inviteCode: '@WORD(5)'
     }),
+  EXAM_LIST_SERIALIZER: () =>
+    mock({
+      'examVOList|5': ['@EXAM_SERIALIZER'],
+      'totalNum|10-30': 1
+    }),
   PAGE_SERIALIZER: () => ({
     pageNum: 1,
     pageSize: 20
@@ -143,16 +149,26 @@ const serializersPlugin = (mock) => ({
     return {
       '19030': 2,
       '19031': 3,
-      '19033': 4,
-      '19035': 3,
-      '19037': 2
+      '19032': 4,
+      '19033': 3,
+      '19034': 2,
+      '19035': 2,
+      '19036': 3,
+      '19037': 4,
+      '19038': 3,
+      '19039': 2,
+      '19040': 2,
+      '19041': 3,
+      '19043': 4,
+      '19045': 3,
+      '19047': 2
     }
   },
   QUESTION_STEM_SERIALIZER: () =>
     mock({
       'id|1-1000': 1,
       stem: '@CPARAGRAPH',
-      type: '@QUESTION_TYPE',
+      type: '@OBJECTIVE_QUESTION_TYPE',
       options: '@OPTION_SERIALIZER',
       keyPoints: '@CWORD',
       'tags|1-3': ['@CWORD'],
@@ -169,7 +185,7 @@ const serializersPlugin = (mock) => ({
     mock({
       'id|1-1000': 1,
       stem: '@CPARAGRAPH',
-      type: '@QUESTION_TYPE',
+      type: '@OBJECTIVE_QUESTION_TYPE',
       options: '@OPTION_SERIALIZER',
       keyPoints: '@CWORD',
       'tags|1-3': ['@CWORD'],
@@ -177,6 +193,10 @@ const serializersPlugin = (mock) => ({
       answer: 'A',
       analysis: '@CPARAGRAPH',
       lastModifiedTime: '@DATETIME'
+    }),
+  EXAM_USERS_SERIALIZER: () =>
+    mock({
+      'users|10-100': ['@USER_SERIALIZER']
     })
 })
 

+ 1 - 0
src/router/student.ts

@@ -9,6 +9,7 @@ const routes: Array<RouteConfig> = [
       import(
         /* webpackChunkName: "student" */ '@/views/student/StudentHome.vue'
       ),
+    redirect: '/student/home',
     children: [
       {
         path: 'home',

+ 3 - 2
src/store/index.ts

@@ -11,8 +11,9 @@ export type RootState = {
 }
 
 export const state = (): RootState => ({
-  user: {},
-  token: ''
+  user: (null as unknown) as UserSerializer,
+  token:
+    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwLnNlZWNvZGVyLmNuIiwic3ViIjoiMTU5OTYyMzEzMTMiLCJhdWQiOiJzZWVjLXBvcnRhbCIsImF1dGhfdGltZSI6MTYxMTkyNTY0MywiaWF0IjoxNjExOTI1NjQzLCJleHAiOjE2MTIwMTIwNDMsInVzZXJfaW5mbyI6eyJwaG9uZSI6IjE1OTk2MjMxMzEzIiwiZW1haWwiOiIxNjExNTAwMDlAc21haWwubmp1LmVkdS5jbiIsImlkIjozMzIsIm5hbWUiOiIxNjExNTAwMDkiLCJyb2xlIjoiU1RVREVOVCJ9fQ.f7wA_0JWmbDW-B4xSWuPyptSrMS1bf-7xvJmdb5hRNQ'
 })
 
 export const mutations: MutationTree<RootState> = {

+ 23 - 0
src/utils/common.ts

@@ -0,0 +1,23 @@
+import { LocalDateTime } from '@/api/types'
+
+export const timeLength = function(
+  start: LocalDateTime,
+  end: LocalDateTime
+): string {
+  const s = new Date(start).getTime()
+  const e = new Date(end).getTime()
+  const t = e - s
+
+  let hour = Math.floor(t / 3600000)
+  let min = Math.floor((t - hour * 3600000) / 60000)
+
+  let format = ''
+  if (hour > 0) {
+    format = `${hour}小时${min}分`
+  }
+  if (hour <= 0) {
+    format = `${min}分`
+  }
+
+  return format
+}

+ 40 - 15
src/views/exam/ExamDetails.vue

@@ -5,7 +5,11 @@
         <v-row justify="end">
           <v-spacer></v-spacer>
           <v-col cols="3">
-            <v-btn color="info" @click="onJoinExam" :loading="joining"
+            <v-btn
+              v-if="!joined"
+              color="info"
+              @click="onJoinExam"
+              :loading="joining"
               >报名参加</v-btn
             >
           </v-col>
@@ -25,22 +29,20 @@
         </v-row>
       </template>
     </page-header>
-    <v-card class="mb-5" hover style="cursor: default">
+    <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 }} 考试时长:{{
-          examInfo.lastTime
-        }}
-        小时</v-card-text
-      >
+        {{ 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">
-        单选题 道 多选题 道 编程题 道
+        客观题 {{ Object.keys(examQuestions.questionAndScore).length }} 道
+        编程题 {{}} 道
       </v-card-text>
     </v-card>
   </v-container>
@@ -49,8 +51,9 @@
 <script lang="ts">
 import Vue from 'vue'
 import PageHeader from '@/components/PageHeader.vue'
-import { ExamSerializer } from '@/api/types'
-import { getExamById } from '@/api/exam'
+import { ExamQuestionSerializer, ExamSerializer } from '@/api/types'
+import { getExamById, getExamQuestions } from '@/api/exam'
+import { timeLength } from '@/utils/common'
 
 export default Vue.extend({
   name: 'ExamDetails',
@@ -77,14 +80,36 @@ export default Vue.extend({
           href: '/'
         }
       ],
-      examInfo: {} as ExamSerializer,
-      joining: false
+      examInfo: (null as unknown) as ExamSerializer,
+      joining: false,
+      joined: false,
+      examLength: '',
+      examQuestions: (null as unknown) as ExamQuestionSerializer
     }
   },
   mounted() {
-    getExamById(this.$route.params.id).then(({ data }) => {
-      this.examInfo = data.res ?? {}
-    })
+    this.joined = this.$route.params.joined === 'true'
+
+    const examId = this.$route.params.id
+
+    getExamQuestions(examId)
+      .then(({ data }) => {
+        this.examQuestions = data.result
+      })
+      .catch((err) => {
+        console.log(err)
+      })
+
+    getExamById(examId)
+      .then(({ data }) => {
+        this.examInfo = data.result
+
+        this.examLength = timeLength(
+          this.examInfo.startTime,
+          this.examInfo.endTime
+        )
+      })
+      .catch((err) => {})
   },
   methods: {
     onJoinExam() {

+ 196 - 256
src/views/exam/ExamPane.vue

@@ -1,87 +1,66 @@
 <template>
   <div>
-    <v-app-bar dense app>
-      <v-row>
-        <v-col>
-          <span class="display-1">{{ examInfo.title }}</span>
-        </v-col>
-        <v-col class="d-inline-flex justify-end">
-          <count-down
-            class="align-self-center"
-            :current-time="currentTime"
-            :end-time="endTime"
-            @timesUp="onTimesUp"
-          ></count-down>
-        </v-col>
-      </v-row>
-    </v-app-bar>
-    <v-navigation-drawer app permanent right class="elevation-5">
-      <div class="flex-column d-flex" style="height: 100vh">
+    <v-navigation-drawer
+      app
+      permanent
+      right
+      class="elevation-5"
+      v-if="examInfo"
+    >
+      <div
+        class="flex-column d-flex justify-space-between"
+        style="height: 100vh"
+      >
         <div class="flex-shrink-0">
           <div class="pa-4">
             <div
               style="height: 40px; width: 100%"
               class="indigo lighten-3 rounded-lg justify-center d-flex "
             >
-              <h1 class="align-self-center">答题卡</h1>
+              <h2 class="align-self-center">{{ examInfo.examName }}</h2>
             </div>
           </div>
         </div>
+        <div
+          v-if="examInfo"
+          class="flex-shrink-0 flex-column d-flex justify-space-between align-center"
+        >
+          <count-down
+            :current-time="currentTime"
+            :end-time="endTime"
+            @timesUp="onTimesUp"
+          ></count-down>
+        </div>
+
         <div
           class="overflow-auto flex-grow-1 pa-4 d-flex flex-column justify-space-around"
           style="box-sizing: border-box;"
         >
           <div class="justify-space-between">
             <div class="d-flex justify-center">
-              <h2>单选题</h2>
+              <h2>客观题</h2>
             </div>
-            <v-btn
-              v-for="i in answerStatus.slice(0, singleChoiceNum)"
-              :key="i.index"
-              class="ml-2 mt-2 pa-0 d-inline"
-              x-small
-              :disabled="currentQuestionIndex + 1 !== i.index && i.answered"
-              :color="currentQuestionIndex + 1 === i.index ? 'secondary' : ''"
-              @click="onSelectQuestion(i.index)"
-            >
-              {{ i.index }}
-            </v-btn>
-          </div>
-          <div class="justify-space-between d-inline">
-            <div class="d-flex justify-center">
-              <h2>多选题</h2>
+            <div class="d-flex justify-space-around">
+              <v-chip label>
+                {{ currentQuestionIndex + 1 }} / {{ totalQuestionNum }}</v-chip
+              >
             </div>
-            <v-btn
-              v-for="i in answerStatus.slice(
-                singleChoiceNum,
-                choiceQuestionNum
-              )"
-              :key="i.index"
-              class="ml-2 mt-2 pa-0 d-inline"
-              x-small
-              :disabled="currentQuestionIndex + 1 !== i.index && i.answered"
-              :color="currentQuestionIndex + 1 === i.index ? 'secondary' : ''"
-              @click="onSelectQuestion(i.index)"
-            >
-              {{ i.index }}
-            </v-btn>
+            <v-progress-linear
+              :value="(currentQuestionIndex / totalQuestionNum) * 100"
+              :buffer-value="
+                ((currentQuestionIndex + 1) / totalQuestionNum) * 100
+              "
+            ></v-progress-linear>
           </div>
           <div class="justify-space-between">
             <div class="d-flex justify-center">
               <h2>代码题</h2>
             </div>
-            <v-btn
-              v-for="i in answerStatus.slice(choiceQuestionNum)"
-              :key="i.index"
-              class="ml-2 mt-2 pa-0 d-inline"
-              x-small
-              :color="currentQuestionIndex + 1 === i.index ? 'secondary' : ''"
-              @click="onSelectQuestion(i.index)"
-            >
-              {{ i.index }}
-            </v-btn>
           </div>
         </div>
+        <div style="height: 20vh;width: 80%" class="align-self-center">
+          <!--          预留给反作弊-->
+        </div>
         <div class="flex-shrink-0">
           <div class="pa-2 px-6 text--secondary justify-space-between d-flex">
             <v-dialog v-model="exitDialog" persistent max-width="290">
@@ -138,32 +117,68 @@
       </div>
     </v-navigation-drawer>
     <v-main class="indigo lighten-5 pa-0">
-      <v-container fluid class="pa-2" v-if="currentQuestionIndex >= 0">
-        <choice-pane
-          v-show="isChoiceProblem"
-          @nextQuestion="nextQuestion"
-          @saveChoiceAnswer="saveChoiceAnswer"
-          :total-question-num="curTypeQuestionNum"
-          :question="curChoiceQuestion"
-        ></choice-pane>
+      <v-container fluid class="pa-2">
+        <v-card class="questionCard">
+          <div v-if="!fetchingStem">
+            <blank-pane
+              v-if="questionType() === 'blank'"
+              :question="currentQuestion"
+              @saveAnswer="saveObjectiveAnswer"
+            >
+              <template #index>
+                <v-chip class="align-self-center"
+                  >{{ currentQuestionIndex + 1 }} /
+                  {{ totalQuestionNum }}</v-chip
+                >
+              </template>
+            </blank-pane>
+            <choice-pane
+              v-else-if="
+                questionType() === 'choice' || questionType() === 'multi_choice'
+              "
+              :question="currentQuestion"
+              @saveAnswer="saveObjectiveAnswer"
+            >
+              <template #index>
+                <v-chip class="align-self-center"
+                  >{{ currentQuestionIndex + 1 }} /
+                  {{ totalQuestionNum }}</v-chip
+                >
+              </template>
+            </choice-pane>
+          </div>
+
+          <div
+            v-if="!fetchingStem"
+            class="d-flex justify-space-around px-10 py-10"
+          >
+            <v-btn color="info" @click="nextQuestion">下一题</v-btn>
+          </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-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="noticeUnfinished">
-        还有题目未作答
+      <v-snackbar v-model="finished" absolute>
+        已到最后一题
       </v-snackbar>
     </v-main>
   </div>
@@ -174,232 +189,148 @@ import Vue from 'vue'
 import CodePane from '@/views/exam/components/CodePane.vue'
 import ChoicePane from '@/views/exam/components/ChoicePane.vue'
 import CountDown from '@/views/exam/components/CountDown.vue'
+import BlankPane from '@/views/exam/components/BlankPane.vue'
 import {
-  ChoiceQuestionSerializer,
-  CodeQuestionSerializer,
   ExamQuestionSerializer,
   ExamSerializer,
-  QuestionSerializer
+  ID,
+  QuestionStemSerializer,
+  QuestionType
 } from '@/api/types'
 import { getExamById, getExamQuestions } from '@/api/exam'
+import { getQuestionStem } from '@/api/question'
+import { objectiveJudge } from '@/api/judge'
 
 export default Vue.extend({
   name: 'ExamPane',
   components: {
     CountDown,
     CodePane,
-    ChoicePane
+    ChoicePane,
+    BlankPane
   },
   data() {
     return {
+      fetchingStem: true,
       exitDialog: false,
       submitDialog: false,
       submitted: false,
       currentTime: 0,
-      examInfo: {} as ExamSerializer,
+      examInfo: (null as unknown) as ExamSerializer,
       endTime: 0,
-      choiceAnswers: [] as Array<{ index: number; answer: string }>,
-      examId: 0,
-      singleChoiceQuestions: [] as Array<ChoiceQuestionSerializer>,
-      multiChoiceQuestions: [] as Array<ChoiceQuestionSerializer>,
-      codeQuestions: [] as Array<CodeQuestionSerializer>,
-      answeredChoices: [] as Array<{ index: number; answered: boolean }>,
-      answeredCodes: [] as Array<{ index: number; answered: boolean }>,
-      answerStatus: [] as Array<{ index: number; answered: boolean }>,
+      examQuestions: (null as unknown) as ExamQuestionSerializer,
+
+      currentQuestion: (null as unknown) as QuestionStemSerializer,
       currentQuestionIndex: -1,
-      codeIndex: -1,
-      noticeUnfinished: false
+      objectiveAnswers: [] as Array<string>,
+      questionIds: [] as Array<string>,
+
+      examId: 0,
+      // codeQuestions: [] as Array<CodeQuestionSerializer>,
+      // codeIndex: -1
+
+      submitting: false,
+      finished: false
     }
   },
   computed: {
-    curTypeQuestionNum(): number {
-      return this.isChoiceProblem
-        ? this.isSingleChoice
-          ? this.singleChoiceNum
-          : this.multiChoiceNum
-        : this.codeQuestionNum
-    },
-    isChoiceProblem(): boolean {
-      return this.currentQuestionIndex < this.choiceQuestionNum
-    },
-    isSingleChoice(): boolean {
-      return this.currentQuestionIndex < this.singleChoiceNum
-    },
-    singleChoiceNum(): number {
-      return this.singleChoiceQuestions.length
-    },
-    multiChoiceNum(): number {
-      return this.multiChoiceQuestions.length
+    isCodeQuestion(): boolean {
+      return false
     },
-    codeQuestionNum(): number {
-      return this.codeQuestions.length
-    },
-    choiceQuestionNum(): number {
-      return this.singleChoiceNum + this.multiChoiceNum
-    },
-    totalQuestionNum(): number {
-      return this.choiceQuestionNum + this.codeQuestionNum
-    },
-    curChoiceQuestion(): ChoiceQuestionSerializer {
-      if (!this.isChoiceProblem) {
-        return {} as ChoiceQuestionSerializer
-      }
-      if (this.isSingleChoice) {
-        return this.singleChoiceQuestions[this.currentQuestionIndex]
-      } else {
-        return this.multiChoiceQuestions[
-          this.currentQuestionIndex - this.singleChoiceNum
-        ]
-      }
-    }
-  },
-  watch: {
-    choiceAnswers: {
-      deep: true,
-      handler: function(val: Array<{ index: number; answer: string }>) {
-        this.answeredChoices = val
-          .slice(0, this.singleChoiceNum + this.multiChoiceNum)
-          .map((value) => {
-            return { index: value.index, answered: value.answer !== '' }
-          })
+    // codeQuestionNum(): number {
+    //   return this.codeQuestions.length
+    // },
+    objectiveQuestionNum(): number {
+      if (this.examQuestions) {
+        return Object.keys(this.examQuestions.questionAndScore).length
       }
+      return 0
     },
-    currentQuestionIndex(val) {
-      console.log('before ' + this.codeIndex)
-      if (val >= this.choiceQuestionNum) {
-        this.codeIndex = val - this.choiceQuestionNum
-      }
-      console.log(this.codeIndex)
+    totalQuestionNum(): number {
+      return this.objectiveQuestionNum
+      // return this.objectiveQuestionNum + this.codeQuestionNum
     }
   },
+  watch: {},
   methods: {
-    onTimesUp() {
-      this.submitAnswer()
+    questionType(): QuestionType {
+      return this.currentQuestion.type
     },
-    saveSubmittedCode(code: string, lang: string) {
-      const changedCodeQuestion = this.codeQuestions[this.codeIndex]
-      const changedCodeTuple =
-        changedCodeQuestion.lastCommitCodes?.find((val) => {
-          return val.lang === lang
-        }) ?? {}
-      changedCodeTuple.code = code
+    fetchQuestionStem(index: number) {
+      this.fetchingStem = true
+      console.log(this.questionIds)
+      console.log(this.questionIds[index])
+      getQuestionStem(this.questionIds[index])
+        .then(({ data }) => {
+          this.currentQuestion = data.result
+          this.fetchingStem = false
+        })
+        .catch((err) => {})
     },
-    saveChoiceAnswer(answer: string | undefined) {
-      if (this.currentQuestionIndex >= this.choiceQuestionNum) {
-        return
-      }
-
-      if (answer === undefined) {
-        return
-      }
-      this.choiceAnswers[this.currentQuestionIndex].answer = answer
 
-      if (answer !== '') {
-        this.answerStatus[this.currentQuestionIndex].answered = true
-      } else {
-        this.answerStatus[this.currentQuestionIndex].answered = false
-      }
+    onTimesUp() {
+      this.submitAnswer()
     },
-    onSelectQuestion(target: number) {
-      this.currentQuestionIndex = target - 1
+    // saveSubmittedCode(code: string, lang: string) {
+    //   const changedCodeQuestion = this.codeQuestions[this.codeIndex]
+    //   const changedCodeTuple =
+    //     changedCodeQuestion.lastCommitCodes?.find((val) => {
+    //       return val.lang === lang
+    //     }) ?? {}
+    //   changedCodeTuple.code = code
+    // },
+    saveObjectiveAnswer(answer: string) {
+      this.objectiveAnswers[this.currentQuestionIndex] = answer
+      console.log(answer)
     },
+    // onSelectQuestion(target: number) {
+    //   this.currentQuestionIndex = target - 1
+    // },
     leaveExam() {
       this.exitDialog = false
       this.$router.back()
     },
     submitAnswer() {
-      this.submitDialog = false
+      //TODO code
+      this.submitting = true
 
-      // if (
-      //   this.answerStatus.find((value) => {
-      //     return !value.answered
-      //   }) !== undefined
-      // ) {
-      //   this.noticeUnfinished = true
-      //   return
-      // }
-
-      const answers = [
-        ...this.choiceAnswers.map((value) => {
-          return value.answer
-        })
-      ]
+      console.log(this.objectiveAnswers)
+      console.log(this.questionIds)
 
-      const codes = this.codeQuestions.map((value) => {
-        const tuples = value.lastCommitCodes ?? []
-        return JSON.stringify(tuples)
+      objectiveJudge(this.objectiveAnswers, this.questionIds).then(() => {
+        this.submitting = false
+        this.submitDialog = false
+        this.$router.push({ name: 'home' })
       })
-
-      // TODO post
-      console.log(answers)
-      console.log(codes)
-      this.$router.back()
-    },
-    prevQuestion() {
-      do {
-        this.currentQuestionIndex =
-          (--this.currentQuestionIndex + this.totalQuestionNum) %
-          this.totalQuestionNum
-      } while (this.answerStatus[this.currentQuestionIndex].answered)
     },
     nextQuestion() {
-      do {
-        this.currentQuestionIndex =
-          (this.currentQuestionIndex + 1) % this.totalQuestionNum
-      } while (this.answerStatus[this.currentQuestionIndex].answered)
+      if (this.currentQuestionIndex < this.totalQuestionNum - 1) {
+        if (this.objectiveAnswers[this.currentQuestionIndex] === '') {
+        }
+
+        this.currentQuestionIndex++
+        this.fetchQuestionStem(this.currentQuestionIndex)
+      } else {
+        this.finished = true
+      }
     },
     fetchData() {
       this.examId = parseInt(this.$route.params.id)
 
       getExamById(this.examId).then(({ data }) => {
-        this.examInfo = data.res ?? {}
-        this.endTime = new Date().getTime() + 100
-        // this.endTime = Date.parse(this.examInfo.endAt ?? '')
+        this.examInfo = data.result
+        this.endTime = new Date().getTime() + 1000
+        // this.endTime = Date.parse(this.examInfo.endTime ?? '')
         this.currentTime = new Date().getTime()
       })
 
-      getExamQuestions(this.examId)
-        .then(({ data }) => {
-          const examQuestionsObject: ExamQuestionSerializer = data.res ?? {}
-
-          this.singleChoiceQuestions =
-            examQuestionsObject.singleChoiceQuestions?.map((value, index) => {
-              return { ...value, index: index + 1 }
-            }) ?? []
-          this.multiChoiceQuestions =
-            examQuestionsObject.multiChoiceQuestions?.map((value, index) => {
-              return { ...value, index: index + 1 }
-            }) ?? []
-          this.codeQuestions =
-            examQuestionsObject.codeQuestions?.map((value, index) => {
-              return { ...value, index: index + 1 }
-            }) ?? []
-
-          const allQuestions = [
-            ...this.singleChoiceQuestions,
-            ...this.multiChoiceQuestions,
-            ...this.codeQuestions
-          ]
-          this.choiceAnswers = [
-            ...this.singleChoiceQuestions,
-            ...this.multiChoiceQuestions
-          ].map((value, index) => {
-            return { index: index + 1, answer: '' }
-          })
+      getExamQuestions(this.examId).then(({ data }) => {
+        this.examQuestions = data.result
+        this.questionIds = Object.keys(this.examQuestions.questionAndScore)
 
-          this.answerStatus = allQuestions.map((value, index) => {
-            return {
-              index: index + 1,
-              answered: false
-            }
-          })
-
-          this.currentQuestionIndex = 0 // 表示数据初始化完成
-          console.log(this.codeIndex)
-        })
-        .catch(() => {
-          console.log('No data')
-        })
+        this.currentQuestionIndex = 0
+        this.fetchQuestionStem(0)
+      })
     }
   },
   mounted() {
@@ -408,4 +339,13 @@ export default Vue.extend({
 })
 </script>
 
-<style scoped></style>
+<style scoped>
+.questionCard {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  margin: 5vh 10vw;
+  min-height: 90vh;
+  padding: 10px;
+}
+</style>

+ 61 - 0
src/views/exam/components/BlankPane.vue

@@ -0,0 +1,61 @@
+<template>
+  <div
+    class="d-flex flex-column justify-space-between"
+    hover
+    style="cursor: default"
+  >
+    <div class="d-flex">
+      <slot name="index"></slot>
+      <v-card-title>填空题</v-card-title>
+    </div>
+    <markdown-text :raw="question.stem" class="pa-10"></markdown-text>
+    <v-spacer></v-spacer>
+    <v-text-field
+      v-model="answer"
+      class="pa-10"
+      label="请输入答案"
+    ></v-text-field>
+  </div>
+</template>
+
+<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'
+export default Vue.extend({
+  name: 'BlankPane',
+  components: {
+    MarkdownText
+  },
+  props: {
+    question: {
+      type: Object as PropType<QuestionStemSerializer>,
+      required: true
+    }
+  },
+  data() {
+    return {
+      answer: ''
+    }
+  },
+  mounted() {},
+  watch: {
+    question(val) {
+      this.answer = ''
+    }
+  },
+  methods: {},
+  beforeUpdate() {
+    this.$emit('saveAnswer', this.answer)
+  }
+})
+</script>
+
+<style scoped>
+.questionCard {
+  margin: 5vh 10vw;
+  min-height: 90vh;
+  padding: 10px;
+}
+</style>

+ 83 - 77
src/views/exam/components/ChoicePane.vue

@@ -1,73 +1,70 @@
 <template>
-  <div>
-    <v-card
-      class="questionCard d-flex flex-column justify-space-between"
-      hover
-      style="cursor: default"
-    >
-      <div class="d-flex">
-        <v-card-title v-if="question.single">单选题</v-card-title>
-        <v-card-title v-else>多选题</v-card-title>
-        <v-chip class="align-self-center"
-          >{{ question.index }} / {{ totalQuestionNum }}</v-chip
-        >
-      </div>
-      <markdown-text :raw="question.stem" class="pa-10"></markdown-text>
-      <v-list class="px-10 flex">
-        <v-list-item-group v-if="question.single" v-model="answer">
-          <v-divider></v-divider>
-          <template v-for="choice in choices">
-            <v-list-item :key="choice.label" :value="choice.label">
-              <template v-slot:default="{ active }">
-                <v-list-item-action>
-                  <v-checkbox
-                    hide-details
-                    :input-value="active"
-                    on-icon="mdi-radiobox-marked"
-                    off-icon="mdi-radiobox-blank"
-                  >
-                  </v-checkbox>
-                </v-list-item-action>
-                <v-list-item-content>
-                  <v-list-item-title class="text-wrap"
-                    >{{ choice.label }}: {{ choice.content }}</v-list-item-title
-                  >
-                </v-list-item-content>
-              </template>
-            </v-list-item>
-            <v-divider :key="choice.content"></v-divider>
-          </template>
-        </v-list-item-group>
-        <v-list-item-group v-else v-model="answer" multiple>
-          <v-divider></v-divider>
-          <template v-for="choice in choices">
-            <v-list-item :key="choice.label" :value="choice.label">
-              <template v-slot:default="{ active }">
-                <v-list-item-action>
-                  <v-checkbox hide-details :input-value="active"> </v-checkbox>
-                </v-list-item-action>
-                <v-list-item-content>
-                  <v-list-item-title class="text-wrap"
-                    >{{ choice.label }}: {{ choice.content }}</v-list-item-title
-                  >
-                </v-list-item-content>
-              </template>
-            </v-list-item>
-            <v-divider :key="choice.content"></v-divider>
-          </template>
-        </v-list-item-group>
-      </v-list>
-      <div class="d-flex justify-space-around px-10 py-10">
-        <v-btn color="info" @click="nextQuestion">下一题</v-btn>
-      </div>
-    </v-card>
+  <div
+    class="d-flex flex-column justify-space-between"
+    hover
+    style="cursor: default"
+  >
+    <div class="d-flex">
+      <slot name="index"></slot>
+      <v-card-title v-if="question.type === 'choice'">单选题</v-card-title>
+      <v-card-title v-else>多选题</v-card-title>
+    </div>
+    <markdown-text :raw="question.stem" class="pa-10"></markdown-text>
+    <v-list class="px-10 flex" v-if="!loading">
+      <v-list-item-group v-if="question.type === 'choice'" v-model="answer">
+        <v-divider></v-divider>
+        <template v-for="choice in options">
+          <v-list-item :key="choice.label" :value="choice.label">
+            <template v-slot:default="{ active }">
+              <v-list-item-action>
+                <v-checkbox
+                  hide-details
+                  :input-value="active"
+                  on-icon="mdi-radiobox-marked"
+                  off-icon="mdi-radiobox-blank"
+                >
+                </v-checkbox>
+              </v-list-item-action>
+              <v-list-item-content>
+                <v-list-item-title class="text-wrap"
+                  >{{ choice.label }}: {{ choice.text }}</v-list-item-title
+                >
+              </v-list-item-content>
+            </template>
+          </v-list-item>
+          <v-divider :key="choice.template"></v-divider>
+        </template>
+      </v-list-item-group>
+      <v-list-item-group v-else v-model="answer" multiple>
+        <v-divider></v-divider>
+        <template v-for="choice in options">
+          <v-list-item :key="choice.label" :value="choice.label">
+            <template v-slot:default="{ active }">
+              <v-list-item-action>
+                <v-checkbox hide-details :input-value="active"> </v-checkbox>
+              </v-list-item-action>
+              <v-list-item-content>
+                <v-list-item-title class="text-wrap"
+                  >{{ choice.label }}: {{ choice.text }}</v-list-item-title
+                >
+              </v-list-item-content>
+            </template>
+          </v-list-item>
+          <v-divider :key="choice.text"></v-divider>
+        </template>
+      </v-list-item-group>
+    </v-list>
   </div>
 </template>
 
 <script lang="ts">
 import Vue, { PropType } from 'vue'
 import MarkdownText from '@/views/exam/components/MarkdownText.vue'
-import { ChoiceQuestionSerializer, OptionSerializer } from '@/api/types'
+import {
+  QuestionStemSerializer,
+  OptionSerializer,
+  OptionLabelTextPair
+} from '@/api/types'
 import { clone } from 'ramda'
 export default Vue.extend({
   name: 'ChoicePane',
@@ -76,18 +73,15 @@ export default Vue.extend({
   },
   props: {
     question: {
-      type: Object as PropType<ChoiceQuestionSerializer>,
-      required: true
-    },
-    totalQuestionNum: {
-      type: Number,
+      type: Object as PropType<QuestionStemSerializer>,
       required: true
     }
   },
   data() {
     return {
       answer: [] as Array<string>,
-      choices: [] as Array<OptionSerializer>
+      options: [] as Array<OptionLabelTextPair>,
+      loading: true
     }
   },
   computed: {
@@ -95,7 +89,7 @@ export default Vue.extend({
       if (!this.answer || this.answer.length === 0) {
         return ''
       }
-      if (this.question.single) {
+      if (this.question.type === 'choice') {
         return this.answer[0]
       } else {
         const sorted = clone(this.answer).sort()
@@ -104,28 +98,40 @@ export default Vue.extend({
     }
   },
   mounted() {
-    this.choices = clone(this.question.options) ?? []
+    this.init()
   },
   watch: {
     question(val) {
-      this.choices = clone(val.options) ?? []
-      this.answer = []
+      this.loading = true
+      this.init()
     }
   },
   methods: {
-    nextQuestion() {
-      this.$emit('nextQuestion')
+    init() {
+      let keys = Object.keys(this.question.options)
+      let values = Object.values(this.question.options)
+
+      console.log(keys)
+      console.log(values)
+
+      this.options = []
+
+      for (let i = 0; i < keys.length; i++) {
+        this.options.push({ label: keys[i], text: values[i] })
+      }
+      this.answer = []
+      this.loading = false
     }
   },
   beforeUpdate() {
-    this.$emit('saveChoiceAnswer', this.answerString.toString())
+    this.$emit('saveAnswer', this.answerString.toString())
   }
 })
 </script>
 
 <style scoped>
 .questionCard {
-  margin: 1vh 10vw;
+  margin: 5vh 10vw;
   min-height: 90vh;
   padding: 10px;
 }

+ 5 - 2
src/views/exam/components/CodePane.vue

@@ -82,7 +82,10 @@
         </div>
 
         <div style="height: 32px" class="d-flex justify-space-between">
-          <v-chip> 代码题 {{ question.index }} / {{ totalQuestionNum }}</v-chip>
+          <span>
+            <slot name="index"></slot>
+            <v-card-title> 代码题</v-card-title>
+          </span>
           <v-btn color="info" small @click="nextQuestion">下一题</v-btn>
         </div>
       </div>
@@ -159,7 +162,7 @@ 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 { CodeQuestionSerializer, CodeTuple } from '@/api/types'
+import { CodeTuple } from '@/api/types'
 
 interface CommitInfo {
   id?: string

+ 121 - 7
src/views/student/Overview.vue

@@ -1,33 +1,147 @@
 <template>
   <v-container>
-    <page-header title="Overview" :breadcrumb="breadcrumb"></page-header>
+    <page-header title="评测列表" :breadcrumb="breadcrumb">
+      <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-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 small>
+              <v-icon dark>mdi-cached</v-icon>
+            </v-btn>
+          </v-col>
+        </v-row>
+      </template>
+    </page-header>
+    <student-exam-info-card
+      hover
+      v-for="item in examList"
+      :key="item.examId"
+      :exam-info="item"
+    >
+    </student-exam-info-card>
+    <v-pagination
+      v-model="page"
+      :length="Math.floor(totalNum / pageSize) + 1"
+      @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>
+    <v-snackbar v-model="joinFailed">
+      邀请码有误
+    </v-snackbar>
   </v-container>
 </template>
 
 <script lang="ts">
 import Vue from 'vue'
 import PageHeader from '@/components/PageHeader.vue'
+import { getAllExamList, joinExamByInviteCode } from '@/api/exam'
+import { ExamSerializer, ExamState, PageSerializer } from '@/api/types'
+import StudentExamInfoCard from '@/views/student/components/StudentExamInfoCard.vue'
 
 export default Vue.extend({
   name: 'Overview',
   components: {
-    PageHeader
+    PageHeader,
+    StudentExamInfoCard
   },
-  data() {
+  data: function() {
     return {
+      selected: '所有评测',
+      selectItems: ['所有评测', '未开始', '进行中', '已结束'],
+      dialog: false,
       breadcrumb: [
         {
-          text: 'Home',
+          text: '主页',
           disabled: false,
-          href: '/'
+          href: '/student'
         },
         {
-          text: 'Overview',
+          text: '评测列表',
           disabled: true,
           href: '/'
         }
-      ]
+      ],
+      examList: [] as Array<ExamSerializer>,
+      page: 1,
+      totalNum: 0,
+      pageSize: 5,
+      inviteCode: '',
+      joinSuccess: false,
+      joinFailed: false
     }
+  },
+  methods: {
+    changePage() {
+      this.fetchExams()
+    },
+    onJoinExam() {
+      const examId = this.examList.find((val) => {
+        return val.inviteCode === this.inviteCode
+      })?.examId
+      // TODO 核对邀请码加入接口
+      console.log(examId)
+
+      if (examId) {
+        joinExamByInviteCode(this.inviteCode, examId)
+          .then(() => {
+            this.joinSuccess = true
+          })
+          .catch(() => {
+            this.joinFailed = true
+          })
+      } else {
+        this.joinFailed = true
+      }
+
+      this.inviteCode = ''
+    },
+    onOpenInviteCodeDialog() {
+      this.dialog = true
+    },
+    fetchExams(pageNum: number = 1) {
+      getAllExamList({ pageNum: this.page, pageSize: this.pageSize })
+        .then(({ data }) => {
+          this.examList = data.result.examVOList
+          this.totalNum = data.result.totalNum
+        })
+        .catch((err) => {
+          this.examList = []
+        })
+    }
+  },
+  mounted() {
+    this.fetchExams()
   }
 })
 </script>

+ 36 - 20
src/views/student/StudentExamList.vue

@@ -4,11 +4,11 @@
       <template #extra>
         <v-row class=" my-0 py-0" no-gutters justify="end">
           <v-spacer></v-spacer>
-          <v-col cols="3">
-            <v-btn color="info" @click="onJoinExam">
-              参加评测
-            </v-btn>
-          </v-col>
+          <!--          <v-col cols="3">-->
+          <!--            <v-btn color="info" @click="onJoinExam">-->
+          <!--              参加评测-->
+          <!--            </v-btn>-->
+          <!--          </v-col>-->
 
           <v-col cols="3">
             <v-select
@@ -20,7 +20,7 @@
           </v-col>
 
           <v-col cols="1" class="ml-5">
-            <v-btn color="secondary" fab small>
+            <v-btn color="secondary" fab small @click="refreshState">
               <v-icon dark>mdi-cached</v-icon>
             </v-btn>
           </v-col>
@@ -30,11 +30,15 @@
     <student-exam-info-card
       hover
       v-for="item in examList"
-      :key="item.id"
+      :key="item.examId"
       :exam-info="item"
     >
     </student-exam-info-card>
-    <v-pagination v-model="page" :length="pageInfo.total" @input="changePage">
+    <v-pagination
+      v-model="page"
+      :length="Math.floor(totalNum / pageSize) + 1"
+      @input="changePage"
+    >
     </v-pagination>
   </v-container>
 </template>
@@ -42,8 +46,8 @@
 <script lang="ts">
 import Vue from 'vue'
 import PageHeader from '@/components/PageHeader.vue'
-import { getAllExamList } from '@/api/exam'
-import { ExamSerializer, PageSerializer } from '@/api/types'
+import { getExamsByStudentId } from '@/api/exam'
+import { ExamSerializer, ExamState, PageSerializer } from '@/api/types'
 import StudentExamInfoCard from '@/views/student/components/StudentExamInfoCard.vue'
 
 export default Vue.extend({
@@ -55,8 +59,10 @@ export default Vue.extend({
   data: function() {
     return {
       page: 1,
-      selected: '正在进行',
-      selectItems: ['未开始', '正在进行', '已结束'],
+      totalNum: 0,
+      pageSize: 5,
+      selected: '所有评测',
+      selectItems: ['所有评测', '未开始', '进行中', '已结束'],
       dialog: false,
       breadcrumb: [
         {
@@ -70,28 +76,38 @@ export default Vue.extend({
           href: '/'
         }
       ],
-      examList: [] as Array<ExamSerializer>,
-      pageInfo: {} as PageSerializer
+      examList: [] as Array<ExamSerializer>
     }
   },
   methods: {
     changePage() {
-      this.fetchExams()
+      this.fetchExams(this.page)
+    },
+    refreshState() {
+      this.page = 1
+      this.fetchExams(1)
     },
     onJoinExam() {},
-    fetchExams(page: number = 1) {
-      getAllExamList({ page, limit: 10, status: 'READY' })
+    fetchExams(pageNum: number = 1) {
+      getExamsByStudentId(
+        { pageNum: pageNum, pageSize: 10 },
+        this.selectItems.findIndex((val) => {
+          return this.selected === val
+        }) as ExamState
+      )
         .then(({ data }) => {
-          this.examList = data.res ?? []
-          this.pageInfo = data.page ?? {}
+          this.examList = data.result.examVOList
+          this.totalNum = data.result.totalNum
         })
         .catch((err) => {
+          console.log(err)
+
           this.examList = []
         })
     }
   },
   mounted() {
-    this.fetchExams()
+    this.fetchExams(this.page)
   }
 })
 </script>

+ 26 - 6
src/views/student/StudentHome.vue

@@ -6,8 +6,10 @@
           <div class="pa-4">
             <div
               style="height: 40px; width: 100%"
-              class="indigo lighten-3 rounded-lg"
-            ></div>
+              class="indigo lighten-3 rounded-lg d-flex justify-space-around"
+            >
+              <h1 class="align-self-center">SEEC-EVAL</h1>
+            </div>
           </div>
         </div>
         <div class="overflow-auto flex-grow-1" style="box-sizing: border-box;">
@@ -32,7 +34,7 @@
         </div>
         <div class="flex-shrink-0">
           <div class="pa-2 pl-6 text--secondary">
-            <v-chip color="primary">Your Name ></v-chip>
+            <v-chip color="primary">{{ userName }}</v-chip>
           </div>
           <div class="pa-2 pl-6 pb-5 text--secondary">All Right Reserve</div>
         </div>
@@ -57,18 +59,23 @@ import {
   mdiPackage,
   mdiHome
 } from '@mdi/js'
+import { getUserInfo } from '@/api/user'
+import { UserSerializer } from '@/api/types'
+import { mapMutations } from 'vuex'
 
 export default Vue.extend({
   name: 'StudentHome',
   components: {},
   data: () => ({
+    userName: '',
+    userInfo: (null as unknown) as UserSerializer,
     icons: {
       mdiAccount,
       mdiBell
     },
     linkList: [
       {
-        name: '首页',
+        name: '评测列表',
         url: '/student/home',
         icon: mdiHome
       },
@@ -78,12 +85,25 @@ export default Vue.extend({
         icon: mdiAccount
       },
       {
-        name: '评测',
+        name: '我的评测',
         url: '/student/exams',
         icon: mdiViewList
       }
     ]
-  })
+  }),
+  methods: {
+    ...mapMutations(['setUser']),
+    getUser() {
+      getUserInfo().then(({ data }) => {
+        this.userInfo = data.result
+        this.userName = this.userInfo.name
+        this.setUser(this.userInfo)
+      })
+    }
+  },
+  mounted() {
+    this.getUser()
+  }
 })
 </script>
 

+ 54 - 28
src/views/student/components/StudentExamInfoCard.vue

@@ -2,24 +2,18 @@
   <v-card hover class="ma-5 px-3 my-8" style="cursor: default;height: 20%">
     <v-row>
       <v-col class="d-flex">
-        <v-card-title class="ma-0 pa-0">{{ examInfo.title }}</v-card-title>
-        <v-chip :color="examInfo.state" label class="ml-5" outlined
-          >已结束</v-chip
-        >
+        <v-card-title class="ma-0 pa-0">{{ examInfo.examName }}</v-card-title>
+        <v-chip :color="statusToColor(state)" label class="ml-5" outlined>{{
+          state
+        }}</v-chip>
       </v-col>
       <v-col class="mr-3 d-flex justify-end">
-        <v-btn
-          color="success"
-          class="mx-2"
-          v-if="examInfo.joined === true"
-          @click="showExamDetail"
+        <!--         TODO v-if-->
+        <v-btn color="success" class="mx-2" @click="showExamDetail"
           >开始评测</v-btn
         >
-        <v-btn color="info" v-else @click="onJoinExam" class="mx-2"
-          >报名参加</v-btn
-        >
         <v-btn
-          v-if="examInfo.status === 'FINISHED'"
+          v-if="examInfo.state === '已结束'"
           color="secondary"
           class="mx-2"
           @click="onOpenResult"
@@ -41,20 +35,18 @@
         >
       </v-col>
       <v-col>
-        <v-card-text class="pa-0 pb-5"
-          >评测时长: {{ examInfo.lastTime }} 小时</v-card-text
-        >
+        <v-card-text class="pa-0 pb-5">评测时长: {{}} 小时</v-card-text>
       </v-col>
     </v-row>
     <v-row no-gutters>
       <v-col>
         <v-card-text class="pa-0 pb-5"
-          >开始时间: {{ examInfo.startAt }}</v-card-text
+          >开始时间: {{ examInfo.startTime }}</v-card-text
         >
       </v-col>
       <v-col>
         <v-card-text class="pa-0 pb-5"
-          >结束时间: {{ examInfo.endAt }}</v-card-text
+          >结束时间: {{ examInfo.endTime }}</v-card-text
         >
       </v-col>
       <v-spacer></v-spacer>
@@ -64,12 +56,14 @@
 
 <script lang="ts">
 import Vue, { PropType } from 'vue'
-import { ExamSerializer } from '@/api/types'
+import { ExamSerializer, ExamState, ExamStateText } from '@/api/types'
 
 export default Vue.extend({
   name: 'ExamInfoCard',
   data() {
-    return {}
+    return {
+      state: (null as unknown) as ExamStateText
+    }
   },
   props: {
     examInfo: {
@@ -77,10 +71,28 @@ export default Vue.extend({
       required: true
     }
   },
+  mounted() {
+    this.state = this.computeExamState()
+  },
   methods: {
+    computeExamState(): ExamStateText {
+      const now = new Date().getTime()
+      const startTime = new Date(this.examInfo.startTime).getTime()
+      const endTime = new Date(this.examInfo.endTime).getTime()
+
+      if (endTime < now) {
+        return '已结束'
+      }
+
+      if (startTime > now) {
+        return '未开始'
+      }
+
+      return '进行中'
+    },
     onJoinExam() {},
     onOpenResult() {
-      const examId = this.examInfo.id?.toString() ?? ''
+      const examId = this.examInfo.examId?.toString() ?? ''
 
       this.$router.push({
         name: 'result',
@@ -90,14 +102,28 @@ export default Vue.extend({
       })
     },
     showExamDetail() {
-      const examId = this.examInfo.id?.toString()
+      const examId = this.examInfo.examId?.toString()
       if (examId !== undefined) {
-        this.$router.push({
-          name: 'detail',
-          params: {
-            id: examId
-          }
-        })
+        if (this.$route.name === 'exams') {
+          this.$router.push({
+            name: 'detail',
+            params: {
+              id: examId,
+              joined: 'true'
+            }
+          })
+        }
+      } else {
+      }
+    },
+    statusToColor(state: ExamStateText) {
+      switch (state) {
+        case '未开始':
+          return 'info'
+        case '进行中':
+          return 'success'
+        case '已结束':
+          return 'secondary'
       }
     }
   }

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

@@ -170,8 +170,10 @@
     <!--创建成功后显示-->
     <v-container v-else>
       <v-card class="px-3 mb-5">
-        <v-card-title>考试「{{ createdExamInfo.title }}」创建成功</v-card-title>
-        <v-card-subtitle>考试ID: {{ createdExamInfo.id }}</v-card-subtitle>
+        <v-card-title
+          >考试「{{ createdExamInfo.examName }}」创建成功</v-card-title
+        >
+        <v-card-subtitle>考试ID: {{ createdExamInfo.examId }}</v-card-subtitle>
         <v-row no-gutters align-content="start">
           <v-col>
             <v-card-text class="pa-0 pb-3"
@@ -256,7 +258,8 @@ export default Vue.extend({
       endTime: '',
       comment: '',
       selectedQuestions: [] as Array<ID>,
-      selectQuestion: false
+      selectQuestion: false,
+      antiCheating: false
     }
   },
   computed: {
@@ -296,20 +299,21 @@ export default Vue.extend({
     },
     onCreateExam() {
       const obj = {
-        userId: 'userId1',
+        creatorId: 'userId1',
         examName: this.examName,
         position: this.position,
         skills: this.skills,
         difficulty: this.difficulty,
         startTime: this.startDateFormatted + ' ' + this.startTime + ':00',
         endTime: this.endDateFormatted + ' ' + this.endTime + ':00',
-        comment: this.comment
-      }
+        comment: this.comment,
+        antiCheating: this.antiCheating
+      } as ExamSerializer
       console.log('创建考试', obj)
       createExam(obj).then(({ data }) => {
-        console.log(data.res)
-        this.isExamCreated = data.res ? true : false
-        this.createdExamInfo = data.res || {}
+        console.log(data.result)
+        this.isExamCreated = data.result ? true : false
+        this.createdExamInfo = obj
       })
     }
   },

+ 19 - 13
src/views/teacher/ExamManage.vue

@@ -5,7 +5,7 @@
         <v-row class=" my-0 py-0" no-gutters justify="end">
           <v-spacer></v-spacer>
           <v-col cols="3">
-            <router-link :to="{ name: 'create' }">
+            <router-link :to="{ name: 'create' }" style="text-decoration: none">
               <!--            <router-link :to="{ name: 'teacher-home' }">-->
               <v-btn color="info">
                 创建评测
@@ -33,11 +33,15 @@
     <teacher-exam-info-card
       hover
       v-for="item in examList"
-      :key="item.id"
+      :key="item.examId"
       :exam-info="item"
     >
     </teacher-exam-info-card>
-    <v-pagination v-model="page" :length="pageInfo.total" @input="changePage">
+    <v-pagination
+      v-model="page"
+      :length="totalNum / pageSize"
+      @input="changePage"
+    >
     </v-pagination>
   </v-container>
 </template>
@@ -46,8 +50,8 @@
 import Vue from 'vue'
 import PageHeader from '@/components/PageHeader.vue'
 import TeacherExamInfoCard from '@/views/teacher/components/TeacherExamInfoCard.vue'
-import { ExamSerializer, PageSerializer } from '@/api/types'
-import { getAllExamList } from '@/api/exam'
+import { ExamSerializer, ExamState, PageSerializer } from '@/api/types'
+import { getExamsByTeacherId } from '@/api/exam'
 import { mapGetters } from 'vuex'
 
 export default Vue.extend({
@@ -59,9 +63,12 @@ export default Vue.extend({
   data: function() {
     return {
       page: 1,
-      selected: '正在进行',
-      selectItems: ['未开始', '正在进行', '已结束'],
+      totalNum: 0,
+      pageSize: 5,
+      selected: 0 as ExamState,
+      selectItems: ['所有考试', '未开始', '进行中', '已结束'],
       dialog: false,
+
       breadcrumb: [
         {
           text: '主页',
@@ -74,8 +81,7 @@ export default Vue.extend({
           href: '/'
         }
       ],
-      examList: [] as Array<ExamSerializer>,
-      pageInfo: {} as PageSerializer
+      examList: [] as Array<ExamSerializer>
     }
   },
   methods: {
@@ -87,11 +93,11 @@ export default Vue.extend({
       // this.$router.push({ name: 'exam-create' })
       this.$router.push('/teacher/exam-create')
     },
-    fetchExams(page: number = 1) {
-      getAllExamList({ page, limit: 10, id: this.getId() })
+    fetchExams(pageNum: number = 1) {
+      getExamsByTeacherId({ pageNum: pageNum, pageSize: 5 }, this.selected)
         .then(({ data }) => {
-          this.examList = data.res ?? []
-          this.pageInfo = data.page ?? {}
+          this.examList = data.result.examVOList
+          this.totalNum = data.result.totalNum
         })
         .catch((err) => {
           this.examList = []

+ 5 - 9
src/views/teacher/components/QuestionList.vue

@@ -68,17 +68,17 @@
 
 <script lang="ts">
 import Vue, { PropType } from 'vue'
-import { ChoiceQuestionSerializer, ID, Pageable } from '@/api/types'
-import { getQuestions } from '@/api/question'
+import { QuestionStemSerializer, ID, Pageable } from '@/api/types'
+import { getExamQuestions } from '@/api/exam'
 
 export default Vue.extend({
   name: 'QuestionList',
   props: {},
   data() {
     return {
-      questions: [] as Array<ChoiceQuestionSerializer>,
+      questions: [] as Array<QuestionStemSerializer>,
       selectedId: [] as Array<ID>,
-      selectedQuestions: [] as Array<ChoiceQuestionSerializer>,
+      selectedQuestions: [] as Array<QuestionStemSerializer>,
       headers: [
         { text: '技能点', value: 'skillId' },
         { text: '知识点', value: 'knowledgeId' },
@@ -117,11 +117,7 @@ export default Vue.extend({
   },
   mounted() {},
   methods: {
-    fetchData() {
-      getQuestions({}).then(({ data }) => {
-        this.questions = data.res ?? []
-      })
-    }
+    fetchData() {}
   }
 })
 </script>

+ 4 - 4
src/views/teacher/components/TeacherExamInfoCard.vue

@@ -2,7 +2,7 @@
   <v-card hover class="ma-5 px-3 my-8" style="cursor: default;height: 20%">
     <v-row>
       <v-col class="d-flex">
-        <v-card-title class="ma-0 pa-0">{{ examInfo.title }}</v-card-title>
+        <v-card-title class="ma-0 pa-0">{{ examInfo.examName }}</v-card-title>
         <v-chip :color="examInfo.state" label class="ml-5" outlined
           >已结束</v-chip
         >
@@ -48,12 +48,12 @@
     <v-row no-gutters>
       <v-col>
         <v-card-text class="pa-0 pb-5"
-          >开始时间: {{ examInfo.startAt }}</v-card-text
+          >开始时间: {{ examInfo.startTime }}</v-card-text
         >
       </v-col>
       <v-col>
         <v-card-text class="pa-0 pb-5"
-          >结束时间: {{ examInfo.endAt }}</v-card-text
+          >结束时间: {{ examInfo.endTime }}</v-card-text
         >
       </v-col>
       <v-col>
@@ -83,7 +83,7 @@ export default Vue.extend({
   methods: {
     onExtendTime() {},
     showExamDetail() {
-      const examId = this.examInfo.id?.toString()
+      const examId = this.examInfo.examId?.toString()
       if (examId !== undefined) {
         this.$router.push({
           name: 'detail',

+ 1 - 0
vue.config.js

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