Browse Source

feat: 收藏、评价功能

ChenYangfan 6 years ago
parent
commit
1a37ec47ca

+ 7 - 6
src/components/Pagination.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="pagination-wrapper">
-    <div class="pagination mt-16">
+    <div class="pagination mt-8">
       <c-button :theme="theme" :size="32" :left="12" :right="4" @click="handleChange({ page: page - 1 })">
         <c-icon bold :theme="theme" :size="16">keyboard_arrow_left</c-icon>
       </c-button>
@@ -27,7 +27,7 @@
         <c-icon :theme="theme" bold :size="16">keyboard_arrow_right</c-icon>
       </c-button>
     </div>
-    <div class="mt-16">
+    <div class="mt-8">
       <label>
         <input v-model="inputPage" class="page-input" @keypress="handlePageInputEvent"/>
         <span>/ {{ totalPage }}</span>
@@ -131,7 +131,7 @@ input {
   width: 100%;
   height: 40px;
   line-height: 1.5;
-  padding: 0 12px 0 12px;
+  padding: 0 8px 0 8px;
   transition: .2s;
 
   &:hover {
@@ -169,10 +169,11 @@ input {
 }
 
 .page-input {
-  width: 64px;
+  width: 40px;
+  font-size: 14px;
   height: 32px;
   border-radius: 0;
-  margin-right: 8px;
+  margin-right: 4px;
   color: #7a8a9a;
 
   & + span {
@@ -180,7 +181,7 @@ input {
   }
 }
 
-@media (max-width: 450px) {
+@media (max-width: 600px) {
   .c-button {
     margin: 0 4px;
   }

+ 3 - 3
src/router/quiz.js

@@ -37,16 +37,16 @@ export default [{
   meta: { roles: ['student'] },
   path: '/student/quiz/:quizId/do-quiz',
   name: 'student-do-quiz',
-  component: () => import(/* webpackChunkName: "student" */ '@/views/quiz/StudentDoQuiz.vue')
+  component: () => import(/* webpackChunkName: "student-quiz" */ '@/views/quiz/StudentDoQuiz.vue')
 }, {
   meta: { roles: ['student'] },
   path: '/student/quiz/:quizId',
   name: 'student-quiz-detail',
-  component: () => import(/* webpackChunkName: "student" */ '@/views/student/components/StudentQuizDetail.vue')
+  component: () => import(/* webpackChunkName: "student-quiz" */ '@/views/student/components/StudentQuizDetail.vue')
 }, {
   meta: { roles: ['student'] },
   path: '/student/questions',
   name: 'student-question-table',
-  component: () => import(/* webpackChunkName: "student" */ '@/views/quiz/StudentQuestionTable.vue')
+  component: () => import(/* webpackChunkName: "student-quiz" */ '@/views/quiz/StudentQuestions.vue')
 }
 ]

+ 0 - 46
src/views/quiz/StudentQuestionTable.vue

@@ -1,46 +0,0 @@
-<template>
-  <div class="student-question-table">
-    <back-title>
-      我的题库
-      <c-button :left="12" icon="assignment" :theme="getListFunction === getCompletedQuestions ? 'green' : 'normal'"
-                @click="getListFunction = getCompletedQuestions">已完成
-      </c-button>
-      <c-button :left="12" icon="bookmarks" :theme="getListFunction === getRatedQuestions ? 'green' : 'normal'"
-                @click="getListFunction = getRatedQuestions">已收藏
-      </c-button>
-      <c-button :left="12" icon="stars" :theme="getListFunction === getCollectedQuestions ? 'green' : 'normal'"
-                @click="getListFunction = getCollectedQuestions">已评价
-      </c-button>
-    </back-title>
-    <question-table :get-list-function="getListFunction"></question-table>
-  </div>
-</template>
-
-<script>
-import BackTitle from '@/components/Basic/BackTitle'
-import QuestionTable from '@/views/quiz/components/QuestionTable'
-import { getCompletedQuestions, getRatedQuestions, getCollectedQuestions } from '@/server/question'
-import CButton from '@/components/Basic/CButton'
-
-export default {
-  components: { CButton, QuestionTable, BackTitle },
-  data () {
-    return {
-      getCompletedQuestions,
-      getRatedQuestions,
-      getCollectedQuestions,
-
-      getListFunction: getCompletedQuestions
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.student-question-table {
-  margin: 0 auto;
-  max-width: 1000px;
-  padding: 32px 24px 32px 24px;
-}
-
-</style>

+ 55 - 0
src/views/quiz/StudentQuestions.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="student-questions">
+    <back-title>
+      <div>
+        我的题库
+      </div>
+      <div class="mb-16 mt-16">
+        <c-button :left="8" icon="assignment" :theme="getListFunction === getCompletedQuestions ? 'green' : 'normal'"
+                  @click="getListFunction = getCompletedQuestions">已完成
+        </c-button>
+        <c-button :left="8" icon="bookmarks" :theme="getListFunction === getRatedQuestions ? 'green' : 'normal'"
+                  @click="getListFunction = getRatedQuestions">已收藏
+        </c-button>
+        <c-button :left="8" icon="stars" :theme="getListFunction === getCollectedQuestions ? 'green' : 'normal'"
+                  @click="getListFunction = getCollectedQuestions">已评价
+        </c-button>
+      </div>
+    </back-title>
+    <student-question-table :get-list-function="getListFunction"></student-question-table>
+  </div>
+</template>
+
+<script>
+import BackTitle from '@/components/Basic/BackTitle'
+import { getCompletedQuestions, getRatedQuestions, getCollectedQuestions } from '@/server/question'
+import CButton from '@/components/Basic/CButton'
+import StudentQuestionTable from '@/views/quiz/components/StudentQuestionTable'
+
+export default {
+  components: { StudentQuestionTable, CButton, BackTitle },
+  data () {
+    return {
+      getCompletedQuestions,
+      getRatedQuestions,
+      getCollectedQuestions,
+
+      getListFunction: getCompletedQuestions
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.student-questions {
+  box-sizing: border-box;
+  margin: 0 auto;
+  max-width: 1000px;
+  padding: 32px 24px 32px 24px;
+}
+
+.back-title {
+  flex-wrap: wrap;
+  margin-bottom: 0;
+}
+</style>

+ 5 - 37
src/views/quiz/components/DoQuizQuestionCard.vue

@@ -24,12 +24,7 @@
         </div>
       </div>
     </div>
-    <div class="action-bar">
-      <c-stars title="评分" theme="orange" :level="5" v-model="rate"></c-stars>
-      <icon-button no-bold :left="12" theme="orange" title="收藏 / 取消收藏" @click="marked = !marked">
-        {{marked ? 'bookmark' : 'bookmark_border'}}
-      </icon-button>
-    </div>
+    <mark-action-bar :marked-prop="markedProp" :rate-prop="rateProp" :question-id="question.id"></mark-action-bar>
   </div>
 </template>
 
@@ -37,13 +32,11 @@
 import CIcon from '@/components/Basic/CIcon'
 import MarkdownSection from '@/views/quiz/components/MarkdownSection'
 import QuestionKind from '@/enums/question-kind'
-import CStars from '@/components/Basic/CStars'
-import IconButton from '@/components/Basic/IconButton'
-import { rateQuestion, starQuestion } from '@/server/question'
+import MarkActionBar from '@/views/quiz/components/MarkActionBar'
 
 // fixme: 通过几层reference传值可能会造成一点困惑 - 咕咕咕
 export default {
-  components: { IconButton, CStars, CIcon, MarkdownSection },
+  components: { MarkActionBar, CIcon, MarkdownSection },
   props: {
     questionProp: Object,
     index: Number,
@@ -53,35 +46,12 @@ export default {
     },
     markedProp: Boolean
   },
-
   data () {
     return {
       question: null,
-      rate: this.rateProp,
-      marked: this.markedProp,
       QuestionKind
     }
   },
-  watch: {
-    markedProp (newValue) {
-      this.marked = newValue
-    },
-    rateProp (newValue) {
-      this.rate = newValue
-    },
-    rate (newValue) {
-      if (newValue !== 0 && newValue !== this.rateProp) {
-        rateQuestion({ questionId: this.question.id, rate: newValue })
-          .catch(err => this.$setErrorMessage(err.response.data.msg))
-      }
-    },
-    marked (newValue) {
-      if (newValue !== 0 && newValue !== this.markedProp) {
-        starQuestion({ questionId: this.question.id, collect: newValue })
-          .catch(err => this.$setErrorMessage(err.response.data.msg))
-      }
-    }
-  },
   created () {
     this.question = this.questionProp
     this.$watch('questionProp', newValue => {
@@ -122,16 +92,14 @@ export default {
   border-left: 6px solid $card-background;
 }
 
-.action-bar {
+.mark-action-bar {
   opacity: 0.4;
   display: flex;
   justify-content: flex-end;
   align-items: center;
   padding: 0 16px 6px 16px;
   position: relative;
-  border-top: 100px;
   transition: .2s;
-  backdrop-filter: saturate(200%);
 
   &:hover {
     opacity: 1;
@@ -139,7 +107,7 @@ export default {
 }
 
 @media (max-width: 500px) {
-  .action-bar {
+  .mark-action-bar {
     opacity: 1;
   }
 }

+ 65 - 0
src/views/quiz/components/MarkActionBar.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="mark-action-bar">
+    <c-stars title="评分" :theme="theme" :level="5" v-model="rate"></c-stars>
+    <icon-button no-bold :left="12" :theme="theme" title="收藏 / 取消收藏" @click="marked = !marked">
+      {{marked ? 'bookmark' : 'bookmark_border'}}
+    </icon-button>
+  </div>
+</template>
+
+<script>
+import CStars from '@/components/Basic/CStars'
+import IconButton from '@/components/Basic/IconButton'
+import { rateQuestion, starQuestion } from '@/server/question'
+export default {
+  name: 'MarkActionBar',
+  components: { IconButton, CStars },
+  props: {
+    rateProp: {
+      type: Number,
+      default: 0
+    },
+    markedProp: Boolean,
+    questionId: String,
+    theme: {
+      type: String,
+      default: 'orange'
+    }
+  },
+  data () {
+    return {
+      rate: this.rateProp,
+      marked: this.markedProp
+    }
+  },
+  watch: {
+    markedProp (newValue) {
+      this.marked = newValue
+    },
+    rateProp (newValue) {
+      this.rate = newValue
+    },
+    rate (newValue) {
+      if (newValue !== 0 && newValue !== this.rateProp) {
+        rateQuestion({ questionId: this.questionId, rate: newValue })
+          .then(_ => this.$setSuccessMessage('评价成功'))
+          .catch(err => this.$setErrorMessage(err.response.data.msg))
+      }
+    },
+    marked (newValue) {
+      if (newValue !== 0 && newValue !== this.markedProp) {
+        starQuestion({ questionId: this.questionId, collect: newValue })
+          .then(_ => this.$setSuccessMessage('收藏成功'))
+          .catch(err => this.$setErrorMessage(err.response.data.msg))
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.mark-action-bar {
+  display: flex;
+  align-items: center;
+}
+</style>

+ 0 - 3
src/views/quiz/components/QuestionCard.vue

@@ -17,9 +17,6 @@
         <router-link v-if="!studentMode" :to="{ name: 'question-detail', params: { questionId }}">
           <icon-button no-bold theme="blue" :size="32">more_horiz</icon-button>
         </router-link>
-        <router-link v-else :to="{ name: 'question-detail', params: { questionId }}">
-          <icon-button no-bold theme="blue" :size="32">more_horiz</icon-button>
-        </router-link>
       </div>
     </div>
     <markdown-section :raw="stem"/>

+ 2 - 12
src/views/quiz/components/QuestionTable.vue

@@ -6,7 +6,6 @@
     <div class="question-card-container">
       <template v-for="item in questionList">
         <question-card
-          :student-mode="studentMode"
           v-if="item !== null"
           :key="item.id"
           :editable="ownQuestionIds.indexOf(item.id) !== -1"
@@ -45,12 +44,6 @@ export default {
     showOwn: {
       type: Boolean,
       default: false
-    },
-    // 学生模式补丁,会将教师端操作屏蔽
-    studentMode: Boolean,
-    getListFunction: {
-      type: Function,
-      default: getQuestionList
     }
   },
   model: {
@@ -63,9 +56,6 @@ export default {
   watch: {
     showOwn () {
       this.fetchQuestionList()
-    },
-    getListFunction () {
-      this.fetchQuestionList()
     }
   },
   data () {
@@ -84,7 +74,7 @@ export default {
   },
   mounted () {
     this.fetchQuestionList()
-    this.studentMode && this.fetchOwnQuestionIds()
+    this.fetchOwnQuestionIds()
   },
   methods: {
     fetchQuestionList ({
@@ -98,7 +88,7 @@ export default {
             this.questionList = questions
             this.page = page
           })
-        : this.getListFunction({ key: stem, size, page })
+        : getQuestionList({ key: stem, size, page })
           .then(({ data: questions, page }) => {
             this.questionList = questions
             this.page = page

+ 39 - 5
src/views/quiz/components/QuizDetailFactory.vue

@@ -24,10 +24,15 @@
           </c-icon>
           <div v-if="answers[question.id]">
             {{typeof answers[question.id].answer === 'boolean'
-              ? answers[question.id].answer ? '对' : '错'
-              : answers[question.id].answer}}
+            ? answers[question.id].answer ? '对' : '错'
+            : answers[question.id].answer}}
           </div>
         </template>
+        <div class="spacer"></div>
+        <mark-action-bar :question-id="question.id" theme="blue"
+                         :marked-prop="interactiveMap ? interactiveMap[question.id].marked : false"
+                         :rate-prop="interactiveMap ? interactiveMap[question.id].rate : 0">
+        </mark-action-bar>
       </div>
       <question-detail-factory :question="question"/>
     </div>
@@ -42,11 +47,12 @@ import QuestionDetailFactory from '@/views/quiz/components/QuestionDetailFactory
 import { isEmpty } from '@/utils/types'
 import { QuizTypeNaming } from '@/enums/quiz-type'
 import momentFromNow from '@/utils/moment-from-now'
-import { getQuestionsByQuiz } from '@/server/question'
+import { getInteractiveList, getQuestionsByQuiz } from '@/server/question'
 import CIcon from '@/components/Basic/CIcon'
+import MarkActionBar from '@/views/quiz/components/MarkActionBar'
 
 export default {
-  components: { CIcon, QuestionDetailFactory },
+  components: { MarkActionBar, CIcon, QuestionDetailFactory },
   props: {
     quizDetail: {
       type: Object,
@@ -59,6 +65,7 @@ export default {
   data () {
     return {
       questions: null,
+      interactiveMap: null,
       QuizTypeNaming,
       QuestionKind,
       QuizState,
@@ -77,6 +84,13 @@ export default {
     const { quizId } = this.$route.params
     getQuestionsByQuiz({ quizId }).then(res => {
       this.questions = res
+      getInteractiveList({ questionIdJoinByComma: this.questions.map(_ => _.id).join(',') }).then(res => {
+        const tempMap = {}
+        res.forEach(item => {
+          tempMap[item.questionId] = { marked: item.collection, rate: item.rate }
+        })
+        this.interactiveMap = tempMap
+      })
     }).catch(err => { this.$setErrorMessage(err.response.data.msg) })
   },
   methods: {
@@ -108,8 +122,28 @@ export default {
   display: flex;
   align-items: center;
   color: $theme-blue;
-  padding: 6px 8px;
+  padding: 0 12px;
   margin: 16px -8px;
   background-color: $theme-blue-transparent;
 }
+
+.mark-action-bar {
+  opacity: 0.4;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 0 8px;
+  position: relative;
+  transition: .2s;
+
+  &:hover {
+    opacity: 1;
+  }
+}
+
+@media (max-width: 500px) {
+  .mark-action-bar {
+    opacity: 1;
+  }
+}
 </style>

+ 154 - 0
src/views/quiz/components/StudentQuestionTable.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="student-question-table">
+    <div v-if="questionsList === null" class="no-content-warning">加载中...</div>
+    <div v-else-if="questionsList.length === 0" class="no-content-warning">无内容</div>
+    <template v-else>
+      <div class="left">
+        <template v-for="question of questionsList">
+          <question-card
+            @click.native="onShowQuestion === question ? onShowQuestion = null : onShowQuestion = question"
+            v-if="question !== null"
+            :style="onShowQuestion === question ? 'box-shadow: 0 4px 6px rgba(0, 0, 0, .1);' : ''"
+            :key="question.id"
+            :type="question.type"
+            :stem="question.stem"
+            :question-id="question.id"
+            student-mode
+          />
+        </template>
+      </div>
+      <div class="right">
+        <div v-if="onShowQuestion">
+          <question-detail-factory :question="onShowQuestion"></question-detail-factory>
+          <mark-action-bar v-if="interactiveMap" :marked-prop="interactiveMap[onShowQuestion.id].marked"
+                           :rate-prop="interactiveMap[onShowQuestion.id].rate"
+                           :question-id="onShowQuestion.id"></mark-action-bar>
+        </div>
+        <div v-else-if="questionsList && questionsList.length > 0" class="no-content-warning">未选中具体题目</div>
+        <pagination @change="handlePageChange" v-show="questionsList && questionsList.length > 0" :page="page"
+                    :size="size"
+                    :total-elements="totalElements"></pagination>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import Pagination from '@/components/Pagination'
+import QuestionDetailFactory from '@/views/quiz/components/QuestionDetailFactory'
+import QuestionCard from '@/views/quiz/components/QuestionCard'
+import MarkActionBar from '@/views/quiz/components/MarkActionBar'
+import { getInteractiveList } from '@/server/question'
+
+export default {
+  name: 'StudentQuestionTable',
+  components: { MarkActionBar, QuestionCard, QuestionDetailFactory, Pagination },
+  props: {
+    getListFunction: Function
+  },
+  data () {
+    return {
+      questionsList: null,
+      onShowQuestion: null,
+      page: 1,
+      size: 6,
+      totalElements: 0,
+      interactiveMap: null
+    }
+  },
+  watch: {
+    getListFunction: {
+      handler () {
+        this.fetchQuestions()
+        this.onShowQuestion = null
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    fetchQuestions () {
+      this.questionsList = null
+      this.interactiveMap = null
+      this.getListFunction && this.getListFunction({ page: this.page, size: this.size })
+        .then(res => {
+          this.totalElements = res.page.totalElements
+          this.questionsList = res.data
+          if (this.page > res.page.totalPages) {
+            this.page = res.page.totalPages
+            this.fetchQuestions()
+          }
+          getInteractiveList({ questionIdJoinByComma: res.data.map(_ => _.id).join(',') }).then(res => {
+            const tempMap = {}
+            res.forEach(item => {
+              tempMap[item.questionId] = { marked: item.collection, rate: item.rate }
+            })
+            this.interactiveMap = tempMap
+          }).catch(err => this.$setErrorMessage(err.response.data.msg))
+        })
+        .catch(err => this.$setErrorMessage(err))
+    },
+    handlePageChange ({ page }) {
+      this.page = page
+      this.onShowQuestion = null
+      this.fetchQuestions()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "~@/style/theme.scss";
+
+@media (min-width: 720px) {
+  .student-question-table {
+    display: flex;
+    box-sizing: border-box;
+    background-color: #f8f8f8;
+    justify-content: center;
+    min-height: 350px;
+  }
+
+  .left {
+    padding: 24px;
+    width: 1px;
+    max-width: 350px;
+    flex: 1;
+  }
+
+  .right {
+    width: 1px;
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    min-height: 200px;
+
+    .no-content-warning {
+      height: unset;
+    }
+  }
+}
+
+.right {
+  margin-top: 16px;
+  padding: 24px;
+}
+
+.question-card {
+  cursor: pointer;
+  margin-top: 12px;
+  break-inside: avoid;
+  border-radius: 12px;
+  background-color: white;
+  transition: .2s;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, .05);
+
+  &:hover {
+    box-shadow: 2px 4px 6px rgba(0, 0, 0, .05);
+  }
+
+  &:last-child {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 1 - 0
src/views/quiz/components/TrueFalseQuestionDetail.vue

@@ -46,4 +46,5 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@import "~@/views/quiz/style/question-detail.scss";
 </style>

+ 0 - 22
src/views/quiz/student-question-table-component/CompletedQuestions.vue

@@ -1,22 +0,0 @@
-<template>
-  <div>
-    <question-table :get-list-function="getCompletedQuestions"></question-table>
-  </div>
-</template>
-
-<script>
-import QuestionTable from '@/views/quiz/components/QuestionTable'
-import { getCompletedQuestions } from '@/server/question'
-export default {
-  components: { QuestionTable },
-  data () {
-    return {
-      getCompletedQuestions
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-
-</style>

+ 0 - 13
src/views/quiz/student-question-table-component/MarkedQuestions.vue

@@ -1,13 +0,0 @@
-<template>
-  <div>
-    2
-  </div>
-</template>
-
-<script>
-export default {}
-</script>
-
-<style lang="scss" scoped>
-
-</style>

+ 0 - 13
src/views/quiz/student-question-table-component/RatedQuestions.vue

@@ -1,13 +0,0 @@
-<template>
-  <div>
-    3
-  </div>
-</template>
-
-<script>
-export default {}
-</script>
-
-<style lang="scss" scoped>
-
-</style>