ChengYuXuan 5 лет назад
Родитель
Сommit
94c4068bbc

+ 3 - 0
src/api/axios.config.ts

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

+ 2 - 2
src/api/exam.ts

@@ -35,12 +35,12 @@ export const extendExamTime = (examId: ID, endTime: LocalDateTime) => {
 
 export const getExamQuestions = (examId: ID) => {
   return axios.get<ReceivedData<ExamQuestionSerializer>>(
-    `${EXAM_MODULE}/${examId}/question`
+    `${EXAM_MODULE}/${examId}/questions`
   )
 }
 
 export const joinExamByInviteCode = (inviteCode: string, examId: ID) => {
-  return axios.put<ReceivedData>(
+  return axios.put<ReceivedData<boolean>>(
     `${EXAM_MODULE}/invite/code?inviteCode=${inviteCode}&examId=${examId}`
   )
 }

+ 2 - 2
src/api/judge.ts

@@ -4,10 +4,10 @@ import { ReceivedData } from '@/api/types'
 
 export const objectiveJudge = (
   answers: Array<string>,
-  questionIds: Array<string>
+  questionsIds: Array<string>
 ) => {
   return axios.post<ReceivedData>(`${JUDGE_MODULE}/objective`, {
     answers,
-    questionIds
+    questionsIds
   })
 }

+ 3 - 2
src/api/question.ts

@@ -6,7 +6,8 @@ import {
   Pageable,
   ExtendedQuestionSerializer,
   QuestionStemSerializer,
-  ReceivedData
+  ReceivedData,
+  SampleSerializer
 } from '@/api/types'
 
 export const getQuestionInfo = (id: ID) => {
@@ -22,7 +23,7 @@ export const getQuestionStem = (id: ID) => {
 }
 
 export const getSamplePaper = () => {
-  return axios.get<ReceivedData<Array<ExamQuestionSerializer>>>(
+  return axios.get<ReceivedData<Array<SampleSerializer>>>(
     `${QUESTION_MODULE}/sample`
   )
 }

+ 6 - 2
src/api/types.d.ts

@@ -44,7 +44,7 @@ export interface ExamSerializer {
   // 上边是创建评测所需的信息
   examId?: number
   inviteCode?: string
-  joined?: boolean
+  isJoined?: boolean // 该变量仅在/exam/list 中有意义,其他接口要注意处理
 }
 
 export interface CreateExamResponse {
@@ -121,10 +121,14 @@ export interface CodeTuple {
 // }
 
 export interface ExamQuestionSerializer {
+  questionAndScore: QuestionAndScore
+  // codeQuestions?: Array<CodeQuestionSerializer>
+}
+
+export interface SampleSerializer {
   id: number
   name: string
   questionAndScore: QuestionAndScore
-  // codeQuestions?: Array<CodeQuestionSerializer>
 }
 
 export interface UserSerializer {

+ 0 - 0
src/mock/api/exam/_examId/question/get.json → src/mock/api/exam/_examId/questions/get.json


+ 0 - 0
src/mock/api/exam/question/sample/_examId/post.json → src/mock/api/exam/questions/sample/_examId/post.json


+ 3 - 3
src/router/exam.ts

@@ -8,10 +8,10 @@ const routes: Array<RouteConfig> = [
       import(/* webpackChunkName: "exam" */ '@/views/exam/ExamBasicLayout.vue'),
     children: [
       {
-        path: 'detail',
-        name: 'detail',
+        path: 'before-exam',
+        name: 'before-exam',
         component: () =>
-          import(/* webpackChunkName: "exam" */ '@/views/exam/ExamDetails.vue')
+          import(/* webpackChunkName: "exam" */ '@/views/exam/BeforeExam.vue')
       },
       {
         path: 'exam-pane',

+ 9 - 1
src/router/teacher.ts

@@ -36,12 +36,20 @@ const routes: Array<RouteConfig> = [
           )
       },
       {
-        path: 'exam/:examId',
+        path: 'exam/:examId/edit',
         name: 'edit-exams',
         component: () =>
           import(
             /* webpackChunkName: "teacher" */ '@/views/teacher/EditExam.vue'
           )
+      },
+      {
+        path: 'exam/:examId/details',
+        name: 'teacher-exam-details',
+        component: () =>
+          import(
+            /* webpackChunkName: "teacher" */ '@/views/teacher/TeacherExamDetail.vue'
+          )
       }
     ]
   }

+ 3 - 2
src/store/index.ts

@@ -12,8 +12,9 @@ export type RootState = {
 
 export const state = (): RootState => ({
   user: (null as unknown) as UserSerializer,
-  token:
-    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwLnNlZWNvZGVyLmNuIiwic3ViIjoiMTU5OTYyMzEzMTMiLCJhdWQiOiJzZWVjLXBvcnRhbCIsImF1dGhfdGltZSI6MTYxMjAxMjk1MCwiaWF0IjoxNjEyMDEyOTUwLCJleHAiOjE2MTIwOTkzNTAsInVzZXJfaW5mbyI6eyJwaG9uZSI6IjE1OTk2MjMxMzEzIiwiZW1haWwiOiIxNjExNTAwMDlAc21haWwubmp1LmVkdS5jbiIsImlkIjozMzIsIm5hbWUiOiIxNjExNTAwMDkiLCJyb2xlIjoiU1RVREVOVCJ9fQ.qLCB7yQnH1_ZLzl5Ir_9LuDYmn6xQk7ZRlFxywMKYgc'
+  token: ''
+  // token:
+  //   'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwLnNlZWNvZGVyLmNuIiwic3ViIjoiMTU5OTYyMzEzMTMiLCJhdWQiOiJzZWVjLXBvcnRhbCIsImF1dGhfdGltZSI6MTYxMjAxMjk1MCwiaWF0IjoxNjEyMDEyOTUwLCJleHAiOjE2MTIwOTkzNTAsInVzZXJfaW5mbyI6eyJwaG9uZSI6IjE1OTk2MjMxMzEzIiwiZW1haWwiOiIxNjExNTAwMDlAc21haWwubmp1LmVkdS5jbiIsImlkIjozMzIsIm5hbWUiOiIxNjExNTAwMDkiLCJyb2xlIjoiU1RVREVOVCJ9fQ.qLCB7yQnH1_ZLzl5Ir_9LuDYmn6xQk7ZRlFxywMKYgc'
 })
 
 export const mutations: MutationTree<RootState> = {

+ 55 - 7
src/utils/common.ts

@@ -1,15 +1,26 @@
-import { LocalDateTime } from '@/api/types'
-
+import { ExamStateText, LocalDateTime } from '@/api/types'
+import dayjs from 'dayjs'
+import Duration from 'dayjs/plugin/duration'
+dayjs.extend(Duration)
+/**
+ * 两个时间的差值,默认精确到分钟
+ * @param start
+ * @param end
+ * @param second 可选,是否精确到秒
+ */
 export const timeLength = function(
   start: LocalDateTime,
-  end: LocalDateTime
+  end: LocalDateTime,
+  second: boolean = false
 ): string {
-  const s = new Date(start).getTime()
-  const e = new Date(end).getTime()
+  const s = dayjs(start, TIME_FORMAT).valueOf()
+  const e = dayjs(end, TIME_FORMAT).valueOf()
   const t = e - s
 
-  let hour = Math.floor(t / 3600000)
-  let min = Math.floor((t - hour * 3600000) / 60000)
+  const duration = dayjs.duration(t, 'millisecond')
+
+  let hour = duration.get('hour')
+  let min = duration.get('minute')
 
   let format = ''
   if (hour > 0) {
@@ -19,5 +30,42 @@ export const timeLength = function(
     format = `${min}分`
   }
 
+  if (second) {
+    let second = duration.get('second')
+    format += `${second}秒`
+  }
+
   return format
 }
+
+export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
+
+export const computeExamStateText = function(
+  startTime: LocalDateTime,
+  endTime: LocalDateTime
+): ExamStateText {
+  const now = dayjs()
+  const formattedStartTime = dayjs(startTime, TIME_FORMAT)
+  const formattedEndTime = dayjs(endTime, TIME_FORMAT)
+
+  if (formattedEndTime < now) {
+    return '已结束'
+  }
+  if (formattedStartTime > now) {
+    return '未开始'
+  }
+  return '进行中'
+}
+
+export const statusToColor = function(state: ExamStateText): string {
+  switch (state) {
+    case '未开始':
+      return 'info'
+    case '进行中':
+      return 'success'
+    case '已结束':
+      return 'secondary'
+    default:
+      return ''
+  }
+}

+ 2 - 2
src/views/exam/ExamDetails.vue → src/views/exam/BeforeExam.vue

@@ -56,7 +56,7 @@ import { getExamById, getExamQuestions } from '@/api/exam'
 import { timeLength } from '@/utils/common'
 
 export default Vue.extend({
-  name: 'ExamDetails',
+  name: 'BeforeExam',
   components: {
     PageHeader
   },
@@ -75,7 +75,7 @@ export default Vue.extend({
           href: '/student/exams'
         },
         {
-          text: '评测详情',
+          text: '考前确认',
           disabled: true,
           href: '/'
         }

+ 16 - 4
src/views/exam/ExamPane.vue

@@ -181,6 +181,9 @@
       <v-snackbar v-model="finished" absolute>
         已到最后一题
       </v-snackbar>
+      <v-snackbar v-model="examTimesUp" absolute>
+        已到最后一题
+      </v-snackbar>
     </v-main>
   </div>
 </template>
@@ -232,7 +235,8 @@ export default Vue.extend({
 
       submitting: false,
       finished: false,
-      answeredx: false
+      answered: false,
+      examTimesUp: false
     }
   },
   computed: {
@@ -260,8 +264,7 @@ export default Vue.extend({
     },
     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
@@ -271,6 +274,7 @@ export default Vue.extend({
     },
 
     onTimesUp() {
+      this.examTimesUp = true
       this.submitAnswer()
     },
     // saveSubmittedCode(code: string, lang: string) {
@@ -302,7 +306,12 @@ export default Vue.extend({
       objectiveJudge(this.objectiveAnswers, this.questionIds).then(() => {
         this.submitting = false
         this.submitDialog = false
-        this.$router.push({ name: 'home' })
+        this.$router.push({
+          name: 'result',
+          params: {
+            id: this.examId.toString()
+          }
+        })
       })
     },
     nextQuestion() {
@@ -332,6 +341,9 @@ export default Vue.extend({
 
         this.currentQuestionIndex = 0
         this.fetchQuestionStem(0)
+
+        this.objectiveAnswers = Array(this.objectiveQuestionNum)
+        this.objectiveAnswers.fill('')
       })
     }
   },

+ 2 - 6
src/views/exam/components/ChoicePane.vue

@@ -89,12 +89,8 @@ export default Vue.extend({
       if (!this.answer || this.answer.length === 0) {
         return ''
       }
-      if (this.question.type === 'choice') {
-        return this.answer[0]
-      } else {
-        const sorted = clone(this.answer).sort()
-        return sorted.join('')
-      }
+      const sorted = clone(this.answer).sort()
+      return sorted.join('')
     }
   },
   mounted() {

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

@@ -29,7 +29,18 @@ export default Vue.extend({
   },
   data() {
     return {
-      breadcrumb: []
+      breadcrumb: [
+        {
+          text: '主页',
+          disabled: false,
+          href: '/student'
+        },
+        {
+          text: '个人信息',
+          disabled: true,
+          href: '/'
+        }
+      ]
     }
   }
 })

+ 20 - 40
src/views/student/Overview.vue

@@ -5,25 +5,24 @@
         <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-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 small>
+            <v-icon dark>mdi-cached</v-icon>
+          </v-btn>
+          <!--          </v-col>-->
         </v-row>
       </template>
     </page-header>
@@ -32,6 +31,7 @@
       v-for="item in examList"
       :key="item.examId"
       :exam-info="item"
+      @joinExam="fetchExams"
     >
     </student-exam-info-card>
     <v-pagination
@@ -105,31 +105,11 @@ export default Vue.extend({
     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 = ''
-    },
+    onJoinExam() {}, // TODO 直接邀请码加入
     onOpenInviteCodeDialog() {
       this.dialog = true
     },
-    fetchExams(pageNum: number = 1) {
+    fetchExams() {
       getAllExamList({ pageNum: this.page, pageSize: this.pageSize })
         .then(({ data }) => {
           this.examList = data.result.examVOList

+ 10 - 7
src/views/student/StudentExamList.vue

@@ -72,9 +72,9 @@ export default Vue.extend({
           href: '/student'
         },
         {
-          text: '评测列表',
+          text: '我的评测',
           disabled: true,
-          href: '/student/exams'
+          href: '/'
         }
       ],
       examList: [] as Array<ExamSerializer>,
@@ -83,16 +83,16 @@ export default Vue.extend({
   },
   methods: {
     changePage() {
-      this.fetchExams(this.page)
+      this.fetchExams()
     },
     refreshState() {
       this.page = 1
-      this.fetchExams(1)
+      this.fetchExams()
     },
     onJoinExam() {},
-    fetchExams(pageNum: number = 1) {
+    fetchExams() {
       getExamsByStudentId(
-        { pageNum: pageNum, pageSize: 10 },
+        { pageNum: this.page, pageSize: 10 },
         this.selectItems.findIndex((val) => {
           return this.selected === val
         }) as ExamState
@@ -100,6 +100,9 @@ export default Vue.extend({
         .then(({ data }) => {
           this.examList = data.result.examVOList
           this.totalNum = data.result.totalNum
+          this.examList.forEach((val, index) => {
+            this.examList[index].isJoined = true // 该变量仅在/exam/list 中有意义
+          })
         })
         .catch((err) => {
           console.log(err)
@@ -109,7 +112,7 @@ export default Vue.extend({
     }
   },
   mounted() {
-    this.fetchExams(this.page)
+    this.fetchExams()
   }
 })
 </script>

+ 44 - 41
src/views/student/components/StudentExamInfoCard.vue

@@ -8,21 +8,33 @@
         }}</v-chip>
       </v-col>
       <v-col class="mr-3 d-flex justify-end">
-        <!--         TODO v-if-->
-        <v-btn color="success" class="mx-2" @click="showExamDetail"
+        <v-btn
+          v-if="state === '未开始'"
+          :disabled="examInfo.isJoined"
+          :outlined="examInfo.isJoined"
+          color="primary"
+          class="mx-2"
+          @click="onJoinExam"
+        >
+          <template v-if="examInfo.isJoined">已加入</template>
+          <template v-else>报名参加</template>
+        </v-btn>
+        <v-btn
+          v-if="examInfo.isJoined && state === '进行中'"
+          color="success"
+          class="mx-2"
+          @click="onStartExam"
           >开始评测</v-btn
         >
         <v-btn
-          v-if="state === '已结束'"
+          v-if="examInfo.isJoined && state === '已结束'"
           color="secondary"
           class="mx-2"
           @click="onOpenResult"
         >
           结果报告
         </v-btn>
-        <v-btn v-else color="primary" @click="showExamDetail" class="mx-2"
-          >查看详情</v-btn
-        >
+        <v-btn color="info" class="mx-2">查看详情</v-btn>
       </v-col>
     </v-row>
     <v-row justify="space-between" dense>
@@ -64,20 +76,16 @@
 
 <script lang="ts">
 import Vue, { PropType } from 'vue'
-import {
-  ExamSerializer,
-  ExamState,
-  ExamStateText,
-  ID,
-  LocalDateTime
-} from '@/api/types'
-import { timeLength } from '@/utils/common'
+import { ExamSerializer, ExamStateText, LocalDateTime } from '@/api/types'
+import { computeExamStateText, timeLength } from '@/utils/common'
+import { joinExamByInviteCode } from '@/api/exam'
 
 export default Vue.extend({
   name: 'StudentExamInfoCard',
   data() {
     return {
-      state: (null as unknown) as ExamStateText
+      state: (null as unknown) as ExamStateText,
+      timeFormat: 'YYYY-MM-DD HH:mm:ss'
     }
   },
   props: {
@@ -87,28 +95,25 @@ export default Vue.extend({
     }
   },
   mounted() {
-    this.state = this.computeExamState()
+    this.state = computeExamStateText(
+      this.examInfo.startTime,
+      this.examInfo.endTime
+    )
   },
   methods: {
     getExamLength(startTime: LocalDateTime, endTime: LocalDateTime): string {
       return timeLength(startTime, endTime)
     },
-    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() {
+      const examId = this.examInfo.examId ?? 0
+      const inviteCode = this.examInfo.inviteCode ?? ''
+      joinExamByInviteCode(inviteCode, examId).then(({ data }) => {
+        const joinSuccess = data.result
+        if (joinSuccess) {
+          this.$emit('joinExam')
+        }
+      })
     },
-    onJoinExam() {},
     onOpenResult() {
       const examId = this.examInfo.examId?.toString() ?? ''
 
@@ -119,18 +124,16 @@ export default Vue.extend({
         }
       })
     },
-    showExamDetail() {
+    onStartExam() {
       const examId = this.examInfo.examId?.toString()
       if (examId !== undefined) {
-        if (this.$route.name === 'exams') {
-          this.$router.push({
-            name: 'detail',
-            params: {
-              id: examId,
-              joined: 'true'
-            }
-          })
-        }
+        this.$router.push({
+          name: 'before-exam',
+          params: {
+            id: examId,
+            joined: 'true'
+          }
+        })
       } else {
       }
     },

+ 168 - 19
src/views/teacher/EditExam.vue

@@ -1,24 +1,82 @@
 <template>
   <v-container>
+    <page-header title="评测设置" :breadcrumb="breadcrumb"></page-header>
     <v-container>
       <v-card class="px-3 mb-5" v-if="examInfo">
-        <v-card-title>评测「{{ examInfo.examName }}」创建成功</v-card-title>
-        <v-card-subtitle>评测ID: {{ examInfo.examId }}</v-card-subtitle>
-        <v-row no-gutters align-content="start">
-          <v-col>
-            <v-card-text class="pa-0 pb-3"
-              >评测邀请码:{{ 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-card-title v-if="isSampleSet"
+          >评测「{{ examInfo.examName }}」</v-card-title
+        >
+        <v-card-title v-else
+          >评测「{{ examInfo.examName }}」创建成功 请选择样卷</v-card-title
+        >
+        <v-card-subtitle>评测ID: {{ examInfo.examId }} </v-card-subtitle>
+        <v-card-text>
+          <v-row no-gutters align-content="start">
+            <v-col>
+              <v-card-text class="pa-0 pb-3"
+                >评测邀请码:{{ 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-card-text>
       </v-card>
-      <v-container class="grey lighten-5 pa-6">
-        <v-card-title>样卷</v-card-title>
+      <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>
@@ -26,21 +84,112 @@
 
 <script lang="ts">
 import Vue from 'vue'
-import { ExamSerializer } from '@/api/types'
-import { getExamById } from '@/api/exam'
+import {
+  ExamSerializer,
+  ExtendedQuestionSerializer,
+  ID,
+  ReceivedData,
+  SampleSerializer
+} from '@/api/types'
+import {
+  getExamById,
+  getExamQuestions,
+  setExamPaperFromSample
+} 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'
 
 export default Vue.extend({
   name: 'EditExam',
+  components: {
+    MarkdownText,
+    PageHeader
+  },
   data() {
     return {
-      examInfo: (null as unknown) as ExamSerializer
+      examInfo: (null as unknown) as ExamSerializer,
+      tab: 0,
+      examSamples: [] as Array<SampleSerializer>,
+      sampleQuestions: [] as Array<Array<ExtendedQuestionSerializer>>,
+      fetching: false,
+      isSampleSet: false,
+      breadcrumb: [
+        {
+          text: '主页',
+          disabled: false,
+          href: '/teacher'
+        },
+        {
+          text: '评测列表',
+          disabled: false,
+          href: '/teacher/created-exams'
+        },
+        {
+          text: '评测设置',
+          disabled: true,
+          href: '/'
+        }
+      ]
+    }
+  },
+  watch: {
+    tab(val) {
+      this.fetchQuestionInfo(val)
     }
   },
+  methods: {
+    fetchQuestionInfo(val: number) {
+      if (this.sampleQuestions[val]) {
+        return
+      } else {
+        this.fetching = true
+
+        let questionsPromises: Promise<
+          AxiosResponse<ReceivedData<ExtendedQuestionSerializer, 0>>
+        >[] = []
+        let questionsIds = Object.entries(
+          this.examSamples[val].questionAndScore
+        )
+
+        questionsPromises = questionsIds.map((val) => {
+          return getQuestionInfo(val[0])
+        })
+
+        Promise.all(questionsPromises).then((reps) => {
+          this.sampleQuestions[val] = reps.map((value) => {
+            return value.data.result
+          })
+          this.fetching = false
+        })
+      }
+    },
+    onSelectSample(sampleId: ID) {
+      setExamPaperFromSample(this.examInfo.examId ?? '', sampleId)
+    }
+  },
+
   mounted() {
     const examId = this.$route.params.examId
+
     getExamById(examId).then(({ data }) => {
       this.examInfo = data.result
     })
+
+    getExamQuestions(examId ?? '')
+      .then(({ data }) => {
+        this.isSampleSet = true
+      })
+      .catch(({ data }) => {})
+
+    getSamplePaper().then(({ data }) => {
+      this.examSamples = data.result
+      // let tmp = clone(data.result[0])
+      // tmp.id = 2
+      // this.examSamples.push(tmp)
+      this.fetchQuestionInfo(0)
+    })
   }
 })
 </script>

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

@@ -167,7 +167,12 @@ export default Vue.extend({
         {
           text: '主页',
           disabled: false,
-          href: '/'
+          href: '/teacher'
+        },
+        {
+          text: '评测管理',
+          disabled: false,
+          href: '/teacher/created-exams'
         },
         {
           text: '创建评测',

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

@@ -73,7 +73,7 @@ export default Vue.extend({
         {
           text: '主页',
           disabled: false,
-          href: '/student'
+          href: '/teacher'
         },
         {
           text: '评测列表',
@@ -91,15 +91,15 @@ export default Vue.extend({
     },
     refreshState() {
       this.page = 1
-      this.fetchExams(1)
+      this.fetchExams()
     },
     onCreateExam() {
       // this.$router.push({ name: 'exam-create' })
       this.$router.push('/teacher/exam-create')
     },
-    fetchExams(pageNum: number = 1) {
+    fetchExams() {
       getExamsByTeacherId(
-        { pageNum: pageNum, pageSize: 5 },
+        { pageNum: this.page, pageSize: 5 },
         this.selectItems.findIndex((val) => {
           return this.selected === val
         }) as ExamState

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

@@ -19,7 +19,7 @@ export default Vue.extend({
         {
           text: 'Home',
           disabled: false,
-          href: '/'
+          href: '/teacher'
         },
         {
           text: 'Overview',

+ 118 - 0
src/views/teacher/TeacherExamDetail.vue

@@ -0,0 +1,118 @@
+<template>
+  <v-container>
+    <page-header title="考试信息" :breadcrumb="breadcrumb"></page-header>
+    <v-container class="grey lighten-5 pa-6 d-flex flex-column">
+      <div v-if="!fetching" style="min-height: 50vh">
+        <div class="text-center display-1">试卷</div>
+        <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-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-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import { getExamQuestions } from '@/api/exam'
+import { AxiosResponse } from 'axios'
+import {
+  ExtendedQuestionSerializer,
+  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 { getExamJoinedUsers } from '@/api/user'
+
+export default Vue.extend({
+  name: 'TeacherExamDetail',
+  components: {
+    MarkdownText,
+    PageHeader
+  },
+  data() {
+    return {
+      fetching: false,
+      examQuestions: [] as Array<ExtendedQuestionSerializer>,
+      joinedUsers: [] as Array<UserSerializer>,
+      breadcrumb: [
+        {
+          text: '主页',
+          disabled: false,
+          href: '/teacher'
+        },
+        {
+          text: '评测列表',
+          disabled: false,
+          href: '/teacher/created-exams'
+        },
+        {
+          text: '评测详情',
+          disabled: true,
+          href: '/'
+        }
+      ]
+    }
+  },
+  mounted() {
+    const examId = this.$route.params.examId
+
+    getExamQuestions(examId).then(({ data }) => {
+      this.fetching = true
+
+      let questionsPromises: Promise<
+        AxiosResponse<ReceivedData<ExtendedQuestionSerializer, 0>>
+      >[] = []
+      let questionsIds = Object.entries(data.result.questionAndScore)
+      questionsIds.forEach((v, ind) => {
+        questionsPromises[ind] = getQuestionInfo(v[0])
+      })
+
+      Promise.all(questionsPromises).then((reps) => {
+        this.examQuestions = reps.map((value) => {
+          return value.data.result
+        })
+        this.fetching = false
+      })
+    })
+
+    getExamJoinedUsers(examId).then(({ data }) => {
+      this.joinedUsers = data.result
+    })
+  }
+})
+</script>
+
+<style scoped></style>

+ 22 - 4
src/views/teacher/TeacherHome.vue

@@ -32,7 +32,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,6 +57,9 @@ import {
   mdiPackage,
   mdiHome
 } from '@mdi/js'
+import { mapMutations } from 'vuex'
+import { getUserInfo } from '@/api/user'
+import { UserSerializer } from '@/api/types'
 
 export default Vue.extend({
   name: 'StudentHome',
@@ -79,11 +82,26 @@ export default Vue.extend({
       },
       {
         name: '评测管理',
-        url: '/teacher/exams',
+        url: '/teacher/created-exams',
         icon: mdiViewList
       }
-    ]
-  })
+    ],
+    userName: '',
+    userInfo: (null as unknown) as UserSerializer
+  }),
+  methods: {
+    ...mapMutations(['setUser']),
+    getUser() {
+      getUserInfo().then(({ data }) => {
+        this.userInfo = data.result
+        this.userName = this.userInfo.name
+        this.setUser(this.userInfo)
+      })
+    }
+  },
+  mounted() {
+    this.getUser()
+  }
 })
 </script>
 

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

@@ -3,29 +3,29 @@
     <v-row>
       <v-col class="d-flex">
         <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
-        >
+        <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
-          v-if="examInfo.status === '已结束'"
-          color="secondary"
-          class="mx-2"
-        >
+        <v-btn v-if="state === '已结束'" color="secondary" class="mx-2">
           结果报告
         </v-btn>
         <v-btn
           color="warning"
           class="mx-2"
-          v-if="examInfo.status === '进行中'"
+          v-if="state === '进行中'"
           @click="onExtendTime"
           >延长时间</v-btn
         >
-        <v-btn color="info" class="mx-2" v-if="examInfo.status === '未开始'"
-          >修改评测</v-btn
+        <v-btn
+          color="primary"
+          class="mx-2"
+          v-if="state === '未开始'"
+          @click="onEditExam"
+          >编辑评测</v-btn
         >
-        <v-btn color="primary" class="mx-2" @click="showExamDetail"
+        <v-btn color="info" class="mx-2" @click="showExamDetail"
           >查看详情</v-btn
         >
       </v-col>
@@ -65,14 +65,20 @@
 
 <script lang="ts">
 import Vue, { PropType } from 'vue'
-import { ExamSerializer, LocalDateTime, UserSerializer } from '@/api/types'
-import { timeLength } from '@/utils/common'
+import {
+  ExamSerializer,
+  ExamStateText,
+  LocalDateTime,
+  UserSerializer
+} from '@/api/types'
+import { computeExamStateText, timeLength } from '@/utils/common'
 import { getExamJoinedUsers } from '@/api/user'
 
 export default Vue.extend({
   name: 'ExamInfoCard',
   data() {
     return {
+      state: (null as unknown) as ExamStateText,
       joinedUsers: [] as Array<UserSerializer>
     }
   },
@@ -86,24 +92,50 @@ export default Vue.extend({
     getExamLength(startTime: LocalDateTime, endTime: LocalDateTime): string {
       return timeLength(startTime, endTime)
     },
+    onEditExam() {
+      const examId = this.examInfo.examId
+
+      if (examId) {
+        this.$router.push({
+          name: 'edit-exams',
+          params: {
+            examId: examId.toString()
+          }
+        })
+      }
+    },
     onExtendTime() {},
     showExamDetail() {
-      const examId = this.examInfo.examId?.toString()
-      if (examId !== undefined) {
+      const examId = this.examInfo.examId
+      if (examId) {
         this.$router.push({
-          name: 'detail',
+          name: 'teacher-exam-details',
           params: {
-            id: examId
+            examId: examId.toString()
           }
         })
       }
+    },
+    statusToColor(state: ExamStateText) {
+      switch (state) {
+        case '未开始':
+          return 'info'
+        case '进行中':
+          return 'success'
+        case '已结束':
+          return 'secondary'
+      }
     }
   },
   mounted() {
+    this.state = computeExamStateText(
+      this.examInfo.startTime,
+      this.examInfo.endTime
+    )
+
     if (this.examInfo.examId) {
       getExamJoinedUsers(this.examInfo.examId).then(({ data }) => {
         this.joinedUsers = data.result
-        console.log(this.joinedUsers.length)
       })
     }
   }

+ 2 - 2
vue.config.js

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