Преглед изворни кода

老师管理、创建考试界面

ChengYuXuan пре 5 година
родитељ
комит
4f17606e8d

+ 15 - 1
src/api/exam.ts

@@ -1,9 +1,23 @@
 import axios, { ResponseType } from 'axios'
 import axios, { ResponseType } from 'axios'
 import { EXAM_MODULE } from './prefix'
 import { EXAM_MODULE } from './prefix'
-import { ExamSerializer, ReceivedData, Pageable, ExamStatus } from '@/api/types'
+import {
+  ExamSerializer,
+  ReceivedData,
+  Pageable,
+  ExamStatus,
+  ID
+} from '@/api/types'
 
 
 export const getAllExamList = (params: Pageable<{ status: ExamStatus }>) => {
 export const getAllExamList = (params: Pageable<{ status: ExamStatus }>) => {
   return axios.get<ReceivedData<Array<ExamSerializer>>>(`${EXAM_MODULE}`, {
   return axios.get<ReceivedData<Array<ExamSerializer>>>(`${EXAM_MODULE}`, {
     params
     params
   })
   })
 }
 }
+
+export const getExamById = (examId: ID) => {
+  return axios.get<ReceivedData<ExamSerializer>>(`${EXAM_MODULE}/${examId}`)
+}
+
+export const createExam = (exam: object) => {
+  return axios.post(`${EXAM_MODULE}`)
+}

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

@@ -1,5 +1,7 @@
 import { AxiosRequestConfig } from 'axios'
 import { AxiosRequestConfig } from 'axios'
 
 
+export type ID = number | string
+
 export interface AxiosResponseInfo {}
 export interface AxiosResponseInfo {}
 
 
 export interface ReceivedData<R = {}, E = 0> extends AxiosResponseInfo {
 export interface ReceivedData<R = {}, E = 0> extends AxiosResponseInfo {
@@ -47,6 +49,10 @@ export interface ExamSerializer {
   // result?: ResultSerializer
   // result?: ResultSerializer
   isReentryPermitted?: boolean
   isReentryPermitted?: boolean
   inviteCode?: string
   inviteCode?: string
+  comment?: string
+  participantsNumber?: number
+  lastTime?: number
+  difficulty?: number
 }
 }
 
 
 export interface ResultSerializer {
 export interface ResultSerializer {
@@ -68,3 +74,38 @@ export interface ResultSerializer {
     createdAt?: LocalDateTime
     createdAt?: LocalDateTime
   }>
   }>
 }
 }
+
+export interface OptionSerializer {
+  label?: string
+  content?: string
+}
+
+export type ChoiceAnswer = string
+
+export interface ChoiceQuestionSerializer {
+  id?: number
+  studentId?: number
+  stem?: string
+  single?: boolean
+  options?: Array<OptionSerializer>
+  lastCommitAnswers?: ChoiceAnswer
+}
+
+export interface CodeTemplate {
+  lang?: string
+  template?: string
+}
+
+export interface CodeQuestionSerializer {
+  id?: number
+  studentId?: number
+  stem?: string
+  codeTemplate?: CodeTemplate
+  lastCommitCode?: string
+}
+
+export interface ExamQuestionSerializer {
+  id?: number
+  choiceQuestions?: Array<ChoiceQuestionSerializer>
+  codeQuestions?: Array<CodeQuestionSerializer>
+}

+ 4 - 0
src/mock/api/exam/_examId/get.json

@@ -0,0 +1,4 @@
+{
+  "err": 0,
+  "res": "@EXAM_SERIALIZER"
+}

+ 4 - 0
src/mock/api/exam/post.json

@@ -0,0 +1,4 @@
+{
+  "err":0,
+  "res": "@Exam_Serializer"
+}

+ 3 - 2
src/mock/serializers.js

@@ -20,7 +20,7 @@ const basicExtendsTypes = () => ({
 })
 })
 
 
 const serializersPlugin = (mock) => ({
 const serializersPlugin = (mock) => ({
-  EXAM_SERIALIZER: () =>
+  EXAM_SERIALIZER: (examId) =>
     mock({
     mock({
       'id|1-2000': 1,
       'id|1-2000': 1,
       title: '@CTITLE',
       title: '@CTITLE',
@@ -34,7 +34,8 @@ const serializersPlugin = (mock) => ({
       },
       },
       joined: '@BOOLEAN',
       joined: '@BOOLEAN',
       isReentryPermitted: '@BOOLEAN',
       isReentryPermitted: '@BOOLEAN',
-      inviteCode: '@WORD(5)'
+      inviteCode: '@WORD(5)',
+      'difficulty|0-100': 1
     }),
     }),
   PAGE_SERIALIZER: () => ({
   PAGE_SERIALIZER: () => ({
     page: 1,
     page: 1,

+ 5 - 2
src/router/exam.ts

@@ -2,10 +2,8 @@ import { RouteConfig } from 'vue-router'
 
 
 const routes: Array<RouteConfig> = [
 const routes: Array<RouteConfig> = [
   {
   {
-    meta: { roles: ['student'] },
     path: '/exam/:id',
     path: '/exam/:id',
     name: 'exam',
     name: 'exam',
-    redirect: 'student/exam-list',
     component: () =>
     component: () =>
       import(/* webpackChunkName: "exam" */ '@/views/exam/ExamBasicLayout.vue'),
       import(/* webpackChunkName: "exam" */ '@/views/exam/ExamBasicLayout.vue'),
     children: [
     children: [
@@ -30,6 +28,11 @@ const routes: Array<RouteConfig> = [
             name: 'code'
             name: 'code'
           }
           }
         ]
         ]
+      },
+      {
+        path: 'result',
+        name: 'result'
+        // component: ()=> import(/*webpackChunkName: "exam"*/ '@/views/exam')
       }
       }
     ]
     ]
   }
   }

+ 4 - 3
src/router/index.ts

@@ -2,17 +2,18 @@ import Vue from 'vue'
 import VueRouter, { RouteConfig } from 'vue-router'
 import VueRouter, { RouteConfig } from 'vue-router'
 import student from '@/router/student'
 import student from '@/router/student'
 import exam from '@/router/exam'
 import exam from '@/router/exam'
+import teacher from '@/router/teacher'
 Vue.use(VueRouter)
 Vue.use(VueRouter)
 
 
 const routes: Array<RouteConfig> = [
 const routes: Array<RouteConfig> = [
   {
   {
     path: '/',
     path: '/',
     name: 'Home',
     name: 'Home',
-    // redirect: "/student",
-    component: () => import(/* webpackChunkName: "Home" */ '@/views/Home.vue')
+    component: () => import('@/views/Home.vue')
   },
   },
   ...student,
   ...student,
-  ...exam
+  ...exam,
+  ...teacher
 ]
 ]
 
 
 const router = new VueRouter({
 const router = new VueRouter({

+ 1 - 2
src/router/student.ts

@@ -9,7 +9,6 @@ const routes: Array<RouteConfig> = [
       import(
       import(
         /* webpackChunkName: "student" */ '@/views/student/StudentHome.vue'
         /* webpackChunkName: "student" */ '@/views/student/StudentHome.vue'
       ),
       ),
-    redirect: 'student/home',
     children: [
     children: [
       {
       {
         path: 'home',
         path: 'home',
@@ -30,7 +29,7 @@ const routes: Array<RouteConfig> = [
         name: 'exams',
         name: 'exams',
         component: () =>
         component: () =>
           import(
           import(
-            /* webpackChunkName: "student" */ '@/views/student/ExamList.vue'
+            /* webpackChunkName: "student" */ '@/views/student/StudentExamList.vue'
           )
           )
       }
       }
     ]
     ]

+ 42 - 0
src/router/teacher.ts

@@ -0,0 +1,42 @@
+import { RouteConfig } from 'vue-router'
+
+const routes: Array<RouteConfig> = [
+  {
+    meta: { roles: ['teacher'] },
+    path: '/teacher',
+    name: 'teacher',
+    component: () =>
+      import(
+        /* webpackChunkName: "teacher" */ '@/views/teacher/TeacherHome.vue'
+      ),
+    children: [
+      {
+        path: 'home',
+        name: 'teacher-home',
+        component: () =>
+          import(
+            /* webpackChunkName: "teacher" */ '@/views/teacher/Overview.vue'
+          )
+      },
+      {
+        path: 'exams',
+        name: 'created-exams',
+        component: () =>
+          import(
+            /* webpackChunkName: "teacher" */ '@/views/teacher/ExamManage.vue'
+          ),
+        children: []
+      },
+      {
+        path: 'create',
+        name: 'create',
+        component: () =>
+          import(
+            /* webpackChunkName: "teacher" */ '@/views/teacher/ExamCreate.vue'
+          )
+      }
+    ]
+  }
+]
+
+export default routes

+ 16 - 5
src/store/index.ts

@@ -1,13 +1,24 @@
 import Vue from 'vue'
 import Vue from 'vue'
-import Vuex from 'vuex'
+import Vuex, { GetterTree, MutationTree } from 'vuex'
+import { ExamSerializer } from '@/api/types'
 
 
 Vue.use(Vuex)
 Vue.use(Vuex)
 
 
+export type RootState = {
+  examInfo: ExamSerializer
+}
+
+const state = (): RootState => ({
+  examInfo: {}
+})
+
+const mutations: MutationTree<RootState> = {
+  examInfo: (state, examInfo) => (state.examInfo = examInfo)
+}
+
 export default new Vuex.Store({
 export default new Vuex.Store({
-  state: {
-    userId: ''
-  },
-  mutations: {},
+  state: state,
+  mutations: mutations,
   actions: {},
   actions: {},
   modules: {}
   modules: {}
 })
 })

+ 3 - 0
src/views/Home.vue

@@ -5,6 +5,9 @@
     <router-link :to="{ name: 'student' }">
     <router-link :to="{ name: 'student' }">
       <v-btn color="primary">student</v-btn>
       <v-btn color="primary">student</v-btn>
     </router-link>
     </router-link>
+    <router-link :to="{ name: 'teacher' }">
+      <v-btn color="primary">teacher</v-btn>
+    </router-link>
   </v-app>
   </v-app>
 </template>
 </template>
 
 

+ 38 - 16
src/views/exam/ExamDetails.vue

@@ -4,6 +4,11 @@
       <template #extra>
       <template #extra>
         <v-row justify="end">
         <v-row justify="end">
           <v-spacer></v-spacer>
           <v-spacer></v-spacer>
+          <v-col cols="3">
+            <v-btn color="info" @click="onJoinExam" :loading="joining"
+              >参加考试</v-btn
+            >
+          </v-col>
           <v-col cols="3">
           <v-col cols="3">
             <router-link :to="{ name: 'exam-pane' }">
             <router-link :to="{ name: 'exam-pane' }">
               <v-btn color="success">进入考试</v-btn>
               <v-btn color="success">进入考试</v-btn>
@@ -18,33 +23,36 @@
       </template>
       </template>
     </page-header>
     </page-header>
     <v-card class="mb-5" hover style="cursor: default">
     <v-card class="mb-5" hover style="cursor: default">
-      <v-card-title>考试须知 </v-card-title>
-      <v-card-subtitle>考试信息</v-card-subtitle>
-      <v-card-text>abc </v-card-text>
-      <v-card-subtitle>考试规则</v-card-subtitle>
-      <v-card-text>abc</v-card-text>
-      <v-card-subtitle>考试内容</v-card-subtitle>
-      <v-card-text>abc</v-card-text>
+      <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
+      >
+      <v-card-text class="text-body-1 black--text">
+        单选题 道 多选题 道 编程题 道
+      </v-card-text>
+      <v-card-subtitle class="text-h5 black--text">考试规则</v-card-subtitle>
+      <v-card-subtitle class="text-h5 black--text">考试内容</v-card-subtitle>
     </v-card>
     </v-card>
   </v-container>
   </v-container>
 </template>
 </template>
 
 
 <script lang="ts">
 <script lang="ts">
-import Vue, { PropType } from 'vue'
+import Vue from 'vue'
 import PageHeader from '@/components/PageHeader.vue'
 import PageHeader from '@/components/PageHeader.vue'
 import { ExamSerializer } from '@/api/types'
 import { ExamSerializer } from '@/api/types'
+import { getExamById } from '@/api/exam'
 
 
 export default Vue.extend({
 export default Vue.extend({
   name: 'ExamDetails',
   name: 'ExamDetails',
   components: {
   components: {
     PageHeader
     PageHeader
   },
   },
-  props: {
-    examInfo: {
-      type: Object as PropType<ExamSerializer>,
-      required: true
-    }
-  },
+
   data() {
   data() {
     return {
     return {
       breadcrumb: [
       breadcrumb: [
@@ -63,10 +71,24 @@ export default Vue.extend({
           disabled: true,
           disabled: true,
           href: '/'
           href: '/'
         }
         }
-      ]
+      ],
+      examInfo: {} as ExamSerializer,
+      joining: false
     }
     }
   },
   },
-  mounted() {}
+  mounted() {
+    getExamById(this.$route.params.id).then(({ data }) => {
+      this.examInfo = data.res ?? {}
+    })
+  },
+  methods: {
+    onJoinExam() {
+      this.joining = true
+      setTimeout(() => {
+        this.joining = false
+      }, 1000)
+    }
+  }
 })
 })
 </script>
 </script>
 
 

+ 31 - 24
src/views/exam/ExamPane.vue

@@ -3,10 +3,14 @@
     <v-app-bar dense app class="indigo lighten-5">
     <v-app-bar dense app class="indigo lighten-5">
       <v-row>
       <v-row>
         <v-col>
         <v-col>
-          <h1>基础能力测试</h1>
+          <span class="display-1">基础能力测试</span>
         </v-col>
         </v-col>
         <v-col class="d-inline-flex justify-end">
         <v-col class="d-inline-flex justify-end">
-          <h2 class="align-self-center">考试剩余时间 01:02:03</h2>
+          <count-down
+            class="align-self-center"
+            :current-time="currentTime"
+            :end-time="`${currentTime + 10000}`"
+          ></count-down>
         </v-col>
         </v-col>
       </v-row>
       </v-row>
     </v-app-bar>
     </v-app-bar>
@@ -35,7 +39,6 @@
               :key="i.id"
               :key="i.id"
               class="ml-2 mt-2 pa-0 d-inline"
               class="ml-2 mt-2 pa-0 d-inline"
               x-small
               x-small
-              :disabled="i.answered"
             >
             >
               {{ i.id }}
               {{ i.id }}
             </v-btn>
             </v-btn>
@@ -124,15 +127,15 @@
         </div>
         </div>
       </div>
       </div>
     </v-navigation-drawer>
     </v-navigation-drawer>
-    <v-main class="indigo lighten-5 pr-0 pt-0">
-      <v-container fluid>
+    <v-main class="indigo lighten-5 pa-0">
+      <v-container fluid class="pa-2">
         <code-pane
         <code-pane
           v-if="!isChoiceProblem"
           v-if="!isChoiceProblem"
           v-on:nextQuestion="nextQuestion"
           v-on:nextQuestion="nextQuestion"
         ></code-pane>
         ></code-pane>
         <choice-pane
         <choice-pane
           v-if="isChoiceProblem"
           v-if="isChoiceProblem"
-          v-on:nextQuestion="nextQuestion"
+          v-on:nextQuestion="nextQuestion(answer)"
         ></choice-pane>
         ></choice-pane>
       </v-container>
       </v-container>
     </v-main>
     </v-main>
@@ -143,11 +146,10 @@
 import Vue from 'vue'
 import Vue from 'vue'
 import CodePane from '@/views/exam/components/CodePane.vue'
 import CodePane from '@/views/exam/components/CodePane.vue'
 import ChoicePane from '@/views/exam/components/ChoicePane.vue'
 import ChoicePane from '@/views/exam/components/ChoicePane.vue'
+import CountDown from '@/views/exam/components/CountDown.vue'
+import { ChoiceQuestionSerializer } from '@/api/types'
 
 
-interface Question {
-  answered: boolean
-  id: number
-}
+interface OrderedCQuestion {}
 
 
 export default Vue.extend({
 export default Vue.extend({
   name: 'ExamPane',
   name: 'ExamPane',
@@ -156,36 +158,41 @@ export default Vue.extend({
       isChoiceProblem: true,
       isChoiceProblem: true,
       exitDialog: false,
       exitDialog: false,
       submitDialog: false,
       submitDialog: false,
-      questions: [] as Array<Question>
+      choiceQuestions: [] as Array<ChoiceQuestionSerializer>,
+      submitted: false,
+      currentTime: new Date().getTime(),
+      choiceAnswers: [] as Array<String>
+    }
+  },
+  computed: {
+    choiceQuestionAnswered(): Array<Boolean> {
+      return this.choiceAnswers.map((value, index) => value !== '')
     }
     }
   },
   },
   components: {
   components: {
+    CountDown,
     CodePane,
     CodePane,
     ChoicePane
     ChoicePane
   },
   },
   methods: {
   methods: {
     leaveExam() {
     leaveExam() {
       this.exitDialog = false
       this.exitDialog = false
-      this.$router.go(-1)
+      this.$router.back()
     },
     },
     submitAnswer() {
     submitAnswer() {
       this.submitDialog = false
       this.submitDialog = false
-      this.$router.go(-1)
+      this.$router.back()
     },
     },
-    nextQuestion() {
+    nextQuestion(answer: String) {
+      this.choiceAnswers.push(answer)
       this.isChoiceProblem = !this.isChoiceProblem
       this.isChoiceProblem = !this.isChoiceProblem
     }
     }
   },
   },
-  beforeMount() {
-    for (let i = 1; i <= 20; i++) {
-      const r = Math.floor(Math.random() * 2)
-
-      this.questions.push({
-        answered: r === 0,
-        id: i
-      })
-    }
-  }
+  beforeMount() {}
+  // beforeRouteLeave(to, from, next) {
+  //   this.exitDialog = true
+  //
+  // }
 })
 })
 </script>
 </script>
 
 

+ 12 - 33
src/views/exam/components/ChoicePane.vue

@@ -43,51 +43,31 @@
 </template>
 </template>
 
 
 <script lang="ts">
 <script lang="ts">
-import Vue from 'vue'
+import Vue, { PropType } from 'vue'
 import MarkdownText from '@/views/exam/components/MarkdownText.vue'
 import MarkdownText from '@/views/exam/components/MarkdownText.vue'
+import { ChoiceQuestionSerializer, OptionSerializer } from '@/api/types'
 export default Vue.extend({
 export default Vue.extend({
   name: 'ChoicePane',
   name: 'ChoicePane',
   components: {
   components: {
     MarkdownText
     MarkdownText
   },
   },
+  props: {
+    question: {},
+    type: Object as PropType<ChoiceQuestionSerializer>
+  },
   data() {
   data() {
     return {
     return {
-      isHovered: false,
-      question: {
-        description:
-          '床前明月光 疑是地上霜 举头望明月 低头思故乡\n床前明月光 疑是地上霜 举头望明月 低头思故乡\n床前明月光 疑是地上霜 举头望明月 低头思故乡\n\n ![picture](http://static.runoob.com/images/runoob-logo.png)'
-      },
       answer: [],
       answer: [],
-      choices: [
-        {
-          id: 'A',
-          text: '床前明月光',
-          choose: ''
-        },
-        {
-          id: 'B',
-          text: '床前明月光 疑是地上霜',
-          choose: ''
-        },
-        {
-          id: 'C',
-          text: '床前明月光 疑是地上霜 举头望明月',
-          choose: ''
-        },
-        {
-          id: 'D',
-          text:
-            '床前明月光 疑是地上霜 举头望明月 低头思故乡 床前明月光 疑是地上霜 举头望明月 低头思故乡 床前明月光 疑是地上霜 举头望明月 低头思故乡床前明月光 疑是地上霜 举头望明月 低头思故乡',
-          choose: ''
-        }
-      ]
+      choices: [] as Array<OptionSerializer>
     }
     }
   },
   },
   methods: {
   methods: {
     nextQuestion() {
     nextQuestion() {
-      this.$emit('nextQuestion')
+      this.$emit('nextQuestion', this.answer.join(''))
     },
     },
-    prevQuestion() {}
+    prevQuestion() {
+      this.$emit('prevQuestion')
+    }
   }
   }
 })
 })
 </script>
 </script>
@@ -95,8 +75,7 @@ export default Vue.extend({
 <style scoped>
 <style scoped>
 .questionCard {
 .questionCard {
   margin: 1vh 10vw;
   margin: 1vh 10vw;
-  min-height: 90vh;
-  max-height: 90vh;
+  height: 90vh;
   padding: 10px;
   padding: 10px;
 }
 }
 </style>
 </style>

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

@@ -86,7 +86,7 @@ export default Vue.extend({
         }
         }
       })
       })
     },
     },
-    setValue(val) {
+    setValue(val: string) {
       this.editor.setValue(val)
       this.editor.setValue(val)
     }
     }
   }
   }

+ 24 - 24
src/views/exam/components/CodePane.vue

@@ -85,10 +85,7 @@
         </div>
         </div>
       </div>
       </div>
       <v-divider vertical class="mx-1"></v-divider>
       <v-divider vertical class="mx-1"></v-divider>
-      <div
-        style="width: 50vw"
-        class="d-flex-grow-1 d-flex flex-column justify-space-between"
-      >
+      <div style="width: 50vw; height: 100%" class="d-flex flex-column">
         <div
         <div
           style="height: 48px"
           style="height: 48px"
           class="d-flex pr-5 justify-space-between elevation-1 mb-1"
           class="d-flex pr-5 justify-space-between elevation-1 mb-1"
@@ -145,9 +142,7 @@
           >
           >
         </div>
         </div>
         <code-editor
         <code-editor
-          style="height: 100%"
           :language="currentLang"
           :language="currentLang"
-          class="flex-grow-1"
           v-model="currentCode"
           v-model="currentCode"
         ></code-editor>
         ></code-editor>
       </div>
       </div>
@@ -203,30 +198,30 @@ export default Vue.extend({
         '等待运行'
         '等待运行'
       ],
       ],
       statusColors: ['success', 'warning', 'error', 'accent', 'info'],
       statusColors: ['success', 'warning', 'error', 'accent', 'info'],
-      stem: '123145',
+      stem: '',
       codeQuestions: [] as CodeQuestionPair[],
       codeQuestions: [] as CodeQuestionPair[],
       currentQuestion: {} as CodeQuestionPair,
       currentQuestion: {} as CodeQuestionPair,
       currentCode: [] as CodeQuestionPair[]
       currentCode: [] as CodeQuestionPair[]
     }
     }
   },
   },
   computed: {
   computed: {
-    lastEdit: function() {
-      return clone(this.currentQuestion.currentCode)
-    },
-
-    codeByLanguage() {
-      for (const languageCode of this.currentQuestion.currentCode) {
-        if (this.currentLang === languageCode.type) {
-          return languageCode.code
-        }
-      }
-      return ''
-    }
+    // lastEdit: function() {
+    //   return clone(this.currentQuestion.currentCode)
+    // },
+    //
+    // codeByLanguage() {
+    //   for (const languageCode of this.currentQuestion.currentCode) {
+    //     if (this.currentLang === languageCode.type) {
+    //       return languageCode.code
+    //     }
+    //   }
+    //   return ''
+    // }
   },
   },
   watch: {
   watch: {
-    currentLang() {
-      this.currentCode = this.codeByLanguage
-    }
+    // currentLang() {
+    //   this.currentCode = this.codeByLanguage
+    // }
   },
   },
   beforeMount() {
   beforeMount() {
     for (let i = 0; i < 10; i++) {
     for (let i = 0; i < 10; i++) {
@@ -284,7 +279,7 @@ export default Vue.extend({
         currentCode: [cpp, java, python]
         currentCode: [cpp, java, python]
       })
       })
 
 
-      this.currentCode = this.codeByLanguage
+      // this.currentCode = this.codeByLanguage
       console.log(this.currentCode)
       console.log(this.currentCode)
     },
     },
     resetCode() {
     resetCode() {
@@ -299,4 +294,9 @@ export default Vue.extend({
 })
 })
 </script>
 </script>
 
 
-<style scoped></style>
+<style scoped>
+.questionCard {
+  margin: 0.5vh;
+  height: 92vh;
+}
+</style>

+ 77 - 0
src/views/exam/components/CountDown.vue

@@ -0,0 +1,77 @@
+<template>
+  <span>
+    <v-chip label> 离结束还有:{{ content }} </v-chip>
+  </span>
+</template>
+<script>
+export default {
+  props: {
+    currentTime: {
+      type: Number,
+      default: 0
+    },
+    endTime: {
+      type: Number,
+      default: 0
+    },
+    endText: {
+      type: String,
+      default: '已结束'
+    }
+  },
+  data() {
+    return {
+      content: '加载中...',
+      end: '', //  结束时间
+      current: '' // 当前时间
+    }
+  },
+  watch: {
+    currentTime() {
+      this.startCount()
+    }
+  },
+  mounted() {
+    this.startCount()
+  },
+  methods: {
+    startCount() {
+      this.current = this.currentTime
+      this.end = this.endTime
+      this.countDown()
+    },
+    countDown() {
+      const self = this
+      const timer = setInterval(function() {
+        const nowTime = new Date(self.current * 1000)
+        const endTime = new Date(self.end * 1000)
+        const t = endTime.getTime() - nowTime.getTime()
+        if (t > 0) {
+          const day = Math.floor(t / 86400000)
+          let hour = Math.floor((t / 3600000) % 24)
+          let min = Math.floor((t / 60000) % 60)
+          let sec = Math.floor((t / 1000) % 60)
+          hour = hour < 10 ? '0' + hour : hour
+          min = min < 10 ? '0' + min : min
+          sec = sec < 10 ? '0' + sec : sec
+          let format = ''
+          if (day > 0) {
+            format = `${day}天${hour}小时${min}分${sec}秒`
+          }
+          if (day <= 0 && hour > 0) {
+            format = `${hour}小时${min}分${sec}秒`
+          }
+          if (day <= 0 && hour <= 0) {
+            format = `${min}分${sec}秒`
+          }
+          self.content = format
+          self.current = self.current + 1
+        } else {
+          clearInterval(timer)
+          self.content = self.endText
+        }
+      }, 1000)
+    }
+  }
+}
+</script>

+ 4 - 4
src/views/student/ExamList.vue → src/views/student/StudentExamList.vue

@@ -27,13 +27,13 @@
         </v-row>
         </v-row>
       </template>
       </template>
     </page-header>
     </page-header>
-    <exam-info-card
+    <student-exam-info-card
       hover
       hover
       v-for="item in examList"
       v-for="item in examList"
       :key="item.id"
       :key="item.id"
       :exam-info="item"
       :exam-info="item"
     >
     >
-    </exam-info-card>
+    </student-exam-info-card>
     <v-pagination v-model="page" :length="pageInfo.total" @input="changePage">
     <v-pagination v-model="page" :length="pageInfo.total" @input="changePage">
     </v-pagination>
     </v-pagination>
   </v-container>
   </v-container>
@@ -42,15 +42,15 @@
 <script lang="ts">
 <script lang="ts">
 import Vue from 'vue'
 import Vue from 'vue'
 import PageHeader from '@/components/PageHeader.vue'
 import PageHeader from '@/components/PageHeader.vue'
-import ExamInfoCard from '@/views/student/components/ExamInfoCard.vue'
 import { getAllExamList } from '@/api/exam'
 import { getAllExamList } from '@/api/exam'
 import { ExamSerializer, PageSerializer } from '@/api/types'
 import { ExamSerializer, PageSerializer } from '@/api/types'
+import StudentExamInfoCard from '@/views/student/components/StudentExamInfoCard.vue'
 
 
 export default Vue.extend({
 export default Vue.extend({
   name: 'ExamList',
   name: 'ExamList',
   components: {
   components: {
     PageHeader,
     PageHeader,
-    ExamInfoCard
+    StudentExamInfoCard
   },
   },
   data: function() {
   data: function() {
     return {
     return {

+ 14 - 7
src/views/student/components/ExamInfoCard.vue → src/views/student/components/StudentExamInfoCard.vue

@@ -8,12 +8,14 @@
         >
         >
       </v-col>
       </v-col>
       <v-col align="end" class="mr-3">
       <v-col align="end" class="mr-3">
-        <v-btn color="primary" class="mx-2">查看详情</v-btn>
+        <v-btn color="primary" class="mx-2" @click="showExamDetail"
+          >查看详情</v-btn
+        >
         <v-btn
         <v-btn
           color="secondary"
           color="secondary"
           class="mx-2"
           class="mx-2"
           v-if="examInfo.joined === true"
           v-if="examInfo.joined === true"
-          @click="onEnterExam"
+          @click="showExamDetail"
           >开始评测</v-btn
           >开始评测</v-btn
         >
         >
         <v-btn color="info" class="mx-2" v-else @click="onJoinExam"
         <v-btn color="info" class="mx-2" v-else @click="onJoinExam"
@@ -69,11 +71,16 @@ export default Vue.extend({
   },
   },
   methods: {
   methods: {
     onJoinExam() {},
     onJoinExam() {},
-    onEnterExam() {
-      const examId = this.examInfo.id
-      this.$router.push({
-        path: `/exam/${examId}/detail`
-      })
+    showExamDetail() {
+      const examId = this.examInfo.id?.toString()
+      if (examId !== undefined) {
+        this.$router.push({
+          name: 'detail',
+          params: {
+            id: examId
+          }
+        })
+      }
     }
     }
   }
   }
 })
 })

+ 311 - 0
src/views/teacher/ExamCreate.vue

@@ -0,0 +1,311 @@
+<template>
+  <v-container>
+    <page-header title="创建考试" :breadcrumb="breadcrumb"> </page-header>
+    <!--创建的表格-->
+    <v-container class="grey lighten-5 pa-6" v-if="!isExamCreated">
+      <v-form ref="form" lazy-validation>
+        <v-text-field
+          v-model="examName"
+          :counter="10"
+          label="考试名称"
+          required
+          class="ma-4"
+        ></v-text-field>
+
+        <v-text-field
+          v-model="position"
+          :counter="10"
+          label="招聘职位"
+          required
+          class="ma-4"
+        ></v-text-field>
+
+        <v-select
+          v-model="skills"
+          :items="skillList"
+          chips
+          label="技能点"
+          multiple
+          outlined
+          class="ma-4"
+        ></v-select>
+
+        <v-subheader class="ma-2 sm">
+          预期难度
+        </v-subheader>
+        <v-slider
+          v-model="difficulty"
+          thumb-label="always"
+          class="ml-4 mr-4 mt-8"
+        ></v-slider>
+
+        <!--开始日期/时间-->
+        <v-row class="ma-4">
+          <v-menu
+            v-model="menu1"
+            :close-on-content-click="false"
+            transition="scale-transition"
+            offset-y
+            max-width="290px"
+            min-width="290px"
+          >
+            <template v-slot:activator="{ on, attrs }">
+              <v-text-field
+                v-model="startDateFormatted"
+                label="开始日期"
+                prepend-icon="mdi-calendar"
+                readonly
+                v-bind="attrs"
+                v-on="on"
+              ></v-text-field>
+            </template>
+            <v-date-picker
+              v-model="startDate"
+              no-title
+              @input="menu1 = false"
+            ></v-date-picker>
+          </v-menu>
+
+          <v-menu
+            v-model="menu2"
+            ref="menu2"
+            :close-on-content-click="false"
+            :nudge-right="40"
+            :return-value.sync="startTime"
+            transition="scale-transition"
+            offset-y
+            max-width="290px"
+            min-width="290px"
+          >
+            <template v-slot:activator="{ on, attrs }">
+              <v-text-field
+                v-model="startTime"
+                label="开始时间"
+                prepend-icon="mdi-clock-time-four-outline"
+                readonly
+                v-bind="attrs"
+                v-on="on"
+              ></v-text-field>
+            </template>
+            <v-time-picker
+              v-if="menu2"
+              v-model="startTime"
+              full-width
+              @click:minute="$refs.menu2.save(startTime)"
+            ></v-time-picker>
+          </v-menu>
+        </v-row>
+
+        <!--结束日期/时间-->
+        <v-row class="ma-4">
+          <v-menu
+            v-model="menu3"
+            :close-on-content-click="false"
+            transition="scale-transition"
+            offset-y
+            max-width="290px"
+            min-width="290px"
+          >
+            <template v-slot:activator="{ on, attrs }">
+              <v-text-field
+                v-model="endDateFormatted"
+                label="结束日期"
+                prepend-icon="mdi-calendar"
+                readonly
+                v-bind="attrs"
+                v-on="on"
+              ></v-text-field>
+            </template>
+            <v-date-picker
+              v-model="endDate"
+              no-title
+              @input="menu3 = false"
+            ></v-date-picker>
+          </v-menu>
+
+          <v-menu
+            v-model="menu4"
+            ref="menu4"
+            :close-on-content-click="false"
+            :nudge-right="40"
+            :return-value.sync="endTime"
+            transition="scale-transition"
+            offset-y
+            max-width="290px"
+            min-width="290px"
+          >
+            <template v-slot:activator="{ on, attrs }">
+              <v-text-field
+                v-model="endTime"
+                label="结束时间"
+                prepend-icon="mdi-clock-time-four-outline"
+                readonly
+                v-bind="attrs"
+                v-on="on"
+              ></v-text-field>
+            </template>
+            <v-time-picker
+              v-if="menu4"
+              v-model="endTime"
+              full-width
+              @click:minute="$refs.menu4.save(endTime)"
+            ></v-time-picker>
+          </v-menu>
+        </v-row>
+
+        <v-text-field
+          v-model="comment"
+          label="备注"
+          class="ma-4 mb-8"
+        ></v-text-field>
+
+        <v-btn color="success" class="ma-4" @click="onCreateExam">
+          提交创建
+        </v-btn>
+      </v-form>
+    </v-container>
+
+    <!--创建成功后显示-->
+    <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-row no-gutters align-content="start">
+          <v-col>
+            <v-card-text class="pa-0 pb-3"
+              >考试邀请码:{{ createdExamInfo.inviteCode }}</v-card-text
+            >
+          </v-col>
+          <v-col>
+            <v-card-text class="pa-0 pb-5"
+              >当前难度值:{{ createdExamInfo.difficulty }}</v-card-text
+            >
+          </v-col>
+        </v-row>
+      </v-card>
+
+      <v-container class="grey lighten-5 pa-6">
+        <v-card-title>样卷</v-card-title>
+      </v-container>
+    </v-container>
+  </v-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import PageHeader from '@/components/PageHeader.vue'
+import { createExam } from '@/api/exam'
+import { ExamSerializer } from '@/api/types'
+
+export default Vue.extend({
+  name: 'ExamCreate',
+  components: {
+    PageHeader
+  },
+
+  data() {
+    return {
+      breadcrumb: [
+        {
+          text: '主页',
+          disabled: false,
+          href: '/'
+        },
+        {
+          text: '创建考试',
+          disabled: true,
+          href: '/'
+        }
+      ],
+      skillList: [
+        'Java',
+        'C++',
+        'Python',
+        'JavaScript',
+        'Go',
+        'MySQL',
+        'Redis',
+        'React',
+        'Vue'
+      ],
+      isExamCreated: false,
+      createdExamInfo: {} as ExamSerializer,
+      menu1: false,
+      menu2: false,
+      menu3: false,
+      menu4: false,
+      examName: '',
+      position: '',
+      skills: [],
+      difficulty: 1,
+      startDate: '',
+      startDateFormatted: '',
+      startTime: '',
+      endDate: '',
+      endDateFormatted: '',
+      endTime: '',
+      comment: ''
+    }
+  },
+  computed: {
+    computedStartDateFormatted(): string {
+      return this.formatDate(this.startDate)
+    },
+    computedEndDateFormatted(): string {
+      return this.formatDate(this.endDate)
+    }
+  },
+
+  watch: {
+    startDate(val) {
+      this.startDateFormatted = this.formatDate(this.startDate)
+    },
+    endDate(val) {
+      this.endDateFormatted = this.formatDate(this.endDate)
+    }
+  },
+
+  methods: {
+    formatDate(date: string): string {
+      if (!date) return ''
+
+      const [year, month, day] = date.split('-')
+      return `${year}-${month}-${day}`
+    },
+    parseDate(date: string): string {
+      if (!date) return ''
+
+      const [month, day, year] = date.split('-')
+      return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
+    },
+    onCreateExam() {
+      const obj = {
+        userId: '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
+      }
+      console.log('创建考试', obj)
+      createExam(obj).then(({ data }) => {
+        console.log(data.res)
+        this.isExamCreated = data.res ? true : false
+        this.createdExamInfo = data.res || {}
+      })
+    }
+  },
+  mounted() {
+    this.startDateFormatted = this.formatDate(
+      new Date().toISOString().substr(0, 10)
+    )
+    this.endDateFormatted = this.formatDate(
+      new Date().toISOString().substr(0, 10)
+    )
+  }
+})
+</script>
+
+<style scoped></style>

+ 105 - 0
src/views/teacher/ExamManage.vue

@@ -0,0 +1,105 @@
+<template>
+  <v-container>
+    <page-header title="评测管理" :breadcrumb="breadcrumb">
+      <template #extra>
+        <v-row class=" my-0 py-0" no-gutters justify="end">
+          <v-spacer></v-spacer>
+          <v-col cols="3">
+            <router-link :to="{ name: 'create' }">
+              <!--            <router-link :to="{ name: 'teacher-home' }">-->
+              <v-btn color="info">
+                创建评测
+              </v-btn>
+            </router-link>
+          </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>
+    <teacher-exam-info-card
+      hover
+      v-for="item in examList"
+      :key="item.id"
+      :exam-info="item"
+    >
+    </teacher-exam-info-card>
+    <v-pagination v-model="page" :length="pageInfo.total" @input="changePage">
+    </v-pagination>
+  </v-container>
+</template>
+
+<script lang="ts">
+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'
+
+export default Vue.extend({
+  name: 'ExamManage',
+  components: {
+    PageHeader,
+    TeacherExamInfoCard
+  },
+  data: function() {
+    return {
+      page: 1,
+      selected: '正在进行',
+      selectItems: ['未开始', '正在进行', '已结束'],
+      dialog: false,
+      breadcrumb: [
+        {
+          text: '主页',
+          disabled: false,
+          href: '/student'
+        },
+        {
+          text: '考试列表',
+          disabled: true,
+          href: '/'
+        }
+      ],
+      examList: [] as Array<ExamSerializer>,
+      pageInfo: {} as PageSerializer
+    }
+  },
+  methods: {
+    changePage() {
+      this.fetchExams()
+    },
+    onCreateExam() {
+      // this.$router.push({ name: 'exam-create' })
+      this.$router.push('/teacher/exam-create')
+    },
+    fetchExams(page: number = 1) {
+      getAllExamList({ page, limit: 10, status: 'READY' })
+        .then(({ data }) => {
+          this.examList = data.res ?? []
+          this.pageInfo = data.page ?? {}
+        })
+        .catch((err) => {
+          this.examList = []
+        })
+    }
+  },
+  mounted() {
+    this.fetchExams()
+  }
+})
+</script>
+
+<style scoped></style>

+ 35 - 0
src/views/teacher/Overview.vue

@@ -0,0 +1,35 @@
+<template>
+  <v-container>
+    <page-header title="Overview" :breadcrumb="breadcrumb"></page-header>
+  </v-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import PageHeader from '@/components/PageHeader.vue'
+
+export default Vue.extend({
+  name: 'Overview',
+  components: {
+    PageHeader
+  },
+  data() {
+    return {
+      breadcrumb: [
+        {
+          text: 'Home',
+          disabled: false,
+          href: '/'
+        },
+        {
+          text: 'Overview',
+          disabled: true,
+          href: '/'
+        }
+      ]
+    }
+  }
+})
+</script>
+
+<style scoped></style>

+ 90 - 0
src/views/teacher/TeacherHome.vue

@@ -0,0 +1,90 @@
+<template>
+  <v-app>
+    <v-navigation-drawer app floating>
+      <div class="flex-column d-flex" style="height: 100vh">
+        <div class="flex-shrink-0">
+          <div class="pa-4">
+            <div
+              style="height: 40px; width: 100%"
+              class="indigo lighten-3 rounded-lg"
+            ></div>
+          </div>
+        </div>
+        <div class="overflow-auto flex-grow-1" style="box-sizing: border-box;">
+          <v-list nav dense>
+            <v-list-item-group color="primary">
+              <v-list-item
+                v-for="(item, i) in linkList"
+                :to="item.url"
+                :exact="item.exact"
+                :key="i"
+              >
+                <v-list-item-icon>
+                  <v-icon v-text="item.icon"></v-icon>
+                </v-list-item-icon>
+
+                <v-list-item-content>
+                  <v-list-item-title v-text="item.name"></v-list-item-title>
+                </v-list-item-content>
+              </v-list-item>
+            </v-list-item-group>
+          </v-list>
+        </div>
+        <div class="flex-shrink-0">
+          <div class="pa-2 pl-6 text--secondary">
+            <v-chip color="primary">Your Name ></v-chip>
+          </div>
+          <div class="pa-2 pl-6 pb-5 text--secondary">All Right Reserve</div>
+        </div>
+      </div>
+      <!--  -->
+    </v-navigation-drawer>
+
+    <v-main class="indigo lighten-5">
+      <router-view />
+    </v-main>
+  </v-app>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import {
+  mdiAccount,
+  mdiBell,
+  mdiViewList,
+  mdiTab,
+  mdiDetails,
+  mdiPackage,
+  mdiHome
+} from '@mdi/js'
+
+export default Vue.extend({
+  name: 'StudentHome',
+  components: {},
+  data: () => ({
+    icons: {
+      mdiAccount,
+      mdiBell
+    },
+    linkList: [
+      {
+        name: '首页',
+        url: '/teacher/home',
+        icon: mdiHome
+      },
+      {
+        name: '个人信息',
+        url: '/teacher/info',
+        icon: mdiAccount
+      },
+      {
+        name: '评测管理',
+        url: '/teacher/exams',
+        icon: mdiViewList
+      }
+    ]
+  })
+})
+</script>
+
+<style scoped></style>

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

@@ -0,0 +1,93 @@
+<template>
+  <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-col>
+      <v-col align="end" class="mr-3">
+        <v-btn color="primary" class="mx-2" @click="showExamDetail"
+          >查看详情</v-btn
+        >
+        <v-btn
+          color="secondary"
+          class="mx-2"
+          v-if="examInfo.status === 'AVAILABLE'"
+          @click="showExamDetail"
+          >延长时间</v-btn
+        >
+        <v-btn color="info" class="mx-2" v-if="examInfo.status === 'READY'"
+          >修改评测</v-btn
+        >
+      </v-col>
+    </v-row>
+    <v-row justify="space-between" dense>
+      <v-col>
+        <v-card-text class="pa-0 pb-5">ID: {{ examInfo.id }}</v-card-text>
+      </v-col>
+      <v-col>
+        <v-card-text class="pa-0 pb-5"
+          >邀请码: {{ examInfo.inviteCode }}</v-card-text
+        >
+      </v-col>
+      <v-col>
+        <v-card-text class="pa-0 pb-5"
+          >评测时长: {{ examInfo.lastTime }} 小时</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
+        >
+      </v-col>
+      <v-col>
+        <v-card-text class="pa-0 pb-5"
+          >结束时间: {{ examInfo.endAt }}</v-card-text
+        >
+      </v-col>
+      <v-col>
+        <v-card-text class="pa-0 pb-5"
+          >参加人数: {{ examInfo.participantsNumber }}</v-card-text
+        >
+      </v-col>
+    </v-row>
+  </v-card>
+</template>
+
+<script lang="ts">
+import Vue, { PropType } from 'vue'
+import { ExamSerializer } from '@/api/types'
+
+export default Vue.extend({
+  name: 'ExamInfoCard',
+  data() {
+    return {}
+  },
+  props: {
+    examInfo: {
+      type: Object as PropType<ExamSerializer>,
+      required: true
+    }
+  },
+  methods: {
+    onJoinExam() {},
+    showExamDetail() {
+      const examId = this.examInfo.id?.toString()
+      if (examId !== undefined) {
+        this.$router.push({
+          name: 'detail',
+          params: {
+            id: examId
+          }
+        })
+      }
+    }
+  }
+})
+</script>
+
+<style scoped></style>

+ 22 - 0
yarn.lock

@@ -1036,6 +1036,11 @@
   resolved "https://registry.npm.taobao.org/@types/json-schema/download/@types/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
   resolved "https://registry.npm.taobao.org/@types/json-schema/download/@types/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
   integrity sha1-9MfsQ+gbMZqYFRFQMXCfJph4kfA=
   integrity sha1-9MfsQ+gbMZqYFRFQMXCfJph4kfA=
 
 
+"@types/marked@^1.2.1":
+  version "1.2.1"
+  resolved "https://registry.npm.taobao.org/@types/marked/download/@types/marked-1.2.1.tgz#9864ecc10d0edb72e7be4e94acb5fcc607c15eed"
+  integrity sha1-mGTswQ0O23Lnvk6UrLX8xgfBXu0=
+
 "@types/mime@*":
 "@types/mime@*":
   version "2.0.3"
   version "2.0.3"
   resolved "https://registry.npm.taobao.org/@types/mime/download/@types/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
   resolved "https://registry.npm.taobao.org/@types/mime/download/@types/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
@@ -1076,6 +1081,13 @@
   resolved "https://registry.npm.taobao.org/@types/qs/download/@types/qs-6.9.5.tgz?cache=0&sync_timestamp=1605055634970&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fqs%2Fdownload%2F%40types%2Fqs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b"
   resolved "https://registry.npm.taobao.org/@types/qs/download/@types/qs-6.9.5.tgz?cache=0&sync_timestamp=1605055634970&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fqs%2Fdownload%2F%40types%2Fqs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b"
   integrity sha1-Q0cRvdSete5p2QwdZ8NUqajssYs=
   integrity sha1-Q0cRvdSete5p2QwdZ8NUqajssYs=
 
 
+"@types/ramda@^0.27.34":
+  version "0.27.34"
+  resolved "https://registry.npm.taobao.org/@types/ramda/download/@types/ramda-0.27.34.tgz#08505be193a1ac9d9de0411fbf5aa0d9e817fd9a"
+  integrity sha1-CFBb4ZOhrJ2d4EEfv1qg2egX/Zo=
+  dependencies:
+    ts-toolbelt "^6.15.1"
+
 "@types/range-parser@*":
 "@types/range-parser@*":
   version "1.2.3"
   version "1.2.3"
   resolved "https://registry.npm.taobao.org/@types/range-parser/download/@types/range-parser-1.2.3.tgz?cache=0&sync_timestamp=1605055635232&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Frange-parser%2Fdownload%2F%40types%2Frange-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
   resolved "https://registry.npm.taobao.org/@types/range-parser/download/@types/range-parser-1.2.3.tgz?cache=0&sync_timestamp=1605055635232&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Frange-parser%2Fdownload%2F%40types%2Frange-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
@@ -6825,6 +6837,11 @@ path-to-regexp@0.1.7:
   resolved "https://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
   resolved "https://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
   integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
   integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
 
 
+path-to-regexp@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38"
+  integrity sha1-97OAMzYQTDRoia3s5hRmkjBkXzg=
+
 path-type@^3.0.0:
 path-type@^3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.npm.taobao.org/path-type/download/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
   resolved "https://registry.npm.taobao.org/path-type/download/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@@ -8726,6 +8743,11 @@ ts-pnp@^1.1.6:
   resolved "https://registry.npm.taobao.org/ts-pnp/download/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
   resolved "https://registry.npm.taobao.org/ts-pnp/download/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
   integrity sha1-pQCtCEsHmPHDBxrzkeZZEshrypI=
   integrity sha1-pQCtCEsHmPHDBxrzkeZZEshrypI=
 
 
+ts-toolbelt@^6.15.1:
+  version "6.15.5"
+  resolved "https://registry.npm.taobao.org/ts-toolbelt/download/ts-toolbelt-6.15.5.tgz?cache=0&sync_timestamp=1601987713799&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fts-toolbelt%2Fdownload%2Fts-toolbelt-6.15.5.tgz#cb3b43ed725cb63644782c64fbcad7d8f28c0a83"
+  integrity sha1-yztD7XJctjZEeCxk+8rX2PKMCoM=
+
 tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
 tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
   version "1.14.1"
   version "1.14.1"
   resolved "https://registry.npm.taobao.org/tslib/download/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   resolved "https://registry.npm.taobao.org/tslib/download/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"