Explorar o código

feat: 测试初步流程

ChenYangfan %!s(int64=6) %!d(string=hai) anos
pai
achega
0380bc759c
Modificáronse 39 ficheiros con 676 adicións e 418 borrados
  1. 1 1
      mock/api/question/_questionId/get.json
  2. 5 5
      mock/api/question/get.json
  3. 6 6
      mock/api/quiz/_quizId/get.json
  4. 3 3
      mock/api/quiz/get.json
  5. 2 2
      src/components/Basic/CSelect.vue
  6. 80 53
      src/components/Cards/QuizCard.vue
  7. 6 0
      src/components/Layouts/TabSideComponent.vue
  8. 1 6
      src/router/quiz.js
  9. 6 0
      src/router/student.js
  10. 2 2
      src/server/comment.js
  11. 11 0
      src/server/enums/quiz-type.js
  12. 2 2
      src/server/notice.js
  13. 37 0
      src/server/question-backup.js
  14. 8 27
      src/server/question.js
  15. 63 0
      src/server/quiz-backup.js
  16. 45 26
      src/server/quiz.js
  17. 1 1
      src/server/utils/generate-search-params.js
  18. 5 1
      src/views/quiz/CreateQuestion.vue
  19. 1 1
      src/views/quiz/EditQuestion.vue
  20. 8 7
      src/views/quiz/EditQuiz.vue
  21. 1 1
      src/views/quiz/QuestionDetail.vue
  22. 7 7
      src/views/quiz/QuizDetail.vue
  23. 0 58
      src/views/quiz/QuizView.vue
  24. 141 0
      src/views/quiz/StudentDoQuiz.vue
  25. 3 3
      src/views/quiz/TeacherQuizSetting.vue
  26. 2 2
      src/views/quiz/components/QuestionCard.vue
  27. 4 2
      src/views/quiz/components/QuestionDetailFactory.vue
  28. 4 3
      src/views/quiz/components/QuestionTable.vue
  29. 55 17
      src/views/quiz/components/QuizDetailFactory.vue
  30. 59 21
      src/views/quiz/components/QuizEditor.vue
  31. 1 1
      src/views/quiz/components/QuizResult.vue
  32. 1 1
      src/views/register/Register.vue
  33. 2 0
      src/views/shared/Discussion.vue
  34. 68 0
      src/views/student/components/QuizView.vue
  35. 5 5
      src/views/student/components/StudentCourseSlides.vue
  36. 0 112
      src/views/student/components/StudentDoQuiz.vue
  37. 8 8
      src/views/student/components/StudentQuizDetail.vue
  38. 2 2
      src/views/student/components/StudentSlideDetail.vue
  39. 20 32
      src/views/student/tabs/StudentQuiz.vue

+ 1 - 1
mock/api/question/_questionId/get.json

@@ -1,5 +1,5 @@
 {
-  "kind": "CHOICE",
+  "type": "CHOICE",
   "stem": "计算机中常采用原码、反码、补码和移码表奈数据,其中;±0 编码相同的是( )。",
   "options": {
     "A": "原码和补码",

+ 5 - 5
mock/api/question/get.json

@@ -1,7 +1,7 @@
 {
   "questions": [
     {
-      "kind": "CHOICE",
+      "type": "CHOICE",
       "stem": "计算机中常采用原码、反码、补码和移码表奈数据,其中;±0 编码相同的是( )。",
       "options": {
         "A": "原码和补码",
@@ -28,7 +28,7 @@
       }
     },
     {
-      "kind": "TRUE_FALSE",
+      "type": "TRUE_FALSE",
       "stem": "某指令流水线由 5 段组成,第 1、3、5 段所需时间为△t,第 2、4 段所需时间分别为 3△t、2△t,如下图所示,那么连续输入n条指令时的吞吐率(单位时内可执行的指令个 数)TP 为( )。\n\n![](https://seec.oss-cn-beijing.aliyuncs.com/%E9%A2%98%E5%BA%93/%E9%A2%98%E7%9B%AE%E5%9B%BE%E7%89%87%E6%95%B0%E6%8D%AE/11/media/image5.png)\n\n![](https://seec.oss-cn-beijing.aliyuncs.com/%E9%A2%98%E5%BA%93/%E9%A2%98%E7%9B%AE%E5%9B%BE%E7%89%87%E6%95%B0%E6%8D%AE/11/media/image6.png)",
       "options": null,
       "answer": "【答案】B",
@@ -51,7 +51,7 @@
       }
     },
     {
-      "kind": "CHOICE",
+      "type": "CHOICE",
       "stem": "下面关于漏洞扫描系统的叙述,错误的是( )。",
       "options": {
         "A": "漏洞扫描系统是一种自动检测目标主机安全弱点,的程序",
@@ -79,7 +79,7 @@
       }
     },
     {
-      "kind": "CHOICE",
+      "type": "CHOICE",
       "stem": "网络安全包含了网络信息的可用性、保密性、完整性和网络通信对象的真实性。其中,数字签名是对( )的保护;",
       "options": {
         "A": "可用性",
@@ -107,7 +107,7 @@
       }
     },
     {
-      "kind": "CHOICE",
+      "type": "CHOICE",
       "stem": "计算机感染特洛伊木马后的典型现象是( )。",
       "options": {
         "A": "程序异常退出",

+ 6 - 6
mock/api/quiz/_quizId/get.json

@@ -3,11 +3,11 @@
   "name": "xxx科目测试1",
   "state": "ONGOING",
   "course": {},
-  "quizTime": "BEFORE_CLASS",
+  "type": "BEFORE_CLASS",
   "slide": {},
   "questions": [
     {
-      "kind": "CHOICE",
+      "type": "CHOICE",
       "stem": "计算机中常采用原码、反码、补码和移码表奈数据,其中;±0 编码相同的是( )。",
       "options": {
         "A": "原码和补码",
@@ -21,7 +21,7 @@
       "id": 1005
     },
     {
-      "kind": "TRUE_FALSE",
+      "type": "TRUE_FALSE",
       "stem": "某指令流水线由 5 段组成,第 1、3、5 段所需时间为△t,第 2、4 段所需时间分别为 3△t、2△t,如下图所示,那么连续输入n条指令时的吞吐率(单位时内可执行的指令个 数)TP 为( )。\n\n![](https://seec.oss-cn-beijing.aliyuncs.com/%E9%A2%98%E5%BA%93/%E9%A2%98%E7%9B%AE%E5%9B%BE%E7%89%87%E6%95%B0%E6%8D%AE/11/media/image5.png)\n\n![](https://seec.oss-cn-beijing.aliyuncs.com/%E9%A2%98%E5%BA%93/%E9%A2%98%E7%9B%AE%E5%9B%BE%E7%89%87%E6%95%B0%E6%8D%AE/11/media/image6.png)",
       "options": null,
       "answer": true,
@@ -29,7 +29,7 @@
       "id": 1006
     },
     {
-      "kind": "CHOICE",
+      "type": "CHOICE",
       "stem": "下面关于漏洞扫描系统的叙述,错误的是( )。",
       "options": {
         "A": "漏洞扫描系统是一种自动检测目标主机安全弱点,的程序",
@@ -57,7 +57,7 @@
       }
     },
     {
-      "kind": "CHOICE",
+      "type": "CHOICE",
       "stem": "网络安全包含了网络信息的可用性、保密性、完整性和网络通信对象的真实性。其中,数字签名是对( )的保护;",
       "options": {
         "A": "可用性",
@@ -85,7 +85,7 @@
       }
     },
     {
-      "kind": "CHOICE",
+      "type": "CHOICE",
       "stem": "计算机感染特洛伊木马后的典型现象是( )。",
       "options": {
         "A": "程序异常退出",

+ 3 - 3
mock/api/quiz/get.json

@@ -7,7 +7,7 @@
       "course": {
         "name": "xxx课程"
       },
-      "quizTime": "BEFORE_CLASS",
+      "type": "BEFORE_CLASS",
       "slide": {
         "name": "xxx幻灯片"
       },
@@ -20,7 +20,7 @@
       "course": {
         "name": "xxx课程2"
       },
-      "quizTime": "AFTER_CLASS",
+      "type": "AFTER_CLASS",
       "slide": {
         "name": "xxx幻灯片"
       },
@@ -33,7 +33,7 @@
       "course": {
         "name": "xxx课程3"
       },
-      "quizTime": "FINISH",
+      "type": "FINISH",
       "slide": {
         "name": "xxx幻灯片"
       },

+ 2 - 2
src/components/Basic/CSelect.vue

@@ -1,5 +1,5 @@
 <template>
-  <component :class="value === '' && 'unfilled'" :is="'select'" :value="value" @change="$emit('input', $event.target.value)">
+  <component class="c-select" :class="value === '' && 'unfilled'" :is="'select'" :value="value" @change="$emit('input', $event.target.value)">
     <option disabled :selected="value === ''" :value="''">{{disabledDisplay}}</option>
     <option v-for="item in list" :value="item.value" :key="item.value">
       {{ item.display }}
@@ -43,7 +43,7 @@ select {
   width: 100%;
   height: 40px;
   line-height: 40px;
-  padding: 0 0 0 12px;
+  padding: 0 12px 0 12px;
   //隐藏select的下拉图标
   appearance: none;
   -webkit-appearance: none;

+ 80 - 53
src/components/Cards/QuizCard.vue

@@ -1,33 +1,45 @@
 <template>
-  <router-link tag="div" class="quiz-card" :to="navPath">
-    <div class="quiz-card-title">
-      <c-icon theme="green" :left="-2" :right="6" :size="18">edit</c-icon>
+  <div class="quiz-card" :to="navPath">
+    <div class="quiz-card-title" @click="showMore = !showMore"
+         :class="showMore ? 'quiz-card-title--active' : null">
+      <c-icon :theme="quiz.state === QuizState.ongoing ? 'green' : 'orange'" :left="-2" :right="6" :size="18" bold>
+        {{showMore ? 'expand_more' : 'keyboard_arrow_right'}}
+      </c-icon>
       {{quiz.name || '暂无课程名称'}}
+      <div class="spacer"></div>
+      <router-link :to="navPath">
+        <c-button theme="green" :standout="showMore" icon="arrow_forward">
+          {{quiz.state === QuizState.ongoing ? '开始做题' : '查看详情'}}
+        </c-button>
+      </router-link>
     </div>
     <show-transition-small>
-      <div v-if="!hideCourseName" class="quiz-card-row">{{ quiz.course && quiz.course.name }}</div>
+      <div v-if="showMore" class="quiz-card-row">测试时间:{{ SlideStateTranslation[quiz.type] }}</div>
     </show-transition-small>
     <show-transition-small>
-      <div v-if="!hideSlideName" class="quiz-card-row">{{ quiz.slide && quiz.slide.name }}</div>
+      <div v-if="showMore" class="quiz-card-row"
+           :class="quiz.state === QuizState.ongoing ? 'green' : 'orange'"
+      >测试状态:{{ QuizStateNaming[quiz.state] }}</div>
     </show-transition-small>
     <show-transition-small>
-      <div v-if="!hideQuizTime" class="quiz-card-row">测试时间:{{ SlideStateTranslation[quiz.quizTime] }}</div>
+      <div v-if="showMore" class="quiz-card-row">最大提交次数:{{ quiz.maxSubmitTime }}</div>
     </show-transition-small>
     <show-transition-small>
-      <div v-if="!hideQuizState" class="quiz-card-row">测试状态:{{ QuizStateNaming[quiz.state] }}</div>
+      <div v-if="showMore" class="quiz-card-row">所属课件:{{ quiz.slideName }}</div>
     </show-transition-small>
-  </router-link>
+  </div>
 </template>
 
 <script>
 import CIcon from '@/components/Basic/CIcon'
 import ShowTransitionSmall from '@/animations/ShowTransitionSmall'
 import SlideStateTranslation from '@/server/enums/slide-state-translation'
-import { QuizStateNaming } from '@/server/enums/quiz-state'
+import { QuizState, QuizStateNaming } from '@/server/enums/quiz-state'
+import CButton from '@/components/Basic/CButton'
 
 export default {
   name: 'QuizCard',
-  components: { CIcon, ShowTransitionSmall },
+  components: { CButton, CIcon, ShowTransitionSmall },
   props: {
     clickHandler: {
       type: Function,
@@ -41,64 +53,79 @@ export default {
       type: Object,
       default: () => ({})
     },
-    hideQuizTime: {
-      type: Boolean,
-      default: false
-    },
-    hideQuizState: {
-      type: Boolean,
-      default: false
-    },
-    hideCourseName: {
-      type: Boolean,
-      default: false
-    },
-    hideSlideName: {
-      type: Boolean,
-      default: false
-    }
+    showMoreDefault: Boolean
   },
-  data() {
+  data () {
     return {
+      showMore: this.showMoreDefault,
       SlideStateTranslation,
-      QuizStateNaming
+      QuizStateNaming,
+      QuizState
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
-  @import '~@/style/theme.scss';
+@import '~@/style/theme.scss';
 
-  .quiz-card {
-    margin: 12px 0;
-    padding: 8px 8px 8px 16px;
-    // border-left: 4px solid $input-background-color;
-    transition: .2s;
-    cursor: pointer;
-    border-radius: 12px;
-    background-color: $card-background;
+.quiz-card {
+  margin: 12px 0;
+  padding: 8px 8px 8px 16px;
+  // border-left: 4px solid $input-background-color;
+  overflow: hidden;
+  transition: .2s;
+  cursor: pointer;
+  border-radius: 12px;
+  background-color: lighten($theme-green-transparent, 25);
 
-    &:hover {
-      background: darken($card-background, 5);
-      // border-left: 4px solid $theme-blue;
-    }
+  &:hover {
+    background: $theme-green-transparent;
+    // border-left: 4px solid $theme-blue;
   }
+}
 
-  .quiz-card-title {
-    font-size: 16px;
-    display: flex;
-    align-items: center;
-    position: relative;
-    line-height: 1.8;
-  }
+.quiz-card-title {
+  font-size: 16px;
+  display: flex;
+  align-items: center;
+  position: relative;
+  line-height: 1.8;
+  transition: .2s;
 
-  .quiz-card-row {
-    font-size: 14px;
-    color: #6a7a8a;
+  .spacer {
+    flex: 1;
   }
 
-  .c-icon {
-    transform: scale(1.1);
+  .c-button {
+    padding: 6px 12px;
+    float: right;
+    position: relative;
+    right: -2px;
   }
+}
+
+.quiz-card-title--active {
+  background: $theme-green-light;
+  margin-bottom: 8px;
+  padding-bottom: 8px;
+  box-shadow: -8px -16px 0 16px $theme-green-light;
+}
+
+.quiz-card-row {
+  font-size: 14px;
+  color: #6a7a8a;
+  line-height: 1.7;
+}
+
+.c-icon {
+  transform: scale(1.1);
+}
+
+.green {
+  color: $theme-green;
+}
+.orange {
+  color: $theme-orange;
+}
 </style>

+ 6 - 0
src/components/Layouts/TabSideComponent.vue

@@ -84,4 +84,10 @@ export default {
   }
 }
 
+@media (max-width: 600px) {
+  .content {
+    padding-bottom: 80px;
+  }
+}
+
 </style>

+ 1 - 6
src/router/quiz.js

@@ -18,11 +18,6 @@ export default [{
   path: '/quiz-detail/:quizId/edit',
   name: 'edit-quiz',
   component: () => import(/* webpackChunkName: "teacher-quiz" */ '../views/quiz/EditQuiz.vue')
-}, {
-  meta: { roles: ['student'] },
-  path: '/quiz/:slideId',
-  name: 'quiz',
-  component: () => import(/* webpackChunkName: "student-quiz" */ '../views/quiz/QuizView.vue')
 }, {
   meta: { roles: ['teacher'] },
   path: '/questions',
@@ -47,7 +42,7 @@ export default [{
   meta: { roles: ['student'] },
   path: '/student/quiz/:quizId/do-quiz',
   name: 'student-do-quiz',
-  component: () => import(/* webpackChunkName: "student" */ '../views/student/components/StudentDoQuiz.vue')
+  component: () => import(/* webpackChunkName: "student" */ '../views/quiz/StudentDoQuiz.vue')
 }, {
   meta: { roles: ['student'] },
   path: '/student/quiz/:quizId',

+ 6 - 0
src/router/student.js

@@ -24,6 +24,12 @@ export default [{
       path: 'course-file-detail/:courseFileId',
       name: 'student-explore-course-file-detail',
       component: () => import(/* webpackChunkName: "student" */ '../views/student/components/StudentCourseFileDetail.vue')
+    }, {
+      // 课件下的测试详情列表
+      meta: { roles: ['student'] },
+      path: 'quiz/:slideId',
+      name: 'student-quiz-list',
+      component: () => import(/* webpackChunkName: "student-quiz" */ '../views/student/components/QuizView.vue')
     }]
   }, {
     // 学生查看正在进行的和已经结束的所有测验(一个汇总的地方))

+ 2 - 2
src/server/comment.js

@@ -1,6 +1,6 @@
 import { useCommentServer } from '@/config/server-info'
 import axios from '@/plugins/axios'
-import generateSortParams from '@/server/utils/generate-sort-params'
+import generateSearchParams from '@/server/utils/generate-search-params'
 
 export function createComment ({ slideId, pageNumber, title, content }) {
   const data = {
@@ -33,7 +33,7 @@ export function unTopComment ({ commentId }) {
 }
 
 export function getComments ({ slideId, pageNumber, key = '', page = 1, size = 10, sort = '' }) {
-  const params = generateSortParams({ key, page, size, sort })
+  const params = generateSearchParams({ key, page, size, sort })
   params.append('pageNumber', pageNumber)
 
   return axios

+ 11 - 0
src/server/enums/quiz-type.js

@@ -0,0 +1,11 @@
+export const QuizType = {
+  beforeClass: 'BEFORE_CLASS', // 课前
+  inClass: 'IN_CLASS', // 课中
+  afterClass: 'AFTER_CLASS' // 课后
+}
+
+export const QuizTypeNaming = {
+  [QuizType.beforeClass]: '课前练习',
+  [QuizType.inClass]: '课中练习',
+  [QuizType.afterClass]: '课后练习'
+}

+ 2 - 2
src/server/notice.js

@@ -1,6 +1,6 @@
 import axios from '@/plugins/axios'
 import { useNoticeServer } from '@/config/server-info'
-import generateSortParams from '@/server/utils/generate-sort-params'
+import generateSearchParams from '@/server/utils/generate-search-params'
 
 export function createNotice ({ courseId, title, content }) {
   const data = {
@@ -34,7 +34,7 @@ export function deleteNotice ({ noticeId }) {
 }
 
 export function getNotices ({ courseId, key = '', page = 1, size = 100, sort = '' }) {
-  const params = generateSortParams({ key, page, size, sort })
+  const params = generateSearchParams({ key, page, size, sort })
   params.append('courseId', courseId)
   return axios
     .get(useNoticeServer('/course/' + courseId))

+ 37 - 0
src/server/question-backup.js

@@ -0,0 +1,37 @@
+import axios from '@/plugins/axios'
+import { useQuestionServer } from '@/config/server-info'
+
+export function getQuestionList({
+  stem,
+  size,
+  page,
+  sort
+}) {
+  const params = { stem, size, page, sort }
+  return axios
+    .get(useQuestionServer(), { params })
+    .then(res => res.data)
+}
+
+export function getQuestionById(questionId) {
+  return axios
+    .get(useQuestionServer(`/${questionId}`))
+    .then(res => res.data)
+}
+
+export function createQuestion(questionBody) {
+  return axios
+    .post(useQuestionServer(), questionBody)
+    .then(res => res.data)
+}
+
+export function updateQuestion({ questionId, questionBody }) {
+  return axios
+    .put(useQuestionServer(`/${questionId}`), questionBody)
+    .then(res => res.data)
+}
+
+export function deleteQuestion(questionId) {
+  return axios
+    .delete(useQuestionServer(`/${questionId}`))
+}

+ 8 - 27
src/server/question.js

@@ -1,37 +1,18 @@
 import axios from '@/plugins/axios'
 import { useQuestionServer } from '@/config/server-info'
+import generateSearchParams from '@/server/utils/generate-search-params'
 
-export function getQuestionList({
-  stem,
-  size,
-  page,
-  sort
-}) {
-  const params = { stem, size, page, sort }
+export function getQuestionList ({ key = '', page = 1, size = 10, sort = '' }) {
+  const params = generateSearchParams({ key, page, sort, size })
   return axios
-    .get(useQuestionServer(), { params })
+    .get(useQuestionServer(), {
+      params
+    })
     .then(res => res.data)
 }
 
-export function getQuestionById(questionId) {
+export function getQuestionsOfQuiz ({ quizId }) {
   return axios
-    .get(useQuestionServer(`/${questionId}`))
+    .get(useQuestionServer('/quiz/' + quizId))
     .then(res => res.data)
 }
-
-export function createQuestion(questionBody) {
-  return axios
-    .post(useQuestionServer(), questionBody)
-    .then(res => res.data)
-}
-
-export function updateQuestion({ questionId, questionBody }) {
-  return axios
-    .put(useQuestionServer(`/${questionId}`), questionBody)
-    .then(res => res.data)
-}
-
-export function deleteQuestion(questionId) {
-  return axios
-    .delete(useQuestionServer(`/${questionId}`))
-}

+ 63 - 0
src/server/quiz-backup.js

@@ -0,0 +1,63 @@
+import axios from '@/plugins/axios'
+import { useQuizServer } from '@/config/server-info'
+
+export function getQuizList({
+  slideId,
+  state,
+  size = 100000, // TODO: 分页功能暂时不做,因此取很大值
+  page = 1,
+  sort
+}) {
+  const params = { slideId, state, size, page, sort }
+  return axios
+    .get(useQuizServer(), { params })
+    .then(res => res.data)
+}
+
+export function getQuizById(quizId) {
+  return axios
+    .get(useQuizServer(`/${quizId}`))
+    .then(res => res.data)
+}
+
+export function createQuiz({
+  name, // 测试名
+  quizTime, // 测试时间
+  slideId, // 幻灯片 id
+  questions = [] // 题目 id;
+}) {
+  const data = { name, quizTime, slideId, questions }
+  return axios
+    .post(useQuizServer(), data)
+    .then(res => res.data)
+}
+
+export function updateQuiz({
+  id, // 考试 id
+  name, // 测试名
+  quizTime, // 测试时间
+  slideId, // 幻灯片 id
+  questions = [] // 题目 id;
+}) {
+  const data = { id, name, quizTime, slideId, questions }
+  return axios
+    .put(useQuizServer(`/${id}`), data)
+    .then(res => res.data)
+}
+
+export function deleteQuiz(quizId) {
+  return axios
+    .delete(useQuizServer(`/${quizId}`))
+}
+
+export function submitQuiz({ quizId, answers = [] }) {
+  return axios
+    .put(useQuizServer(`/${quizId}/student-answer`), { answers })
+    .then(res => res.data)
+}
+
+export function getQuizResult(quizId) {
+  return axios
+    .get(useQuizServer(`/${quizId}/result`))
+    .then(res => res.data)
+}

+ 45 - 26
src/server/quiz.js

@@ -1,38 +1,45 @@
 import axios from '@/plugins/axios'
 import { useQuizServer } from '@/config/server-info'
+import generateSearchParams from '@/server/utils/generate-search-params'
 
-export function getQuizList({
-  slideId,
-  state,
-  size = 100000, // TODO: 分页功能暂时不做,因此取很大值
-  page = 1,
-  sort
-}) {
-  const params = { slideId, state, size, page, sort }
-  return axios
-    .get(useQuizServer(), { params })
+// 教师获取测试列表
+export function teacherGetQuizzes ({ slideId, key = '', page = 1, size = 200, sort = '' }) {
+  const params = generateSearchParams({ key, page, size, sort })
+  return axios.get(useQuizServer('/teacher/slide/' + slideId), { params })
     .then(res => res.data)
 }
 
-export function getQuizById(quizId) {
-  return axios
-    .get(useQuizServer(`/${quizId}`))
+// 学生获取测试列表
+export function studentGetQuizzes ({ slideId, key = '', page = 1, size = 20, sort = '' }) {
+  const params = generateSearchParams({ key, page, size, sort })
+  return axios.get(useQuizServer('/student/slide/' + slideId), { params })
     .then(res => res.data)
 }
 
-export function createQuiz({
-  name, // 测试名
-  quizTime, // 测试时间
-  slideId, // 幻灯片 id
-  questions = [] // 题目 id;
-}) {
-  const data = { name, quizTime, slideId, questions }
+// 学生根据状态获取测试列表
+export function studentGetQuizzesByState ({ state, key = '', page = 1, size = 20, sort = '' }) {
+  const params = generateSearchParams({ key, page, size, sort })
+  params.append('state', state)
+  return axios.get(useQuizServer('/student'), { params })
+    .then(res => res.data)
+}
+
+// 获取某个特定quiz的内容
+export function getOneQuiz ({ quizId }) {
+  return axios.get(useQuizServer('/' + quizId))
+    .then(res => res.data)
+}
+
+// 创建测试
+export function createQuiz ({ name, type, slideId, questions, maxSubmitTime = '5' }) {
+  const data = { name, type, slideId, questions, maxSubmitTime }
   return axios
     .post(useQuizServer(), data)
     .then(res => res.data)
 }
 
-export function updateQuiz({
+// 修改测试(仅限未开始时可以修改)
+export function updateQuiz ({
   id, // 考试 id
   name, // 测试名
   quizTime, // 测试时间
@@ -45,19 +52,31 @@ export function updateQuiz({
     .then(res => res.data)
 }
 
-export function deleteQuiz(quizId) {
+// 删除测试(伪删除)
+export function deleteQuiz ({ quizId }) {
   return axios
     .delete(useQuizServer(`/${quizId}`))
 }
 
-export function submitQuiz({ quizId, answers = [] }) {
+// 提交答案
+export function submitQuizStudentAnswer ({ quizId, answers = [] }) {
+  return axios
+    .post(useQuizServer('/student-answer'), { quizId, answers })
+}
+
+// 老师获取某一测试学生的作答列表
+export function getQuizStudentAnswers ({ quizId, key = '', page = 1, size = 20, sort = '' }) {
+  const params = generateSearchParams({ key, sort, size, page })
   return axios
-    .put(useQuizServer(`/${quizId}/student-answer`), { answers })
+    .get(useQuizServer(`/${quizId}/student-answers`), {
+      params
+    })
     .then(res => res.data)
 }
 
-export function getQuizResult(quizId) {
+// 学生获取某一测试自己的作答
+export function getOneQuizStudentAnswer ({ quizId }) {
   return axios
-    .get(useQuizServer(`/${quizId}/result`))
+    .get(useQuizServer(`/${quizId}/student-answer`))
     .then(res => res.data)
 }

+ 1 - 1
src/server/utils/generate-sort-params.js → src/server/utils/generate-search-params.js

@@ -1,4 +1,4 @@
-export default function generateSortParams ({ key = '', page = 1, size = 10, sort = '' }) {
+export default function generateSearchParams ({ key = '', page = 1, size = 10, sort = '' }) {
   const params = new URLSearchParams()
   params.append('key', key)
   params.append('page', page.toString())

+ 5 - 1
src/views/quiz/CreateQuestion.vue

@@ -10,7 +10,7 @@
 import BackTitle from '@/components/Basic/BackTitle'
 import QuestionKindBar from '@/components/QuestionKindBar'
 import QuestionKind from '@/server/enums/question-kind'
-import { createQuestion } from '@/server/question'
+import { createQuestion } from '@/server/question-backup'
 import QuestionEditorFactory from '@/views/quiz/components/QuestionEditorFactory'
 
 export default {
@@ -19,8 +19,12 @@ export default {
   data () {
     return { currentKind: QuestionKind.choice }
   },
+  mounted () {
+    this.$setErrorMessage('本页面后端 API 需要进行改进,暂不能使用')
+  },
   methods: {
     create (questionBody, kind) {
+      this.$setErrorMessage('本页面后端 API 需要进行改进,暂不能使用')
       createQuestion({ ...questionBody, kind })
         .then(() => this.$setSuccessMessage('创建成功'))
         .then(() => this.$router.go(-1))

+ 1 - 1
src/views/quiz/EditQuestion.vue

@@ -8,7 +8,7 @@
 <script>
 import BackTitle from '@/components/Basic/BackTitle'
 import QuestionKind from '@/server/enums/question-kind'
-import { getQuestionById, updateQuestion } from '@/server/question'
+import { getQuestionById, updateQuestion } from '@/server/question-backup'
 import QuestionEditorFactory from '@/views/quiz/components/QuestionEditorFactory'
 
 export default {

+ 8 - 7
src/views/quiz/EditQuiz.vue

@@ -1,34 +1,35 @@
 <template>
   <div class="edit-quiz">
-    <quiz-editor back-title="修改测试" :init-quiz="initQuiz" @submit="handleSubmit" />
+    <quiz-editor back-title="修改测试" :init-quiz="initQuiz" @submit="handleSubmit"/>
   </div>
 </template>
 
 <script>
-import { getQuizById, updateQuiz } from '@/server/quiz'
+import { updateQuiz } from '@/server/quiz-backup'
 import QuizEditor from '@/views/quiz/components/QuizEditor'
+import { getOneQuiz } from '@/server/quiz'
 
 export default {
   name: 'EditQuiz',
   components: { QuizEditor },
-  data() {
+  data () {
     return {
       initQuiz: {}
     }
   },
-  mounted() {
+  mounted () {
     this.fetchQuizDetail()
   },
   methods: {
-    fetchQuizDetail() {
+    fetchQuizDetail () {
       const { quizId } = this.$route.params
-      getQuizById(quizId)
+      getOneQuiz({ quizId })
         .then(res => (this.initQuiz = res))
         .catch(err =>
           this.$setErrorMessage(err.response.data.msg)
         )
     },
-    handleSubmit(quizData) {
+    handleSubmit (quizData) {
       updateQuiz({ ...quizData, slideId: this.initQuiz?.slide?.id })
         .then(() => this.$setSuccessMessage('创建成功'))
         .then(() => this.$router.go(-1))

+ 1 - 1
src/views/quiz/QuestionDetail.vue

@@ -7,7 +7,7 @@
 
 <script>
 import QuestionKind from '@/server/enums/question-kind'
-import { getQuestionById } from '@/server/question'
+import { getQuestionById } from '@/server/question-backup'
 import BackTitle from '@/components/Basic/BackTitle'
 import QuestionDetailFactory from '@/views/quiz/components/QuestionDetailFactory'
 

+ 7 - 7
src/views/quiz/QuizDetail.vue

@@ -1,35 +1,35 @@
 <template>
   <div class="quiz-detail">
     <back-title>{{ quizDetail.name }}</back-title>
-    <quiz-result v-if="quizDetail.state === QuizState.closed" :quiz-id="$route.params.quizId" />
-    <quiz-detail-factory :quiz-detail="quizDetail" />
+    <quiz-result v-if="quizDetail.state === QuizState.closed" :quiz-id="$route.params.quizId"/>
+    <quiz-detail-factory :quiz-detail="quizDetail"/>
   </div>
 </template>
 
 <script>
 import { createDefaultQuiz } from '@/views/quiz/utils/default'
-import { getQuizById } from '@/server/quiz'
 import BackTitle from '@/components/Basic/BackTitle'
 import QuizDetailFactory from '@/views/quiz/components/QuizDetailFactory'
 import QuizResult from '@/views/quiz/components/QuizResult'
 import { QuizState } from '@/server/enums/quiz-state'
+import { getOneQuiz } from '@/server/quiz'
 
 export default {
   name: 'QuizDetail',
   components: { QuizResult, QuizDetailFactory, BackTitle },
-  data() {
+  data () {
     return {
       quizDetail: createDefaultQuiz(),
       QuizState
     }
   },
-  mounted() {
+  mounted () {
     this.fetchQuizDetail()
   },
   methods: {
-    fetchQuizDetail() {
+    fetchQuizDetail () {
       const { quizId } = this.$route.params
-      getQuizById(quizId)
+      getOneQuiz({ quizId })
         .then(res => (this.quizDetail = res))
         .catch(err => this.$setErrorMessage(err.response.data.msg))
     }

+ 0 - 58
src/views/quiz/QuizView.vue

@@ -1,58 +0,0 @@
-<template>
-  <div class="quiz-view">
-    <back-title>{{slideInfo ? slideInfo.name + '测验列表' : "加载中..."}}</back-title>
-    <quiz-card
-      v-for="item in quizList" :key="item.id"
-      :nav-path="{ name: 'student-quiz-detail', params: { quizId: item.id } }"
-      :quiz="item" hide-quiz-state hide-quiz-time />
-  </div>
-</template>
-
-<script>
-import BackTitle from '@/components/Basic/BackTitle'
-import SlideStateTranslation from '@/server/enums/slide-state-translation'
-import { QuizStateNaming } from '@/server/enums/quiz-state'
-import { getQuizList } from '@/server/quiz'
-import QuizCard from '@/components/Cards/QuizCard'
-import { getSlideInfo } from '@/server/slide'
-export default {
-  name: 'QuizView',
-  components: { QuizCard, BackTitle },
-  data() {
-    return {
-      quizList: null,
-      slideInfo: null,
-
-      SlideStateTranslation,
-      QuizStateNaming
-    }
-  },
-  mounted () {
-    this.fetchQuiz()
-    this.fetchSlideInfo()
-  },
-  methods: {
-    fetchSlideInfo () {
-      getSlideInfo({ slideId: this.$route.params.slideId }).then(res => {
-        this.slideInfo = res
-      }).catch(err => {
-        this.$setErrorMessage(err.response.data.msg)
-      })
-    },
-    fetchQuiz () {
-      const { slideId } = this.$route.params
-      getQuizList({ slideId })
-        .then(res => (this.quizList = res.quizzes))
-        .catch(err => this.$setErrorMessage(err.response.data.msg))
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.quiz-view {
-  margin: 0 auto;
-  max-width: 600px;
-  padding: 64px 24px;
-}
-</style>

+ 141 - 0
src/views/quiz/StudentDoQuiz.vue

@@ -0,0 +1,141 @@
+<template>
+  <div class="do-quiz">
+    <back-title>{{ quiz.name }}</back-title>
+    <form @submit.prevent="handleFormSubmit">
+      <c-button class="mb-16" theme="green" icon="check">提交答案</c-button>
+      <div class="quiz-list mb-16" :key="question.id" v-for="(question, index) in questions">
+        <div class="quiz-list__index">{{ index + 1 }}.</div>
+        <div>
+          <markdown-section :raw="question.stem"/>
+          <div v-if="question.type === QuestionKind.choice">
+            <label class="option" :key="index" v-for="(item, index) in question.options">
+              <input v-model="question.studentAnswer" type="radio" :value="index"/>
+              {{ index }}.
+              <markdown-section style="display: inline" :raw="item"/>
+            </label>
+          </div>
+          <div v-else-if="question.type === QuestionKind.trueOrFalse">
+            <label class="option">
+              <input v-model="question.studentAnswer" type="radio" :value="true"/>
+              <c-icon>check</c-icon>
+            </label>
+            <label class="option">
+              <input v-model="question.studentAnswer" type="radio" :value="false"/>
+              <c-icon>close</c-icon>
+            </label>
+          </div>
+        </div>
+      </div>
+      <c-button class="mb-16" theme="green" icon="check">提交答案</c-button>
+    </form>
+  </div>
+</template>
+<script>
+import BackTitle from '@/components/Basic/BackTitle'
+import { createDefaultQuiz } from '@/views/quiz/utils/default'
+import QuestionKind from '@/server/enums/question-kind'
+import MarkdownSection from '@/views/quiz/components/MarkdownSection'
+import CButton from '@/components/Basic/CButton'
+import CIcon from '@/components/Basic/CIcon'
+import { getOneQuiz, getOneQuizStudentAnswer, submitQuizStudentAnswer } from '@/server/quiz'
+import { getQuestionsOfQuiz } from '@/server/question'
+
+export default {
+  name: 'StudentDoQuiz',
+  components: { CIcon, CButton, MarkdownSection, BackTitle },
+  data () {
+    return {
+      quiz: createDefaultQuiz(),
+      hasSubmit: false,
+      questions: null,
+
+      QuestionKind
+    }
+  },
+  beforeRouteLeave (to, from, next) {
+    if (this.hasSubmit) {
+      next()
+    } else if (window.confirm('您的答案没有提交,确定要离开页面?')) {
+      next()
+    } else {
+      next(false)
+    }
+  },
+  mounted () {
+    this.fetchQuizDetail()
+  },
+  methods: {
+    fetchQuizDetail () {
+      const { quizId } = this.$route.params
+      getOneQuiz({ quizId }).then(res => {
+        this.quiz = res
+      }).catch(err => this.$setErrorMessage(err.response.data.msg))
+      getQuestionsOfQuiz({ quizId }).then(res => {
+        this.questions = res
+        // 获取之前填写的答案
+        getOneQuizStudentAnswer({ quizId }).then(answers => {
+          this.questions = this.questions.map(question => ({
+            ...question,
+            studentAnswer: answers.answers[question.id]
+          }))
+        }).catch(err => { this.$setErrorMessage(err.response.data.msg) })
+      }).catch(err => this.$setErrorMessage(err.response.data.msg))
+    },
+    handleFormSubmit () {
+      const { quizId } = this.$route.params
+      const answers = {}
+      let uncompleted = false
+      this.questions.map(question => {
+        if (question.studentAnswer) {
+          answers[question.id] = question.studentAnswer
+        } else {
+          this.$setErrorMessage('题目:"' + question.stem + '"尚未填写')
+          uncompleted = true
+        }
+      })
+      if (uncompleted) return
+
+      submitQuizStudentAnswer({ quizId, answers }).then(res => {
+        this.hasSubmit = true
+        this.$setSuccessMessage('提交成功')
+        this.$router.go(-1)
+      }).catch(err => { this.$setErrorMessage(err.response.data.msg) })
+      // submitQuiz({ quizId, answers: this.quiz.questions })
+      //   .then(() => {
+      //     this.hasSubmit = true
+      //     this.$setSuccessMessage('提交成功')
+      //     this.$router.go(-1)
+      //   })
+      //   .catch(err =>
+      //     this.$setErrorMessage(err.response.data.msg)
+      //   )
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.do-quiz {
+  margin: 0 auto;
+  max-width: 800px;
+  padding: 64px 24px;
+}
+
+.quiz-list {
+  display: flex;
+
+  &__index {
+    margin-right: 16px;
+  }
+
+  .option {
+    display: flex;
+    align-items: center;
+    margin-top: 8px;
+
+    input {
+      margin-right: 12px;
+    }
+  }
+}
+</style>

+ 3 - 3
src/views/quiz/TeacherQuizSetting.vue

@@ -29,11 +29,11 @@
 
 <script>
 import BackTitle from '@/components/Basic/BackTitle'
-import { getQuizList } from '@/server/quiz'
 import SlideStateTranslation from '@/server/enums/slide-state-translation'
 import { QuizStateNaming, QuizState } from '@/server/enums/quiz-state'
 import { getSlideInfo } from '@/server/slide'
 import CButton from '@/components/Basic/CButton'
+import { teacherGetQuizzes } from '@/server/quiz'
 
 export default {
   name: 'TeacherQuizSetting',
@@ -64,8 +64,8 @@ export default {
   methods: {
     fetchQuiz () {
       const { slideId } = this.$route.params
-      getQuizList({ slideId })
-        .then(res => (this.quizList = res.quizzes))
+      teacherGetQuizzes({ slideId, size: 2000, sort: 'createAt,desc' })
+        .then(res => (this.quizList = res.data))
         .catch(err => this.$setErrorMessage(err.response.data.msg))
     }
   }

+ 2 - 2
src/views/quiz/components/QuestionCard.vue

@@ -7,7 +7,7 @@
           {{selected ? 'check_box' : 'check_box_outline_blank'}}
         </icon-button>
         <span class="question-id">编号:{{ questionId }}</span>
-        <c-tag theme="green">{{ QuestionKind.naming[kind] }}</c-tag>
+        <c-tag theme="green">{{ QuestionKind.naming[type] }}</c-tag>
       </h3>
       <div class="spacer"></div>
       <div class="actions">
@@ -41,7 +41,7 @@ export default {
       type: String,
       required: true
     },
-    kind: {
+    type: {
       type: String,
       required: true
     },

+ 4 - 2
src/views/quiz/components/QuestionDetailFactory.vue

@@ -1,6 +1,8 @@
 <template>
-  <choice-question-detail :fold="fold" v-if="question.kind === QuestionKind.choice" :value="question" />
-  <true-false-question-detail :fold="fold" v-else-if="question.kind === QuestionKind.trueOrFalse" :value="question" />
+  <div>
+    <choice-question-detail :fold="fold" v-if="question.type === QuestionKind.choice" :value="question" />
+    <true-false-question-detail :fold="fold" v-else-if="question.type === QuestionKind.trueOrFalse" :value="question" />
+  </div>
 </template>
 
 <script>

+ 4 - 3
src/views/quiz/components/QuestionTable.vue

@@ -7,7 +7,7 @@
       <question-card
         v-for="item in questionList"
         :key="item.id"
-        :kind="item.kind"
+        :type="item.type"
         :stem="item.stem"
         :question-id="item.id"
         :selectable="selectable"
@@ -65,8 +65,8 @@ export default {
       page = 1,
       size = 10
     } = {}) {
-      getQuestionList({ stem, size, page })
-        .then(({ questions, page }) => {
+      getQuestionList({ key: stem, size, page })
+        .then(({ data: questions, page }) => {
           this.questionList = questions
           this.page = page
         })
@@ -97,5 +97,6 @@ export default {
   break-inside: avoid;
   border-radius: 6px;
   border: 1px solid #cadaea;
+  /*box-shadow: 0 2px 6px rgba(0, 0, 0, .1);*/
 }
 </style>

+ 55 - 17
src/views/quiz/components/QuizDetailFactory.vue

@@ -1,27 +1,44 @@
 <template>
   <div>
-    <div v-if="quizDetail.state === QuizState.closed" class="quiz-info">测试得分:{{ Math.floor(100 * (passQuestionNums / totalQuestionNums)) }} 分</div>
-    <div class="quiz-info">测试时间:{{ SlideStateTranslation[quizDetail.quizTime] }}</div>
-    <div class="quiz-info">测试状态:{{ QuizStateNaming[quizDetail.state] }}</div>
-    <div class="mt-32 mb-32" :key="item.id" v-for="(item, index) in quizDetail.questions">
+    <template v-if="quizDetail.state === QuizState.closed">
+      <template v-if="result === {}">
+        <div class="quiz-info-standout">测试得分:{{result.score}} 分</div>
+        <div class="quiz-info-standout">最后提交时间:{{momentFromNow(result.submitAt)}}</div>
+      </template>
+      <template v-else>
+        <div class="no-content-warning">您未提交过答案</div>
+      </template>
+    </template>
+    <div class="quiz-info-standout">测试时间:{{ QuizTypeNaming[quizDetail.type] }}</div>
+    <div class="quiz-info-standout">测试状态:{{ QuizStateNaming[quizDetail.state] }}</div>
+    <div class="quiz-info">所属课件:{{ quizDetail.slideName }}</div>
+    <div class="quiz-info">提交次数限制:{{ quizDetail.maxSubmitTime }}</div>
+    <div class="quiz-info">发布时间:{{ momentFromNow(quizDetail.createAt) }}</div>
+    <div v-if="questions===null" class="no-content-warning">加载中...</div>
+    <div v-else class="mt-32 mb-32" :key="item.id" v-for="(item, index) in questions">
       <div class="index">题号:{{ index + 1 }}
-        <c-icon v-if="!isEmpty(item.pass)" bold :size="20" :left="6"
-                :theme="item.pass ? 'green' : 'red'">
-          {{ item.pass ? 'check' : 'close' }}
-        </c-icon></div>
-      <question-detail-factory :question="item" />
+        <c-icon v-if="!isEmpty(answers[item.id])" bold :size="20" :left="16" :right="12"
+                :theme="item.answer.match(answers[item.id]) ? 'green' : 'red'">
+          {{ item.answer.match(answers[item.id]) ? 'check' : 'close' }}
+        </c-icon>
+        你的回答:{{answers[item.id]}}
+      </div>
+      <question-detail-factory :question="item"/>
     </div>
   </div>
 </template>
 
 <script>
 import { createDefaultQuiz } from '@/views/quiz/utils/default'
-import SlideStateTranslation from '@/server/enums/slide-state-translation'
 import QuestionKind from '@/server/enums/question-kind'
 import { QuizStateNaming, QuizState } from '@/server/enums/quiz-state'
 import QuestionDetailFactory from '@/views/quiz/components/QuestionDetailFactory'
 import { isEmpty } from '@/utils/types'
 import CIcon from '@/components/Basic/CIcon'
+import { QuizTypeNaming } from '@/server/enums/quiz-type'
+import momentFromNow from '@/utils/moment-from-now'
+import { getQuestionsOfQuiz } from '@/server/question'
+import { getOneQuizStudentAnswer } from '@/server/quiz'
 
 export default {
   components: { CIcon, QuestionDetailFactory },
@@ -31,34 +48,55 @@ export default {
       default: createDefaultQuiz
     }
   },
-  data() {
+  data () {
     return {
+      questions: null,
+      answers: {},
+      result: {},
 
-      SlideStateTranslation,
+      QuizTypeNaming,
       QuestionKind,
       QuizState,
       QuizStateNaming
     }
   },
   computed: {
-    totalQuestionNums() {
+    totalQuestionNums () {
       return this.quizDetail?.questions?.length ?? 0
     },
-    passQuestionNums() {
+    passQuestionNums () {
       return this.quizDetail?.questions?.filter(item => item.pass).length
     }
   },
+  mounted () {
+    const { quizId } = this.$route.params
+    getQuestionsOfQuiz({ quizId }).then(res => {
+      this.questions = res
+    }).catch(err => { this.$setErrorMessage(err.response.data.msg) })
+    getOneQuizStudentAnswer({ quizId }).then(res => {
+      this.answers = res ? res.answers ? res.answers : {} : {}
+      this.result = res || {}
+    }).catch(err => { this.$setErrorMessage(err.response.data.msg) })
+  },
   methods: {
-    isEmpty
+    isEmpty,
+    momentFromNow
   }
 }
 </script>
 <style lang="scss" scoped>
 @import "~@/style/theme.scss";
+
 .quiz-info {
-  margin: 8px 0;
-  color: #7a8a9a;
+  margin: 6px 0;
+  color: #8a9aaa;
+
+  &-standout {
+    margin: 6px 0;
+    color: #5a6a7a;
+  }
 }
+
 .index {
   display: flex;
   align-items: center;

+ 59 - 21
src/views/quiz/components/QuizEditor.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="quiz-editor">
-    <div>
+    <div class="quiz-form-wrapper">
       <back-title>{{backTitle}}</back-title>
       <div class="quiz-form">
         <div class="sub-title mb-16">测试详情</div>
@@ -8,13 +8,23 @@
           测试名称:
         </div>
         <c-input class="mb-24" v-model="quiz.name" placeholder="请输入测试名称"></c-input>
-        <div class="input-title mb-8">
-          开放测试所需课件状态:
+        <div class="mb-24 type-times-bar">
+          <div class="mr-16">
+            <div class="input-title mb-8">
+              测试类型:
+            </div>
+            <c-select disabled-display="[ 未选择 ]" v-model="quiz.type" :list="selectItems"></c-select>
+          </div>
+          <div>
+            <div class="input-title mb-8">
+              最大提交次数:
+            </div>
+            <c-input type="number" v-model="quiz.maxSubmitTime"></c-input>
+          </div>
         </div>
-        <c-select disabled-display="[ 未选择 ]" v-model="quiz.quizTime" class="mb-24" :list="selectItems"></c-select>
         <c-button theme="green" icon="check" block @click="handleSubmit">{{backTitle}}</c-button>
       </div>
-      <div class="sub-title mt-32">
+      <div class="sub-title mt-32" v-if="selectedQuestions && selectedQuestions.length">
         测试题目列表
         <c-button :left="4" @click="isFold = !isFold" text :icon="isFold ? 'unfold_more' : 'unfold_less'" theme="blue">
           {{isFold ? '展开' : '收起'}}题目
@@ -34,7 +44,7 @@
         </div>
       </div>
     </div>
-    <div>
+    <div class="questions">
       <div class="sub-title">题库</div>
       <question-table v-model="selectedQuestions" selectable/>
     </div>
@@ -44,13 +54,13 @@
 <script>
 import { naming as QuestionKindNaming } from '@/server/enums/question-kind'
 import { QuizStateNaming } from '@/server/enums/quiz-state'
-import SlideStateTranslation from '@/server/enums/slide-state-translation'
 import QuestionTable from '@/views/quiz/components/QuestionTable'
 import QuestionDetailFactory from '@/views/quiz/components/QuestionDetailFactory'
 import CInput from '@/components/Basic/CInput'
 import CButton from '@/components/Basic/CButton'
 import CSelect from '@/components/Basic/CSelect'
 import BackTitle from '@/components/Basic/BackTitle'
+import { QuizTypeNaming } from '@/server/enums/quiz-type'
 
 export default {
   name: 'QuizEditor',
@@ -67,29 +77,29 @@ export default {
   },
   data () {
     return {
-      quiz: { name: '', quizTime: '' },
+      quiz: { name: '', type: '', maxSubmitTime: '0' },
       selectedQuestions: [],
       // 默认折叠题目,只显示题干
       isFold: true,
 
       QuestionKindNaming,
       QuizStateNaming,
-      SlideStateTranslation
+      QuizTypeNaming
     }
   },
   computed: {
     selectItems () {
-      return Object.keys(this.SlideStateTranslation).map(key => ({
+      return Object.keys(this.QuizTypeNaming).map(key => ({
         value: key,
-        display: this.SlideStateTranslation[key]
+        display: this.QuizTypeNaming[key]
       }))
     }
   },
   watch: {
     initQuiz: {
       immediate: true,
-      handler ({ id, name, quizTime, questions = [] } = {}) {
-        this.quiz = { id, name, quizTime }
+      handler ({ id, name, type, maxSubmitTime, questions = [] } = {}) {
+        this.quiz = { id, name, type, maxSubmitTime }
         this.selectedQuestions = questions
       }
     }
@@ -137,19 +147,41 @@ export default {
     width: 30%;
   }
 
-  .quiz-form {
-    box-sizing: border-box;
-    border: 1px solid #cadaea;
-    border-radius: 6px;
-    padding: 16px;
+  .quiz-form-wrapper {
+    background: #f6fafe;
+    // 模拟溢出的背景
+    padding-bottom: 8px;
+    box-shadow: 0 -8px 0 24px #f6fafe;
+  }
+
+  .questions {
+    flex: 2;
+  }
+}
+
+.quiz-form {
+  background: white;
+  box-sizing: border-box;
+  box-shadow: 0 3px 6px rgba(0, 0, 0, .1);
+  border-radius: 6px;
+  padding: 16px;
+}
+
+.type-times-bar {
+  display: flex;
+
+  .c-input {
+    max-width: 120px;
   }
 }
 
 .selected-question-card {
-  border: 1px solid #cadaea;
+  /*border: 1px solid #cadaea;*/
+  box-shadow: 0 3px 6px rgba(0, 0, 0, .1);
   border-radius: 6px;
   padding: 8px 12px 12px 12px;
   font-size: 12px;
+  background: white;
 
   .spacer {
     flex: 1;
@@ -165,7 +197,7 @@ export default {
   }
 }
 
-@media (max-width: 900px) {
+@media (max-width: 860px) {
   .quiz-editor {
     display: block;
     margin: 0 auto;
@@ -175,7 +207,7 @@ export default {
       margin: unset;
     }
 
-    & > div:last-child {
+    .questions {
       /*border-top: 1px #cadaea solid;*/
       padding-top: 16px;
       margin-top: 24px;
@@ -183,6 +215,12 @@ export default {
   }
 }
 
+@media (max-width: 1100px) {
+  .quiz-editor .questions {
+    flex: 1;
+  }
+}
+
 .question-preview {
   margin: 24px 0 12px 0;
   font-size: 14px;

+ 1 - 1
src/views/quiz/components/QuizResult.vue

@@ -3,7 +3,7 @@
 </template>
 
 <script>
-import { getQuizResult } from '@/server/quiz'
+import { getQuizResult } from '@/server/quiz-backup'
 
 export default {
   name: 'QuizResult',

+ 1 - 1
src/views/register/Register.vue

@@ -10,7 +10,7 @@
         <c-input placeholder="邮箱" type="email" v-model="email"></c-input>
         <div class="verified-code-wrapper">
           <c-input placeholder="邮箱验证码" type="number" v-model="emailCode"></c-input>
-          <c-button :no-click-warn="typeof this.emailButtonContent === typeof 0"
+          <c-button :no-click-warn="typeof this.emailButtonContent === typeof 0" theme="blue"
                     @click="handleSendEmailCode">{{ emailButtonContent }}
           </c-button>
         </div>

+ 2 - 0
src/views/shared/Discussion.vue

@@ -167,6 +167,8 @@ export default {
     position: relative;
     box-sizing: border-box;
     min-height: 450px;
+    max-height: 90vh;
+    overflow: auto;
   }
 }
 

+ 68 - 0
src/views/student/components/QuizView.vue

@@ -0,0 +1,68 @@
+<template>
+  <tab-side-component class="quiz-view">
+    <back-title>{{slideInfo ? slideInfo.name + '测验列表' : '加载中...'}}</back-title>
+    <div class="no-content-warning" v-if="quizList === null">
+      加载中...
+    </div>
+    <div class="no-content-warning" v-else-if="!quizList.length">暂无测试</div>
+    <quiz-card v-else
+      v-for="item in quizList" :key="item.id"
+      :nav-path="navPath(item)"
+      :quiz="item"/>
+  </tab-side-component>
+</template>
+
+<script>
+import BackTitle from '@/components/Basic/BackTitle'
+import SlideStateTranslation from '@/server/enums/slide-state-translation'
+import { QuizState, QuizStateNaming } from '@/server/enums/quiz-state'
+import QuizCard from '@/components/Cards/QuizCard'
+import { getSlideInfo } from '@/server/slide'
+import { studentGetQuizzes } from '@/server/quiz'
+import TabSideComponent from '@/components/Layouts/TabSideComponent'
+
+export default {
+  name: 'QuizView',
+  components: { TabSideComponent, QuizCard, BackTitle },
+  data () {
+    return {
+      quizList: null,
+      slideInfo: null,
+
+      showMore: true,
+      SlideStateTranslation,
+      QuizStateNaming,
+      QuizState
+    }
+  },
+  mounted () {
+    this.fetchQuiz()
+    this.fetchSlideInfo()
+  },
+  methods: {
+    fetchSlideInfo () {
+      getSlideInfo({ slideId: this.$route.params.slideId }).then(res => {
+        this.slideInfo = res
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    },
+    fetchQuiz () {
+      const { slideId } = this.$route.params
+      studentGetQuizzes({ slideId, size: 2000, sort: 'createAt,desc' })
+        .then(res => (this.quizList = res.data))
+        .catch(err => this.$setErrorMessage(err.response.data.msg))
+    },
+    navPath (quiz) {
+      if (quiz.state === QuizState.ongoing) {
+        return { name: 'student-do-quiz', params: { quizId: quiz.id } }
+      } else {
+        return { name: 'student-quiz-detail', params: { quizId: quiz.id } }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 5 - 5
src/views/student/components/StudentCourseSlides.vue

@@ -1,10 +1,5 @@
 <template>
   <tab-side-component>
-    <template v-for="notice of notices">
-      <notice-card :key="'notice-' + notice.id"
-                   :notice="notice">
-      </notice-card>
-    </template>
     <back-title>课件列表
       <c-button :theme="showActions ? 'orange' : 'normal'" :left="8" text icon="settings"
                 @click="showActions = !showActions">设置
@@ -28,6 +23,11 @@
     <div class="no-content-warning" v-else-if="slides.length === 0 && courseFiles.length === 0">
       <div class="no-content-warning-tip">本课程尚无课件~</div>
     </div>
+    <template v-for="notice of notices">
+      <notice-card :key="'notice-' + notice.id"
+                   :notice="notice">
+      </notice-card>
+    </template>
     <template v-for="slide of slides">
       <slide-card :key="'slide-' + slide.id" :hide-course="!showMore" :hide-teacher="!showMore"
                   :slide="slide" :hide-update-time="!showMore"

+ 0 - 112
src/views/student/components/StudentDoQuiz.vue

@@ -1,112 +0,0 @@
-<template>
-  <div class="do-quiz">
-    <back-title>{{ quiz.name }}</back-title>
-    <form @submit.prevent="handleFormSubmit">
-      <c-button class="mb-16" theme="green" icon="check">提交答案</c-button>
-      <div class="quiz-list mb-16" :key="question.id" v-for="(question, index) in quiz.questions">
-        <div class="quiz-list__index">{{ index + 1 }}.</div>
-        <div>
-          <markdown-section :raw="question.stem" />
-          <div v-if="question.kind === QuestionKind.choice">
-            <label class="option" :key="index" v-for="(item, index) in question.options">
-              <input v-model="question.studentAnswer" type="radio" :value="index" />
-              {{ index }}.
-              <markdown-section style="display: inline" :raw="item" />
-            </label>
-          </div>
-          <div v-else-if="question.kind === QuestionKind.trueOrFalse">
-            <label class="option">
-              <input v-model="question.studentAnswer" type="radio" :value="true" />
-              <c-icon>check</c-icon>
-            </label>
-            <label class="option">
-              <input v-model="question.studentAnswer" type="radio" :value="false" />
-              <c-icon>close</c-icon>
-            </label>
-          </div>
-        </div>
-
-      </div>
-      <c-button class="mb-16" theme="green" icon="check">提交答案</c-button>
-    </form>
-  </div>
-</template>
-<script>
-import BackTitle from '@/components/Basic/BackTitle'
-import { getQuizById, submitQuiz } from '@/server/quiz'
-import { createDefaultQuiz } from '@/views/quiz/utils/default'
-import QuestionKind from '@/server/enums/question-kind'
-import MarkdownSection from '@/views/quiz/components/MarkdownSection'
-import CButton from '@/components/Basic/CButton'
-import CIcon from '@/components/Basic/CIcon'
-
-export default {
-  name: 'StudentDoQuiz',
-  components: { CIcon, CButton, MarkdownSection, BackTitle },
-  data() {
-    return {
-      quiz: createDefaultQuiz(),
-      hasSubmit: false,
-
-      QuestionKind
-    }
-  },
-  beforeRouteLeave (to, from, next) {
-    if (this.hasSubmit) {
-      next()
-    } else if (window.confirm('您的答案没有提交,确定要离开页面?')) {
-      next()
-    } else {
-      next(false)
-    }
-  },
-  mounted() {
-    this.fetchQuizDetail()
-  },
-  methods: {
-    fetchQuizDetail() {
-      const { quizId } = this.$route.params
-      getQuizById(quizId)
-        .then(res => (this.quiz = res))
-        .catch(err => this.$setErrorMessage(err.response.data.msg))
-    },
-    handleFormSubmit() {
-      const { quizId } = this.$route.params
-      submitQuiz({ quizId, answers: this.quiz.questions })
-        .then(() => {
-          this.hasSubmit = true
-          this.$setSuccessMessage('提交成功')
-          this.$router.go(-1)
-        })
-        .catch(err =>
-          this.$setErrorMessage(err.response.data.msg)
-        )
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-  .do-quiz {
-    margin: 0 auto;
-    max-width: 1000px;
-    padding: 64px 24px;
-  }
-  .quiz-list{
-    display: flex;
-
-    &__index{
-      margin-right: 16px;
-    }
-
-    .option{
-      display: flex;
-      align-items: center;
-      margin-top: 8px;
-
-      input{
-        margin-right: 12px;
-      }
-    }
-  }
-</style>

+ 8 - 8
src/views/student/components/StudentQuizDetail.vue

@@ -4,22 +4,22 @@
     <router-link v-if="quiz.state === QuizState.ongoing" :to="{ name: 'student-do-quiz', params: { quizId: quiz.id}}">
       <c-button class="mb-16" theme="green" icon="edit">开始做题</c-button>
     </router-link>
-    <quiz-detail-factory :quiz-detail="quiz" />
+    <quiz-detail-factory :quiz-detail="quiz"/>
   </div>
 </template>
 
 <script>
 import BackTitle from '@/components/Basic/BackTitle'
-import { getQuizById } from '@/server/quiz'
 import QuizDetailFactory from '@/views/quiz/components/QuizDetailFactory'
 import { QuizState } from '@/server/enums/quiz-state'
 import { createDefaultQuiz } from '@/views/quiz/utils/default'
 import CButton from '@/components/Basic/CButton'
+import { getOneQuiz } from '@/server/quiz'
 
 export default {
   name: 'StudentQuizDetail',
   components: { CButton, QuizDetailFactory, BackTitle },
-  data() {
+  data () {
     return {
       quiz: createDefaultQuiz(),
 
@@ -27,20 +27,20 @@ export default {
     }
   },
   computed: {
-    totalQuestionNums() {
+    totalQuestionNums () {
       return this.quiz.questions?.length ?? 0
     },
-    passQuestionNums() {
+    passQuestionNums () {
       return this.quiz.questions?.filter(item => item.pass).length
     }
   },
-  mounted() {
+  mounted () {
     this.fetchQuizDetail()
   },
   methods: {
-    fetchQuizDetail() {
+    fetchQuizDetail () {
       const { quizId } = this.$route.params
-      getQuizById(quizId)
+      getOneQuiz({ quizId })
         .then(res => (this.quiz = res))
         .catch(err => this.$setErrorMessage(err.response.data.msg))
     }

+ 2 - 2
src/views/student/components/StudentSlideDetail.vue

@@ -16,7 +16,7 @@
         下载课件
       </c-button>
     </a>
-<!--    <c-button block theme="green" class="mb-16" @click="handleToQuiz" icon="edit">查看测验</c-button>-->
+    <c-button block theme="green" class="mb-16" @click="handleToQuiz" icon="edit">查看测验</c-button>
   </tab-side-component>
 </template>
 
@@ -56,7 +56,7 @@ export default {
       this.$router.push({ name: 'pdf-reader', params: { slideId: this.slide.id } })
     },
     handleToQuiz () {
-      this.$router.push({ name: 'quiz', params: { slideId: this.slide.id } })
+      this.$router.push({ name: 'student-quiz-list', params: { slideId: this.slide.id } })
     }
   }
 }

+ 20 - 32
src/views/student/tabs/StudentQuiz.vue

@@ -1,7 +1,9 @@
 <template>
   <div class="tab complex-tab">
     <div class="left">
-      <h1 class="title">课程测验 <c-tag theme="green">学生</c-tag></h1>
+      <h1 class="title">课程测验
+        <c-tag theme="green">学生</c-tag>
+      </h1>
       <div class="show-quiz-type-bar mb-24">
         <c-button block :right="6" @click="showOngoing = true" :theme="showOngoing ? 'green' : 'normal'">进行中</c-button>
         <c-button block :left="6" @click="showOngoing = false" :theme="showOngoing ? 'normal' : 'green'">已结束</c-button>
@@ -9,51 +11,40 @@
       <div v-if="showOngoing">
         <div class="sub-title">
           待完成测验
-          <c-button text :left="4" theme="blue" :icon="showMore ? 'unfold_less' : 'unfold_more'" @click="showMore = !showMore">
-            {{showMore ? '收起' : '展开'}}详情
-          </c-button>
         </div>
         <quiz-card
-          :hide-slide-name="!showMore"
-          :hide-course-name="!showMore"
           :key="quiz.id" v-for="quiz in ongoingQuizzes"
-          :nav-path="{ name: 'student-quiz-detail', params: { quizId: quiz.id } }"
-          :quiz="quiz" hide-quiz-state hide-quiz-time />
+          :nav-path="{ name: 'student-do-quiz', params: { quizId: quiz.id}}"
+          :quiz="quiz" hide-quiz-state hide-quiz-time/>
       </div>
       <div v-else>
         <div class="sub-title">
           已结束测验
-          <c-button text :left="4" theme="blue" :icon="showMore ? 'unfold_less' : 'unfold_more'" @click="showMore = !showMore">
-            {{showMore ? '收起' : '展开'}}详情
-          </c-button>
         </div>
         <quiz-card
-          :hide-slide-name="!showMore"
-          :hide-course-name="!showMore"
           :key="quiz.id" v-for="quiz in closedQuizzes"
-          :nav-path="{ name: 'student-quiz-detail', params: { quizId: quiz.id } }"
-          :quiz="quiz" hide-quiz-state hide-quiz-time />
+          :nav-path="{ name: 'student-quiz-detail', params: { quizId: quiz.id}}"
+          :quiz="quiz" hide-quiz-state hide-quiz-time/>
       </div>
     </div>
-    <router-view />
+    <router-view/>
   </div>
 </template>
 
 <script>
-// import { getQuizList } from '@/server/quiz'
-// import { QuizState } from '@/server/enums/quiz-state'
 import CTag from '@/components/Basic/CTag'
 import QuizCard from '@/components/Cards/QuizCard'
 import CButton from '@/components/Basic/CButton'
+import { QuizState } from '@/server/enums/quiz-state'
+import { studentGetQuizzesByState } from '@/server/quiz'
 
 export default {
   components: { CButton, QuizCard, CTag },
-  data() {
+  data () {
     return {
       ongoingQuizzes: [],
       closedQuizzes: [],
-      showOngoing: true,
-      showMore: false
+      showOngoing: true
     }
   },
   watch: {
@@ -65,17 +56,14 @@ export default {
       }
     }
   },
-  mounted() {
-    this.$setInfoMessage('测试功能预计本周上线')
-    // getQuizList({ state: QuizState.ongoing })
-    //   .then(res => {
-    //     this.ongoingQuizzes = res.quizzes
-    //     this.showMore = res.quizzes.length < 5
-    //   })
-    //   .catch(err => this.$setErrorMessage(err.response.data.msg))
-    // getQuizList({ state: QuizState.closed })
-    //   .then(res => (this.closedQuizzes = res.quizzes))
-    //   .catch(err => this.$setErrorMessage(err.response.data.msg))
+  mounted () {
+    studentGetQuizzesByState({ state: QuizState.ongoing, size: 1000 }).then(res => {
+      this.ongoingQuizzes = res.data
+    }).catch(err => { this.$setErrorMessage(err.response.data.msg) })
+
+    studentGetQuizzesByState({ state: QuizState.closed, size: 1000 }).then(res => {
+      this.closedQuizzes = res.data
+    }).catch(err => { this.$setErrorMessage(err.response.data.msg) })
   }
 }
 </script>