瀏覽代碼

接入门户+参加考试逻辑

ChengYuXuan 5 年之前
父節點
當前提交
7e5a39ef7e

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

@@ -61,12 +61,15 @@ export default function axiosConfig() {
   )
 }
 
+axiosConfig()
+
 // const portal = process.env.portalUrl
 const portal = 'http://p.seecoder.cn'
 
 export const getLoginUrl = () => {
+  console.log(portal)
   const location = window.location
-  return `${portal}/api/interaction?client_id=seec-eval&redirect_uri=${encodeURIComponent(
+  return `${portal}/api/interaction?client_id=seec-mooc&redirect_uri=${encodeURIComponent(
     location.origin + '/'
   )}&scope=openid`
 }

+ 0 - 3
src/main.ts

@@ -3,11 +3,8 @@ import App from './App.vue'
 import router from './router'
 import store from './store'
 import vuetify from './plugins/vuetify'
-import axiosConfig from '@/api/axios.config'
 Vue.config.productionTip = false
 
-axiosConfig()
-
 new Vue({
   router,
   store,

+ 1 - 1
src/router/exam.ts

@@ -2,7 +2,7 @@ import { RouteConfig } from 'vue-router'
 
 const routes: Array<RouteConfig> = [
   {
-    path: '/exam/:id',
+    path: '/exam/:examId',
     name: 'exam',
     component: () =>
       import(/* webpackChunkName: "exam" */ '@/views/exam/ExamBasicLayout.vue'),

+ 3 - 3
src/router/index.ts

@@ -29,9 +29,9 @@ router.beforeEach((to, from, next) => {
   if (id_token) {
     store.commit('setToken', id_token)
   }
-  // if (!store.getters.isLogin) {
-  //   jumpToLogin()
-  // }
+  if (!store.getters.isLogin) {
+    jumpToLogin()
+  }
   next()
 })
 

+ 2 - 2
src/router/student.ts

@@ -13,7 +13,7 @@ const routes: Array<RouteConfig> = [
     children: [
       {
         path: 'home',
-        name: 'home',
+        name: 'student-home',
         component: () =>
           import(
             /* webpackChunkName: "student" */ '@/views/student/Overview.vue'
@@ -34,7 +34,7 @@ const routes: Array<RouteConfig> = [
           )
       },
       {
-        path: 'result/:id',
+        path: 'result/:examId',
         name: 'result',
         component: () =>
           import(/*webpackChunkName: "exam"*/ '@/views/exam/PersonalResult.vue')

+ 1 - 0
src/router/teacher.ts

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

+ 38 - 23
src/views/Home.vue

@@ -1,40 +1,55 @@
 <template>
   <v-app>
-    <Login></Login>
-    <Register></Register>
-    <router-link :to="{ name: 'student' }">
-      <v-btn color="primary">student</v-btn>
-    </router-link>
-    <router-link :to="{ name: 'teacher' }">
-      <v-btn color="primary">teacher</v-btn>
-    </router-link>
+    <!--    <router-link :to="{ name: 'student' }">-->
+    <!--      <v-btn color="primary">student</v-btn>-->
+    <!--    </router-link>-->
+    <!--    <router-link :to="{ name: 'teacher' }">-->
+    <!--      <v-btn color="primary">teacher</v-btn>-->
+    <!--    </router-link>-->
   </v-app>
 </template>
 
 <script lang="ts">
 import Vue from 'vue'
-import Login from '@/views/portal/Login.vue'
-import Register from '@/views/portal/Register.vue'
-import { mapMutations } from 'vuex'
+
+import { mapGetters, mapMutations } from 'vuex'
+import { getUserInfo } from '@/api/user'
+import { jumpToLogin } from '@/api/axios.config'
 export default Vue.extend({
   name: 'Home',
-  components: {
-    Login,
-    Register
-  },
   data() {
     return {}
   },
   methods: {
-    ...mapMutations(['setToken', 'setUser'])
+    ...mapMutations(['setToken', 'setUser']),
+    ...mapGetters(['isStudent', 'isTeacher'])
   },
-  beforeRouteLeave(to, from, next) {
-    const { id_token } = to.query
-    if (id_token) {
-      this.setToken(id_token)
-    }
-
-    next()
+  // beforeRouteLeave(to, from, next) {
+  //   const { id_token } = to.query
+  //   if (id_token) {
+  //     this.setToken(id_token)
+  //   }
+  //
+  //   next()
+  // },
+  mounted() {
+    getUserInfo()
+      .then(({ data }) => {
+        this.setUser(data.result)
+        if (this.isStudent()) {
+          this.$router.push({
+            name: 'student'
+          })
+        }
+        if (this.isTeacher()) {
+          this.$router.push({
+            name: 'teacher'
+          })
+        }
+      })
+      .catch(() => {
+        jumpToLogin()
+      })
   }
 })
 </script>

+ 74 - 0
src/views/components/LeftDrawer.vue

@@ -0,0 +1,74 @@
+<template>
+  <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 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;">
+        <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">
+          <v-chip color="primary">{{ userName }}</v-chip>
+        </div>
+        <v-btn class="pa-2 pl-8 pb-5 text--secondary" @click="logout" plain
+          >退出登录</v-btn
+        >
+      </div>
+    </div>
+    <!--  -->
+  </v-navigation-drawer>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import { jumpToLogin } from '@/api/axios.config'
+
+export default Vue.extend({
+  name: 'LeftDrawer',
+  data() {
+    return {}
+  },
+  props: {
+    linkList: {
+      type: Array,
+      required: true
+    },
+    userName: {
+      type: String,
+      required: true
+    }
+  },
+  methods: {
+    logout() {
+      jumpToLogin()
+    }
+  }
+})
+</script>
+
+<style scoped></style>

+ 17 - 27
src/views/exam/BeforeExam.vue

@@ -1,19 +1,10 @@
 <template>
   <v-container>
-    <page-header title="评测详情" :breadcrumb="breadcrumb">
+    <page-header title="考前确认" :breadcrumb="breadcrumb">
       <template #extra>
         <v-row justify="end">
           <v-spacer></v-spacer>
-          <v-col cols="3">
-            <v-btn
-              v-if="!joined"
-              color="info"
-              @click="onJoinExam"
-              :loading="joining"
-              >报名参加</v-btn
-            >
-          </v-col>
-          <v-col cols="3">
+          <v-col cols="3" v-if="canEnter">
             <router-link
               :to="{ name: 'exam-pane' }"
               style="text-decoration: none;"
@@ -40,7 +31,7 @@
       <v-card-subtitle class="text-h5 black--text">评测规则</v-card-subtitle>
       <v-card-text>{{ examInfo.comment }}</v-card-text>
       <v-card-subtitle class="text-h5 black--text">评测内容</v-card-subtitle>
-      <v-card-text class="text-body-1 black--text">
+      <v-card-text class="text-body-1 black--text" v-if="examQuestions">
         客观题 {{ Object.keys(examQuestions.questionAndScore).length }} 道
         编程题 {{}} 道
       </v-card-text>
@@ -53,7 +44,8 @@ import Vue from 'vue'
 import PageHeader from '@/components/PageHeader.vue'
 import { ExamQuestionSerializer, ExamSerializer } from '@/api/types'
 import { getExamById, getExamQuestions } from '@/api/exam'
-import { timeLength } from '@/utils/common'
+import { TIME_FORMAT, timeLength } from '@/utils/common'
+import dayjs from 'dayjs'
 
 export default Vue.extend({
   name: 'BeforeExam',
@@ -81,16 +73,13 @@ export default Vue.extend({
         }
       ],
       examInfo: (null as unknown) as ExamSerializer,
-      joining: false,
-      joined: false,
       examLength: '',
-      examQuestions: (null as unknown) as ExamQuestionSerializer
+      examQuestions: (null as unknown) as ExamQuestionSerializer,
+      canEnter: false
     }
   },
   mounted() {
-    this.joined = this.$route.params.joined === 'true'
-
-    const examId = this.$route.params.id
+    const examId = this.$route.params.examId
 
     getExamQuestions(examId)
       .then(({ data }) => {
@@ -108,17 +97,18 @@ export default Vue.extend({
           this.examInfo.startTime,
           this.examInfo.endTime
         )
+
+        const now = dayjs()
+        const endTime = dayjs(this.examInfo.endTime, TIME_FORMAT)
+        const startTime = dayjs(this.examInfo.startTime, TIME_FORMAT)
+
+        if (now >= startTime && now < endTime) {
+          this.canEnter = true
+        }
       })
       .catch((err) => {})
   },
-  methods: {
-    onJoinExam() {
-      this.joining = true
-      setTimeout(() => {
-        this.joining = false
-      }, 1000)
-    }
-  }
+  methods: {}
 })
 </script>
 

+ 39 - 22
src/views/exam/ExamPane.vue

@@ -26,8 +26,8 @@
           class="flex-shrink-0 flex-column d-flex justify-space-between align-center"
         >
           <count-down
-            :current-time="currentTime"
-            :end-time="endTime"
+            :currentTime="currentTime"
+            :endTime="endTime"
             @timesUp="onTimesUp"
           ></count-down>
         </div>
@@ -153,7 +153,9 @@
             v-if="currentQuestionIndex >= 0"
             class="d-flex justify-space-around px-10 py-10"
           >
-            <v-btn color="info" @click="nextQuestion">下一题</v-btn>
+            <v-btn color="info" @click="nextQuestion" :disabled="fetchingStem"
+              >下一题</v-btn
+            >
           </div>
         </v-card>
 
@@ -181,8 +183,8 @@
       <v-snackbar v-model="finished" absolute>
         已到最后一题
       </v-snackbar>
-      <v-snackbar v-model="examTimesUp" absolute>
-        已到最后一题
+      <v-snackbar v-model="submitting" absolute centered>
+        <span v-if="examTimesUp">考试结束</span> 提交试卷中...
       </v-snackbar>
     </v-main>
   </div>
@@ -204,6 +206,8 @@ import {
 import { getExamById, getExamQuestions } from '@/api/exam'
 import { getQuestionStem } from '@/api/question'
 import { objectiveJudge } from '@/api/judge'
+import dayjs from 'dayjs'
+import { TIME_FORMAT } from '@/utils/common'
 
 export default Vue.extend({
   name: 'ExamPane',
@@ -289,30 +293,41 @@ export default Vue.extend({
       this.objectiveAnswers[this.currentQuestionIndex] = answer
       console.log(answer)
     },
-    // onSelectQuestion(target: number) {
-    //   this.currentQuestionIndex = target - 1
-    // },
     leaveExam() {
       this.exitDialog = false
       this.$router.back()
     },
     submitAnswer() {
       //TODO code
+      this.submitDialog = false
       this.submitting = true
-
       console.log(this.objectiveAnswers)
       console.log(this.questionIds)
 
-      objectiveJudge(this.objectiveAnswers, this.questionIds).then(() => {
-        this.submitting = false
-        this.submitDialog = false
-        this.$router.push({
-          name: 'result',
-          params: {
-            id: this.examId.toString()
-          }
+      objectiveJudge(this.objectiveAnswers, this.questionIds)
+        .then(() => {
+          setTimeout(() => {
+            this.$router.push({
+              name: 'result',
+              params: {
+                examId: this.examId.toString()
+              }
+            })
+          }, 1000)
         })
-      })
+        .catch(() => {
+          objectiveJudge(this.objectiveAnswers, this.questionIds).then(() => {
+            setTimeout(() => {
+              this.$router.push({
+                name: 'result',
+                params: {
+                  examId: this.examId.toString()
+                }
+              })
+            }, 1000)
+          })
+        })
+        .catch(() => {})
     },
     nextQuestion() {
       if (this.currentQuestionIndex < this.totalQuestionNum - 1) {
@@ -326,13 +341,15 @@ export default Vue.extend({
       }
     },
     fetchData() {
-      this.examId = parseInt(this.$route.params.id)
+      this.examId = parseInt(this.$route.params.examId)
 
       getExamById(this.examId).then(({ data }) => {
         this.examInfo = data.result
-        this.endTime = new Date().getTime() + 1000
-        // this.endTime = Date.parse(this.examInfo.endTime ?? '')
-        this.currentTime = new Date().getTime()
+        this.endTime = dayjs(this.examInfo.endTime, TIME_FORMAT).valueOf()
+        this.currentTime = dayjs().valueOf()
+
+        if (this.currentTime >= this.endTime) {
+        }
       })
 
       getExamQuestions(this.examId).then(({ data }) => {

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

@@ -6,7 +6,7 @@
   >
     <div class="d-flex">
       <slot name="index"></slot>
-      <v-card-title>填空题</v-card-title>
+      <v-card-title>填空题<v-icon>mdi-book-open</v-icon></v-card-title>
     </div>
     <markdown-text :raw="question.stem" class="pa-10"></markdown-text>
     <v-spacer></v-spacer>

+ 13 - 8
src/views/exam/components/ChoicePane.vue

@@ -6,8 +6,12 @@
   >
     <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>
+      <v-card-title v-if="question.type === 'choice'"
+        >单选题<v-icon>mdi-radiobox-marked</v-icon></v-card-title
+      >
+      <v-card-title v-else
+        >多选题<v-icon>mdi-checkbox-marked</v-icon></v-card-title
+      >
     </div>
     <markdown-text :raw="question.stem" class="pa-10"></markdown-text>
     <v-list class="px-10 flex" v-if="!loading">
@@ -89,8 +93,13 @@ export default Vue.extend({
       if (!this.answer || this.answer.length === 0) {
         return ''
       }
-      const sorted = clone(this.answer).sort()
-      return sorted.join('')
+
+      if (this.question.type === 'choice') {
+        return this.answer[0]
+      } else {
+        const sorted = clone(this.answer).sort()
+        return sorted.join('')
+      }
     }
   },
   mounted() {
@@ -106,10 +115,6 @@ export default Vue.extend({
     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++) {

+ 261 - 260
src/views/exam/components/CodePane.vue

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

+ 54 - 28
src/views/exam/components/CountDown.vue

@@ -1,9 +1,14 @@
 <template>
   <span>
     <v-chip label> 离结束还有:{{ content }} </v-chip>
+    <v-snackbar v-model="endTimeChanged">考试已延长</v-snackbar>
   </span>
 </template>
 <script>
+import dayjs from 'dayjs'
+import { TIME_FORMAT } from '@/utils/common'
+import { getExamById } from '@/api/exam'
+
 export default {
   props: {
     currentTime: {
@@ -21,9 +26,12 @@ export default {
   },
   data() {
     return {
-      content: '',
-      end: '', //  结束时间
-      current: '' // 当前时间
+      content: '加载中...',
+      end: '', //  结束时间 unix second
+      current: '', // 当前时间 unix second,
+      timer: null,
+      extendTime: '',
+      endTimeChanged: false
     }
   },
   watch: {
@@ -36,40 +44,53 @@ export default {
   },
   methods: {
     startCount() {
-      if (this.currentTime === 0) {
-        this.content = '加载中...'
-      } else {
-        this.current = new Date().getTime() / 1000
-        // this.current = this.currentTime
-        this.end = this.current + 500
+      if (this.currentTime !== 0) {
+        this.current = this.currentTime / 1000
+        this.end = this.endTime / 1000
+        // this.end = this.current + 50
         this.countDown()
       }
     },
     countDown() {
+      const examId = this.$route.params.examId
       const self = this
       const timer = setInterval(function() {
-        const realTime = new Date().getTime() / 1000
-        const nowTime = new Date(self.current * 1000)
-        const endTime = new Date(self.end * 1000)
-        const t = endTime.getTime() - nowTime.getTime()
+        const t = self.end - self.current
+
         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}秒`
+          const duration = dayjs.duration(t, 'second')
+          if (
+            duration.get('minute') % 5 === 0 &&
+            duration.get('second') === 0
+          ) {
+            getExamById(examId)
+              .then(({ data }) => {
+                const newEndTime = data.result.endTime
+                const newEnd = dayjs(newEndTime, TIME_FORMAT).unix()
+                if (newEnd !== self.end) {
+                  self.extendTime =
+                    dayjs
+                      .duration(newEnd - self.end, 'second')
+                      .format(TIME_FORMAT)
+                      .substr(14, 2) + '分钟'
+                  self.end = newEnd
+                  self.endTimeChanged = true
+                }
+              })
+              .catch()
           }
-          if (day <= 0 && hour <= 0) {
-            format = `${min}分${sec}秒`
+
+          const day = duration.get('day')
+          let format =
+            duration
+              .format(TIME_FORMAT)
+              .substr(11)
+              .replace(':', '小时')
+              .replace(':', '分钟') + '秒'
+          if (day > 0) {
+            format = `${day}天` + format
           }
+
           self.content = format
           self.current = self.current + 1
         } else {
@@ -78,7 +99,12 @@ export default {
           self.$emit('timesUp')
         }
       }, 1000)
+
+      this.timer = timer
     }
+  },
+  beforeDestroy() {
+    clearInterval(this.timer)
   }
 }
 </script>

+ 0 - 12
src/views/portal/Login.vue

@@ -1,12 +0,0 @@
-<template>
-  <div>LOGIN</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue'
-export default Vue.extend({
-  name: 'Login'
-})
-</script>
-
-<style scoped></style>

+ 0 - 12
src/views/portal/Register.vue

@@ -1,12 +0,0 @@
-<template>
-  <div>REGISTER</div>
-</template>
-
-<script lang="ts">
-import Vue from 'vue'
-export default Vue.extend({
-  name: 'Register'
-})
-</script>
-
-<style scoped></style>

+ 17 - 2
src/views/student/Overview.vue

@@ -19,7 +19,13 @@
           <!--            ></v-select>-->
           <!--          </v-col>-->
           <!--          <v-col cols="1" class="ml-5">-->
-          <v-btn color="secondary" fab small>
+          <v-btn
+            color="secondary"
+            fab
+            small
+            @click="refreshState"
+            :loading="fetching"
+          >
             <v-icon dark>mdi-cached</v-icon>
           </v-btn>
           <!--          </v-col>-->
@@ -98,10 +104,15 @@ export default Vue.extend({
       pageSize: 5,
       inviteCode: '',
       joinSuccess: false,
-      joinFailed: false
+      joinFailed: false,
+      fetching: false
     }
   },
   methods: {
+    refreshState() {
+      this.page = 1
+      this.fetchExams()
+    },
     changePage() {
       this.fetchExams()
     },
@@ -110,6 +121,7 @@ export default Vue.extend({
       this.dialog = true
     },
     fetchExams() {
+      this.fetching = true
       getAllExamList({ pageNum: this.page, pageSize: this.pageSize })
         .then(({ data }) => {
           this.examList = data.result.examVOList
@@ -118,6 +130,9 @@ export default Vue.extend({
         .catch((err) => {
           this.examList = []
         })
+        .finally(() => {
+          this.fetching = false
+        })
     }
   },
   mounted() {

+ 14 - 3
src/views/student/StudentExamList.vue

@@ -20,7 +20,13 @@
           </v-col>
 
           <v-col cols="1" class="ml-5">
-            <v-btn color="secondary" fab small @click="refreshState">
+            <v-btn
+              color="secondary"
+              fab
+              small
+              @click="refreshState"
+              :loading="fetching"
+            >
               <v-icon dark>mdi-cached</v-icon>
             </v-btn>
           </v-col>
@@ -78,7 +84,7 @@ export default Vue.extend({
         }
       ],
       examList: [] as Array<ExamSerializer>,
-      windowSwitch: 0
+      fetching: false
     }
   },
   methods: {
@@ -91,8 +97,10 @@ export default Vue.extend({
     },
     onJoinExam() {},
     fetchExams() {
+      this.fetching = true
+
       getExamsByStudentId(
-        { pageNum: this.page, pageSize: 10 },
+        { pageNum: this.page, pageSize: this.pageSize },
         this.selectItems.findIndex((val) => {
           return this.selected === val
         }) as ExamState
@@ -109,6 +117,9 @@ export default Vue.extend({
 
           this.examList = []
         })
+        .finally(() => {
+          this.fetching = false
+        })
     }
   },
   mounted() {

+ 45 - 42
src/views/student/StudentHome.vue

@@ -1,47 +1,47 @@
 <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 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;">
-          <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">{{ userName }}</v-chip>
-          </div>
-          <div class="pa-2 pl-6 pb-5 text--secondary">All Right Reserve</div>
-        </div>
-      </div>
-      <!--  -->
-    </v-navigation-drawer>
+    <!--    <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 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;">-->
+    <!--          <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&#45;&#45;secondary">-->
+    <!--            <v-chip color="primary">{{ userName }}</v-chip>-->
+    <!--          </div>-->
+    <!--          <div class="pa-2 pl-6 pb-5 text&#45;&#45;secondary">All Right Reserve</div>-->
+    <!--        </div>-->
+    <!--      </div>-->
+    <!--      &lt;!&ndash;  &ndash;&gt;-->
+    <!--    </v-navigation-drawer>-->
+    <left-drawer :link-list="linkList" :user-name="userName"></left-drawer>
     <v-main class="indigo lighten-5">
       <router-view />
     </v-main>
@@ -62,10 +62,13 @@ import {
 import { getUserInfo } from '@/api/user'
 import { UserSerializer } from '@/api/types'
 import { mapMutations } from 'vuex'
+import LeftDrawer from '@/views/components/LeftDrawer.vue'
 
 export default Vue.extend({
   name: 'StudentHome',
-  components: {},
+  components: {
+    LeftDrawer
+  },
   data: () => ({
     userName: '',
     userInfo: (null as unknown) as UserSerializer,

+ 3 - 4
src/views/student/components/StudentExamInfoCard.vue

@@ -84,8 +84,7 @@ export default Vue.extend({
   name: 'StudentExamInfoCard',
   data() {
     return {
-      state: (null as unknown) as ExamStateText,
-      timeFormat: 'YYYY-MM-DD HH:mm:ss'
+      state: (null as unknown) as ExamStateText
     }
   },
   props: {
@@ -120,7 +119,7 @@ export default Vue.extend({
       this.$router.push({
         name: 'result',
         params: {
-          id: examId
+          examId: examId
         }
       })
     },
@@ -130,7 +129,7 @@ export default Vue.extend({
         this.$router.push({
           name: 'before-exam',
           params: {
-            id: examId,
+            examId: examId,
             joined: 'true'
           }
         })

+ 18 - 4
src/views/teacher/EditExam.vue

@@ -79,6 +79,9 @@
         </v-tabs-items>
       </v-container>
     </v-container>
+    <v-snackbar v-model="success">
+      设置样卷成功
+    </v-snackbar>
   </v-container>
 </template>
 
@@ -115,6 +118,7 @@ export default Vue.extend({
       sampleQuestions: [] as Array<Array<ExtendedQuestionSerializer>>,
       fetching: false,
       isSampleSet: false,
+      success: false,
       breadcrumb: [
         {
           text: '主页',
@@ -166,16 +170,26 @@ export default Vue.extend({
       }
     },
     onSelectSample(sampleId: ID) {
-      setExamPaperFromSample(this.examInfo.examId ?? '', sampleId)
+      setExamPaperFromSample(this.examInfo.examId ?? '', sampleId).then(() => {
+        this.success = true
+
+        setTimeout(() => {
+          this.$router.push({
+            name: 'teacher'
+          })
+        }, 1000)
+      })
     }
   },
 
   mounted() {
     const examId = this.$route.params.examId
 
-    getExamById(examId).then(({ data }) => {
-      this.examInfo = data.result
-    })
+    getExamById(examId)
+      .then(({ data }) => {
+        this.examInfo = data.result
+      })
+      .catch(() => {})
 
     getExamQuestions(examId ?? '')
       .then(({ data }) => {

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

@@ -64,6 +64,7 @@
               v-model="startDate"
               no-title
               @input="menu1 = false"
+              :min="minStartDate"
             ></v-date-picker>
           </v-menu>
 
@@ -94,6 +95,8 @@
               v-model="startTime"
               full-width
               @click:minute="$refs.menu2.save(startTime)"
+              :min="minStartTime"
+              format="24hr"
             ></v-time-picker>
           </v-menu>
         </v-row>
@@ -154,6 +157,7 @@ import { createExam } from '@/api/exam'
 import { ExamSerializer, ID } from '@/api/types'
 import { mapGetters } from 'vuex'
 import dayjs from 'dayjs'
+import { TIME_FORMAT } from '@/utils/common'
 
 export default Vue.extend({
   name: 'ExamCreate',
@@ -214,7 +218,7 @@ export default Vue.extend({
       valid: true,
       nameRules: [
         (v: string) => !!v || '该项为必填项',
-        (v: string) => v.length < 20 || '字数过多'
+        (v: string) => v.length <= 20 || '字数过多'
       ],
       arrayRules: [(v: [] | string) => v.length > 0 || '该项为必填项'],
       examLengthTicks: [
@@ -226,7 +230,8 @@ export default Vue.extend({
         '3 小时'
       ],
       examLength: 0,
-      timeFormat: 'YYYY-MM-DD HH:mm:ss'
+      minStartTime: '',
+      minStartDate: ''
     }
   },
   computed: {
@@ -246,7 +251,7 @@ export default Vue.extend({
     computedExamEndTime(): string {
       const startTime = dayjs(
         this.startDateFormatted + ' ' + this.startTime + ':00',
-        this.timeFormat
+        TIME_FORMAT
       )
 
       const examLength = Number.parseFloat(
@@ -254,7 +259,7 @@ export default Vue.extend({
       )
       const endTime = startTime.add(examLength, 'h')
 
-      return endTime.format(this.timeFormat)
+      return endTime.format(TIME_FORMAT)
     }
   },
 
@@ -313,9 +318,11 @@ export default Vue.extend({
     }
   },
   mounted() {
-    this.startDateFormatted = dayjs()
-      .format(this.timeFormat)
-      .substr(0, 10)
+    const now = dayjs()
+    this.startDateFormatted = now.format(TIME_FORMAT).substr(0, 10)
+
+    this.minStartTime = now.add(5, 'minute').format('HH:mm') // bad
+    this.minStartDate = this.startDateFormatted
   }
 })
 </script>

+ 20 - 7
src/views/teacher/ExamManage.vue

@@ -23,7 +23,13 @@
           </v-col>
 
           <v-col cols="1" class="ml-5">
-            <v-btn color="secondary" fab small @click="refreshState">
+            <v-btn
+              color="secondary"
+              fab
+              small
+              @click="refreshState"
+              :loading="fetching"
+            >
               <v-icon dark>mdi-cached</v-icon>
             </v-btn>
           </v-col>
@@ -43,6 +49,7 @@
       @input="changePage"
     >
     </v-pagination>
+    <v-edit-dialog v-model="extendTimeDialog"></v-edit-dialog>
   </v-container>
 </template>
 
@@ -67,8 +74,7 @@ export default Vue.extend({
       pageSize: 5,
       selected: '所有评测',
       selectItems: ['所有评测', '未开始', '进行中', '已结束'],
-      dialog: false,
-
+      extendTimeDialog: false,
       breadcrumb: [
         {
           text: '主页',
@@ -81,7 +87,8 @@ export default Vue.extend({
           href: '/'
         }
       ],
-      examList: [] as Array<ExamSerializer>
+      examList: [] as Array<ExamSerializer>,
+      fetching: false
     }
   },
   methods: {
@@ -94,12 +101,12 @@ export default Vue.extend({
       this.fetchExams()
     },
     onCreateExam() {
-      // this.$router.push({ name: 'exam-create' })
-      this.$router.push('/teacher/exam-create')
+      this.$router.push({ name: 'create' })
     },
     fetchExams() {
+      this.fetching = true
       getExamsByTeacherId(
-        { pageNum: this.page, pageSize: 5 },
+        { pageNum: this.page, pageSize: this.pageSize },
         this.selectItems.findIndex((val) => {
           return this.selected === val
         }) as ExamState
@@ -111,6 +118,12 @@ export default Vue.extend({
         .catch((err) => {
           this.examList = []
         })
+        .finally(() => {
+          this.fetching = false
+        })
+    },
+    onExtendTime() {
+      this.extendTimeDialog = true
     }
   },
   mounted() {

+ 3 - 8
src/views/teacher/Overview.vue

@@ -1,6 +1,6 @@
 <template>
   <v-container>
-    <page-header title="Overview" :breadcrumb="breadcrumb"></page-header>
+    <page-header title="主页" :breadcrumb="breadcrumb"></page-header>
   </v-container>
 </template>
 
@@ -17,14 +17,9 @@ export default Vue.extend({
     return {
       breadcrumb: [
         {
-          text: 'Home',
-          disabled: false,
-          href: '/teacher'
-        },
-        {
-          text: 'Overview',
+          text: '主页',
           disabled: true,
-          href: '/'
+          href: '/teacher'
         }
       ]
     }

+ 30 - 16
src/views/teacher/TeacherExamDetail.vue

@@ -2,8 +2,8 @@
   <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>
+      <div class="text-center display-1">试卷</div>
+      <div v-if="!fetching">
         <v-row
           v-for="question in examQuestions"
           :key="question.id"
@@ -20,6 +20,11 @@
           </v-col>
         </v-row>
       </div>
+      <v-progress-circular
+        v-else
+        indeterminate
+        class="align-self-center"
+      ></v-progress-circular>
     </v-container>
     <v-container class="grey lighten-5 pa-6 d-flex flex-column mt-5">
       <div class="text-center display-1">评测名单</div>
@@ -89,24 +94,33 @@ export default Vue.extend({
   mounted() {
     const examId = this.$route.params.examId
 
-    getExamQuestions(examId).then(({ data }) => {
-      this.fetching = true
+    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])
-      })
+        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
+        Promise.all(questionsPromises).then((reps) => {
+          this.examQuestions = reps.map((value) => {
+            return value.data.result
+          })
+          this.fetching = false
+        })
+      })
+      .catch(() => {
+        this.$router.push({
+          name: 'edit-exams',
+          params: {
+            examId: examId
+          }
         })
-        this.fetching = false
       })
-    })
 
     getExamJoinedUsers(examId).then(({ data }) => {
       this.joinedUsers = data.result

+ 47 - 46
src/views/teacher/TeacherHome.vue

@@ -1,45 +1,45 @@
 <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">{{ userName }}</v-chip>
-          </div>
-          <div class="pa-2 pl-6 pb-5 text--secondary">All Right Reserve</div>
-        </div>
-      </div>
-      <!--  -->
-    </v-navigation-drawer>
+    <!--    <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&#45;&#45;secondary">-->
+    <!--            <v-chip color="primary">{{ userName }}</v-chip>-->
+    <!--          </div>-->
+    <!--          <div class="pa-2 pl-6 pb-5 text&#45;&#45;secondary">All Right Reserve</div>-->
+    <!--        </div>-->
+    <!--      </div>-->
+    <!--      &lt;!&ndash;  &ndash;&gt;-->
+    <!--    </v-navigation-drawer>-->
+    <left-drawer :link-list="linkList" :user-name="userName"></left-drawer>
     <v-main class="indigo lighten-5">
       <router-view />
     </v-main>
@@ -60,10 +60,11 @@ import {
 import { mapMutations } from 'vuex'
 import { getUserInfo } from '@/api/user'
 import { UserSerializer } from '@/api/types'
+import LeftDrawer from '@/views/components/LeftDrawer.vue'
 
 export default Vue.extend({
   name: 'StudentHome',
-  components: {},
+  components: { LeftDrawer },
   data: () => ({
     icons: {
       mdiAccount,
@@ -71,15 +72,15 @@ export default Vue.extend({
     },
     linkList: [
       {
-        name: '页',
+        name: '页',
         url: '/teacher/home',
         icon: mdiHome
       },
-      {
-        name: '个人信息',
-        url: '/teacher/info',
-        icon: mdiAccount
-      },
+      // {
+      //   name: '个人信息',
+      //   url: '/teacher/info',
+      //   icon: mdiAccount
+      // },
       {
         name: '评测管理',
         url: '/teacher/created-exams',

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

@@ -15,7 +15,7 @@
           color="warning"
           class="mx-2"
           v-if="state === '进行中'"
-          @click="onExtendTime"
+          @click="onOpenExtendTimeDialog"
           >延长时间</v-btn
         >
         <v-btn
@@ -60,6 +60,25 @@
         >
       </v-col>
     </v-row>
+    <v-dialog v-model="extendTimeDialog" max-width="500" attach="">
+      <v-card class="pa-2">
+        <v-slider
+          label="延长时长"
+          :tick-labels="examExtendTicks"
+          class="ma-4"
+          :max="examExtendTicks.length - 1"
+          v-model="extendLength"
+        ></v-slider>
+        <v-card-actions class="justify-end">
+          <v-btn color="success darken-1" @click="onExtendTime">
+            确定
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+    <v-snackbar v-model="extendSuccess" centered>
+      延长考试时间成功
+    </v-snackbar>
   </v-card>
 </template>
 
@@ -71,15 +90,21 @@ import {
   LocalDateTime,
   UserSerializer
 } from '@/api/types'
-import { computeExamStateText, timeLength } from '@/utils/common'
+import { computeExamStateText, TIME_FORMAT, timeLength } from '@/utils/common'
 import { getExamJoinedUsers } from '@/api/user'
+import { extendExamTime } from '@/api/exam'
+import dayjs from 'dayjs'
 
 export default Vue.extend({
   name: 'ExamInfoCard',
   data() {
     return {
       state: (null as unknown) as ExamStateText,
-      joinedUsers: [] as Array<UserSerializer>
+      joinedUsers: [] as Array<UserSerializer>,
+      extendTimeDialog: false,
+      examExtendTicks: ['10 分钟', '20 分钟', '30 分钟'],
+      extendLength: 0,
+      extendSuccess: false
     }
   },
   props: {
@@ -104,7 +129,25 @@ export default Vue.extend({
         })
       }
     },
-    onExtendTime() {},
+    onOpenExtendTimeDialog() {
+      this.extendTimeDialog = true
+    },
+    onExtendTime() {
+      const examId = this.examInfo.examId
+
+      if (examId) {
+        const originEndTime = dayjs(this.examInfo.endTime, TIME_FORMAT)
+        const extendMinutes = Number.parseInt(
+          this.examExtendTicks[this.extendLength]
+        )
+        const newEndTime = originEndTime.add(extendMinutes, 'minute')
+        console.log(newEndTime.format(TIME_FORMAT))
+        extendExamTime(examId, newEndTime.format(TIME_FORMAT)).then(() => {
+          this.extendSuccess = true
+          this.extendTimeDialog = false
+        })
+      }
+    },
     showExamDetail() {
       const examId = this.examInfo.examId
       if (examId) {

+ 2 - 1
vue.config.js

@@ -11,7 +11,8 @@ module.exports = {
         ws: true,
         changeOrigin: true
       }
-    }
+    },
+    port: 8000
   },
   configureWebpack: {
     plugins: [