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

feat: 添加完善教师、学生与课程课件相关内容的诸多功能

ChenYangfan пре 6 година
родитељ
комит
1dab3e71a0

+ 1 - 0
src/App.vue

@@ -67,6 +67,7 @@ body {
   display: flex;
   flex-direction: column;
   justify-content: center;
+  line-height: 1.8;
 
   .no-content-warning-tip {
     padding: 24px 0;

+ 1 - 1
src/components/CourseCard.vue

@@ -11,7 +11,7 @@ export default {
   props: ['index', 'clickHandler', 'course'],
   methods: {
     handleClick () {
-      this.clickHandler(this.course.id)
+      this.clickHandler(this.course)
     }
   }
 }

+ 22 - 7
src/components/DiscussionArea.vue

@@ -1,17 +1,24 @@
 <template>
   <div class="discussion-area">
     <h1 class="title">讨论 & 问答</h1>
-    <c-comment></c-comment>
-    <c-comment></c-comment>
-    <c-comment></c-comment>
+    <discussion-type-bar></discussion-type-bar>
+    <div class="discussion-area-content">
+      <c-comment></c-comment>
+      <c-comment></c-comment>
+      <c-comment></c-comment>
+      <c-comment></c-comment>
+      <c-comment></c-comment>
+      <c-comment></c-comment>
+    </div>
   </div>
 </template>
 
 <script>
 import CComment from '@/components/CComment'
+import DiscussionTypeBar from '@/components/DiscussionTypeBar'
 
 export default {
-  components: { CComment }
+  components: { DiscussionTypeBar, CComment }
 }
 </script>
 
@@ -19,12 +26,17 @@ export default {
 .discussion-area {
   margin-top: 16px;
   box-sizing: border-box;
-  overflow: auto;
-  min-width: 350px;
+  min-width: 380px;
   flex: 1;
   padding: 0 24px;
 }
 
+.discussion-area-content {
+  overflow: auto;
+  margin: 8px -12px;
+  padding: 0 12px;
+}
+
 @media (max-width: 500px) {
   .discussion-area {
     padding: 0;
@@ -35,10 +47,13 @@ export default {
 @media (min-width: 1000px) {
   .discussion-area {
     margin-top: unset;
-    max-height: 80vh;
     max-width: 500px;
     padding: 0 40px;
     min-height: 60vh;
   }
+
+  .discussion-area-content {
+    max-height: 80vh;
+  }
 }
 </style>

+ 74 - 0
src/components/DiscussionTypeBar.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="discussion-type-bar">
+    <button
+      v-for="button of buttons"
+      :key="button.value"
+      :class="mode === button.value ? 'active' : ''"
+      @click="changeMode(button.value)"
+    >{{ button.name }}
+    </button>
+  </div>
+</template>
+
+<script>
+export default {
+  props: ['defaultMode'],
+  data () {
+    return {
+      mode: this.defaultMode || 'comment',
+      buttons: [
+        {
+          value: 'comment',
+          name: '讨论'
+        },
+        {
+          value: 'question',
+          name: '问答'
+        }
+      ]
+    }
+  },
+  methods: {
+    changeMode (value) {
+      this.mode = value
+      this.$emit('change-mode', this.mode)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "~@/style/theme.scss";
+.discussion-type-bar {
+  margin: 0 -2px;
+  padding: 0 2px;
+  box-sizing: border-box;
+  display: flex;
+  background: $theme-blue-transparent;
+  border-radius: 12px;
+}
+
+button {
+  background: transparent;
+  margin: 4px 2px;
+  border: none;
+  outline: none;
+  display: block;
+  flex: 1;
+  color: #3a4a5a;
+  font-weight: bold;
+  padding: 8px 12px;
+  border-radius: 8px;
+  transition: .2s;
+  &:hover {
+    background: rgba(255,255,255,.5);
+  }
+}
+
+.active {
+  background: white;
+  &:hover {
+    background: white;
+  }
+}
+</style>

+ 3 - 2
src/components/SearchBar.vue

@@ -4,7 +4,7 @@
       <input
         type="text"
         :value="value"
-        placeholder="输入关键词搜索"
+        :placeholder="placeholder || '输入关键词搜索'"
         @input="$emit('input', $event.target.value)"
       />
     </label>
@@ -20,7 +20,8 @@ export default {
     event: 'input'
   },
   props: {
-    value: String
+    value: String,
+    placeholder: String
   },
   data () {
     return {

+ 25 - 3
src/router/index.js

@@ -28,6 +28,10 @@ const routes = [{
   path: '/quiz-setting/:slideId',
   name: 'teacher-quiz-setting',
   component: () => import('../views/quiz/TeacherQuizSetting.vue')
+}, {
+  path: '/quiz/:slideId',
+  name: 'quiz',
+  component: () => import('../views/quiz/QuizView.vue')
 }, {
   path: '/teacher',
   name: 'teacher',
@@ -42,7 +46,7 @@ const routes = [{
       name: 'teacher-course-slides',
       component: () => import('../views/teacher/components/TeacherCourseSlides.vue')
     }, {
-      path: 'slide-detail',
+      path: 'slide-detail/:slideId',
       name: 'teacher-explore-slide-detail',
       component: () => import('../views/teacher/components/TeacherSlideDetail.vue')
     }]
@@ -80,7 +84,16 @@ const routes = [{
   children: [{
     path: 'explore',
     name: 'student-explore',
-    component: () => import('../views/student/tabs/StudentExplore.vue')
+    component: () => import('../views/student/tabs/StudentExplore.vue'),
+    children: [{
+      path: 'course-slides/:courseId',
+      name: 'student-course-slides',
+      component: () => import('../views/student/components/StudentCourseSlides.vue')
+    }, {
+      path: 'slide-detail/:slideId',
+      name: 'student-explore-slide-detail',
+      component: () => import('../views/student/components/StudentSlideDetail.vue')
+    }]
   }, {
     path: 'quiz',
     name: 'student-quiz',
@@ -88,7 +101,16 @@ const routes = [{
   }, {
     path: 'courses',
     name: 'student-courses',
-    component: () => import('../views/student/tabs/StudentCoursesManager.vue')
+    component: () => import('../views/student/tabs/StudentCoursesManager.vue'),
+    children: [{
+      path: 'join-course/:courseId',
+      name: 'join-course',
+      component: () => import('../views/student/components/JoinCourse.vue')
+    }, {
+      path: 'course-detail/:courseId',
+      name: 'student-course-detail',
+      component: () => import('../views/student/components/StudentCourseDetail.vue')
+    }]
   }, {
     path: 'discussion',
     name: 'student-discussion',

+ 2 - 3
src/server/user.js

@@ -34,10 +34,9 @@ export function logout () {
     .then(res => {})
 }
 
-export function modifyUser ({ name, password }) {
+export function modifyUser ({ name }) {
   const data = {
-    name,
-    password
+    name
   }
   return axios
     .put(useUserServer(), data)

+ 4 - 2
src/store/index.js

@@ -1,7 +1,8 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
-import pdf from './modules/pdf'
-import message from './modules/message'
+import pdf from '@/store/modules/pdf'
+import message from '@/store/modules/message'
+import main from '@/store/modules/main'
 
 Vue.use(Vuex)
 
@@ -10,6 +11,7 @@ export default new Vuex.Store({
   mutations: {},
   actions: {},
   modules: {
+    main,
     message,
     pdf
   }

+ 16 - 0
src/store/modules/main.js

@@ -0,0 +1,16 @@
+import { RESET_COURSE, SET_COURSE } from '@/store/types/main-types'
+
+export default {
+  state: {
+    courseInfo: {}
+  },
+
+  mutations: {
+    [SET_COURSE] (state, payload) {
+      state.courseInfo = payload.courseInfo
+    },
+    [RESET_COURSE] (state) {
+      state.courseInfo = {}
+    }
+  }
+}

+ 2 - 0
src/store/types/main-types.js

@@ -0,0 +1,2 @@
+export const SET_COURSE = 'setCourse'
+export const RESET_COURSE = 'resetCourse'

+ 16 - 0
src/utils/get-slide-next-state.js

@@ -0,0 +1,16 @@
+export default function getSlideNextState (state) {
+  switch (state) {
+    case 'DRAFT':
+      return 'BEFORE_CLASS'
+    case 'BEFORE_CLASS':
+      return 'IN_CLASS'
+    case 'IN_CLASS':
+      return 'AFTER_CLASS'
+    case 'AFTER_CLASS':
+      return 'FINISH'
+    case 'FINISH':
+      return 'AFTER_CLASS'
+    default:
+      return null
+  }
+}

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

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    学生做题界面(待完成)
+  </div>
+</template>
+
+<script>
+export default {}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 1 - 1
src/views/reader/PdfReader.vue

@@ -56,13 +56,13 @@ export default {
 @media (max-width: 1000px) {
   .back-title {
     padding: 0;
+    margin-top: 16px;
   }
 }
 
 @media (max-width: 600px) {
   .back-title {
     padding: 0 24px;
-    margin-top: 16px;
   }
 }
 

+ 48 - 0
src/views/student/components/JoinCourse.vue

@@ -0,0 +1,48 @@
+<template>
+  <tab-side-component>
+    <back-title>加入课程</back-title>
+    <div class="sub-title">请输入选课码</div>
+    <c-input placeholder="选课码" v-model="code"></c-input>
+    <c-button class="secondary full-width mb-24" @click="handleJoinCourse">确认选课</c-button>
+    <div class="sub-title">课程信息</div>
+  </tab-side-component>
+</template>
+
+<script>
+import TabSideComponent from '@/components/TabSideComponent'
+import CInput from '@/components/CInput'
+import CButton from '@/components/CButton'
+import BackTitle from '@/components/BackTitle'
+import { mapState } from 'vuex'
+import { chooseCourse } from '@/server/course'
+
+export default {
+  components: { BackTitle, CButton, CInput, TabSideComponent },
+  data () {
+    return {
+      code: ''
+    }
+  },
+  computed: {
+    ...mapState({
+      course: state => state.main.courseInfo
+    })
+  },
+  methods: {
+    handleJoinCourse () {
+      chooseCourse({ courseId: this.course.id, code: this.code }).then(res => {
+        this.$setSuccessMessage('成功加入课程:' + this.course.name)
+        this.$router.push({ name: 'student-explore' })
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-input {
+  margin-bottom: 24px;
+}
+</style>

+ 79 - 0
src/views/student/components/StudentCourseDetail.vue

@@ -0,0 +1,79 @@
+<template>
+  <tab-side-component>
+    <back-title>课程详情</back-title>
+    <div class="detail">
+      <div>课程名称:{{course.name}}</div>
+      <div>课程简介:{{course.bio}}</div>
+      <div>创建时间:{{createTime}}</div>
+      <div>教师:{{course.teacherName}}</div>
+    </div>
+    <c-button class="secondary full-width mb-16" @click="handleCheckSlideList">点击查看课件列表</c-button>
+    <c-button class="error full-width">退出本课程</c-button>
+  </tab-side-component>
+</template>
+
+<script>
+import TabSideComponent from '@/components/TabSideComponent'
+import CButton from '@/components/CButton'
+import BackTitle from '@/components/BackTitle'
+import { mapState } from 'vuex'
+
+export default {
+  components: { BackTitle, CButton, TabSideComponent },
+  data () {
+    return {
+      newCode: ''
+    }
+  },
+  computed: {
+    ...mapState({
+      course: state => state.main.courseInfo
+    }),
+    createTime () {
+      return this.$moment(this.course.createAt).fromNow()
+    }
+  },
+  methods: {
+    handleCheckSlideList () {
+      this.$router.push({ name: 'student-course-slides' })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.detail {
+  font-weight: bold;
+  font-size: 14px;
+  margin-bottom: 24px;
+  color: #7a8a9a;
+
+  div {
+    margin-top: 2px;
+
+  }
+
+  div:not(:last-child) {
+    border-bottom: 1px dashed #aabaca;
+    padding-bottom: 2px;
+  }
+}
+
+.c-input {
+  margin-bottom: 24px;
+}
+
+.change-wrapper {
+  display: flex;
+  align-items: center;
+
+  & .c-input {
+    margin-bottom: unset;
+    flex: 1;
+  }
+
+  & .c-button {
+    margin-left: 12px;
+  }
+}
+</style>

+ 77 - 0
src/views/student/components/StudentCourseSlides.vue

@@ -0,0 +1,77 @@
+<template>
+  <tab-side-component>
+    <back-title>{{'软件工程与计算 I'}}</back-title>
+    <div class="sub-title">课件列表</div>
+    <div class="no-content-warning" v-if="!slides || slides.length === 0">
+      <div class="no-content-warning-tip">本课程尚无课件~</div>
+      <c-button class="full-width secondary" @click="handleToCreateCourse">去创建课件</c-button>
+    </div>
+    <template v-for="slide of slides">
+      <slide-card :key="slide.id" :slide="slide" :click-handler="handleSlideCardClick"></slide-card>
+    </template>
+    <c-button class="full-width secondary mt-16" @click="handleToManager">管理本课程</c-button>
+  </tab-side-component>
+</template>
+
+<script>
+import TabSideComponent from '@/components/TabSideComponent'
+import BackTitle from '@/components/BackTitle'
+import SlideCard from '@/components/SlideCard'
+import CButton from '@/components/CButton'
+import { studentGetSlidesByCourse } from '@/server/slide'
+import { SET_SLIDE_INFO } from '@/store/types/pdf-types'
+import { SET_COURSE } from '@/store/types/main-types'
+import { mapState } from 'vuex'
+
+export default {
+  components: { CButton, SlideCard, BackTitle, TabSideComponent },
+  data () {
+    return {
+      slides: []
+    }
+  },
+  watch: {
+    $route () {
+      this.init()
+    }
+  },
+  computed: {
+    ...mapState({
+      course: state => state.main.courseInfo
+    })
+  },
+  mounted () {
+    this.init()
+  },
+  methods: {
+    init () {
+      studentGetSlidesByCourse({ courseId: this.course.id, sort: 'name' }).then(res => {
+        this.slides = res.data
+      }).catch(err => {
+        this.$setErrorMessage('获取课程课件列表过程中:' + err.response.data.msg)
+      })
+    },
+    handleSlideCardClick (slide) {
+      this.$store.commit(SET_SLIDE_INFO, { slideInfo: slide })
+      this.$router.push({ name: 'student-explore-slide-detail', params: { slideId: slide.id } })
+    },
+    handleToManager () {
+      this.$store.commit(SET_COURSE, { courseInfo: this.course })
+      this.$router.push({ name: 'student-course-detail', params: { courseId: this.course.id } })
+    },
+    handleToCreateCourse () {
+      this.$router.push({ name: 'create-slide' })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-input {
+  margin-bottom: 24px;
+}
+
+.no-content-warning {
+  height: unset;
+}
+</style>

+ 113 - 0
src/views/student/components/StudentSlideDetail.vue

@@ -0,0 +1,113 @@
+<template>
+  <tab-side-component>
+    <back-title>课件详情</back-title>
+    <div class="detail">
+      <div>课件名称:{{slide.name}}</div>
+      <div>所属课程:{{slide.courseName}}</div>
+      <div>创建时间:{{createTime}}</div>
+      <div>最后更新:{{updateTime}}</div>
+      <div>总页面数:{{slide.pages}} 页</div>
+      <div>课件状态:{{slideStateTranslation[slide.state]}}</div>
+    </div>
+    <c-button class="full-width mb-16" @click="handleReadSlide">浏览课件</c-button>
+    <a :href="slideUrl">
+      <c-button class="full-width mb-16 secondary">
+        下载课件
+      </c-button>
+    </a>
+    <c-button class="full-width secondary mb-16" @click="handleToQuiz">去做题</c-button>
+  </tab-side-component>
+</template>
+
+<script>
+import TabSideComponent from '@/components/TabSideComponent'
+import CButton from '@/components/CButton'
+import BackTitle from '@/components/BackTitle'
+import { mapState } from 'vuex'
+import { getSlideUrl } from '@/server/slide'
+import { SET_SRC } from '@/store/types/pdf-types'
+import slideStateTranslation from '@/server/enums/slide-state-translation'
+
+export default {
+  components: { BackTitle, CButton, TabSideComponent },
+  data () {
+    return {
+      slideUrl: '',
+      slideStateTranslation: slideStateTranslation
+    }
+  },
+  computed: {
+    ...mapState({
+      slide: state => state.pdf.slideInfo
+    }),
+    updateTime () {
+      return this.$moment(this.slide.updateAt).fromNow()
+    },
+    createTime () {
+      return this.$moment(this.slide.createAt).fromNow()
+    }
+  },
+  mounted () {
+    getSlideUrl({ slideId: this.slide.id }).then(res => {
+      this.slideUrl = res
+      this.$store.commit(SET_SRC, { src: res })
+    }).catch(err => {
+      this.$setErrorMessage('获取课件链接的过程中:' + err.response.data.msg)
+    })
+  },
+  methods: {
+    handleReadSlide () {
+      this.$router.push({ name: 'pdf-reader', params: { slide: this.slide } })
+    },
+    handleToQuiz () {
+      this.$router.push({ name: 'quiz', params: { slideId: this.slide.id } })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@/style/theme.scss';
+
+.detail {
+  font-weight: bold;
+  font-size: 14px;
+  margin-bottom: 24px;
+  color: #7a8a9a;
+
+  div {
+    margin-top: 2px;
+
+  }
+
+  div:not(:last-child) {
+    border-bottom: 1px dashed #aabaca;
+    padding-bottom: 2px;
+  }
+}
+
+.sub-title {
+  margin-bottom: 24px;
+}
+
+.re-upload-wrapper {
+  display: flex;
+  align-items: center;
+  margin-bottom: 24px;
+
+  .c-button {
+    margin-left: 12px;
+    padding: 0 8px;
+  }
+}
+
+.delete-slide {
+  color: $theme-red
+}
+
+@media (max-width: 600px) {
+  .file-uploader {
+    font-size: 14px;
+  }
+}
+</style>

+ 70 - 6
src/views/student/tabs/StudentCoursesManager.vue

@@ -2,21 +2,80 @@
   <div class="tab complex-tab">
     <div class="left">
       <h1 class="title">课程管理</h1>
-      <course-card :course="{}" :index="1"></course-card>
-      <course-card :course="{}" :index="2"></course-card>
-      <c-button class="full-width secondary">新建课程</c-button>
+      <search-bar placeholder="输入关键词搜索课程" class="mb-24" v-model="searchKey"></search-bar>
+      <template v-if="searchKey && searchResult !== null">
+        <div v-if="this.searchResult.length && this.searchResult.length !== 0" class="search-result">
+          <div class="mb-8">"{{searchKey}}" 的相关搜索结果</div>
+          <template v-for="(course, index) of searchResult">
+            <course-card :key="course.id" :course="course" :index="index + 1"
+                         :clickHandler="handleToSelectCourse"></course-card>
+          </template>
+        </div>
+        <div v-else class="no-content-warning">
+          找不到 "{{searchKey}}" 的相关搜索结果
+        </div>
+      </template>
+      <div class="sub-title">我的课程</div>
+      <div v-if="!courses || courses.length === 0" class="no-content-warning">
+        您还没有加入任何课程<br>
+        在上方输入关键字搜索吧~
+      </div>
+      <template v-for="(course, index) of courses">
+        <course-card :key="course.id" :course="course" :index="index + 1"
+                     :clickHandler="handleToCourseDetail"></course-card>
+      </template>
     </div>
     <router-view/>
   </div>
 </template>
 
 <script>
-import CButton from '@/components/CButton'
 import CourseCard from '@/components/CourseCard'
+import SearchBar from '@/components/SearchBar'
+import { getCourses, studentGetCourses } from '@/server/course'
+import { SET_COURSE } from '@/store/types/main-types'
 
 export default {
-  components: { CButton, CourseCard },
-  methods: {}
+  components: { SearchBar, CourseCard },
+  data () {
+    return {
+      courses: null,
+      searchResult: null,
+      searchKey: ''
+    }
+  },
+  mounted () {
+    this.updateInfo()
+  },
+  activated () {
+    this.updateInfo()
+  },
+  watch: {
+    searchKey () {
+      getCourses({ key: this.searchKey }).then(res => {
+        this.searchResult = res.data
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    }
+  },
+  methods: {
+    updateInfo () {
+      studentGetCourses({ sort: 'name' }).then(res => {
+        this.courses = res.data
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    },
+    handleToSelectCourse (course) {
+      this.$store.commit(SET_COURSE, { courseInfo: course })
+      this.$router.push({ name: 'join-course', params: { courseId: course.id } })
+    },
+    handleToCourseDetail (course) {
+      this.$store.commit(SET_COURSE, { courseInfo: course, params: { courseId: course.id } })
+      this.$router.push({ name: 'student-course-detail', params: { courseId: course.id } })
+    }
+  }
 }
 </script>
 
@@ -26,4 +85,9 @@ export default {
 .c-button {
   margin-top: 24px;
 }
+
+.search-result {
+  font-weight: bold;
+  margin-bottom: 24px;
+}
 </style>

+ 47 - 11
src/views/student/tabs/StudentExplore.vue

@@ -3,15 +3,23 @@
     <div class="left">
       <h1 class="title">我的主页</h1>
       <search-bar></search-bar>
+      <!--      <section>-->
+      <!--        <h1 class="sub-title">最近浏览</h1>-->
+      <!--        <slide-card :click-handler="handleSlideCardClick"></slide-card>-->
+      <!--        <br>-->
+      <!--      </section>-->
       <section>
-        <h1 class="sub-title">最近浏览</h1>
-        <slide-card :click-handler="handleSlideCardClick"></slide-card>
-        <br>
-      </section>
-      <section>
-        <h1 class="sub-title">课程列表</h1>
-        <course-card :course="{}" :index="2"></course-card>
-        <course-card :course="{}" :index="2"></course-card>
+        <h1 class="sub-title">按课程查找课件</h1>
+        <div v-if="!courses || courses.length === 0" class="no-content-warning">
+          <div class="no-content-warning-tip">
+            您还没有加入任何课程
+          </div>
+          <c-button class="full-width secondary" @click="handleToCoursesManager">去课程管理界面</c-button>
+        </div>
+        <template v-for="(course, index) of courses">
+          <course-card :key="course.id" :course="course" :index="index + 1"
+                       :clickHandler="handleToCourseSlideList"></course-card>
+        </template>
       </section>
     </div>
     <router-view/>
@@ -20,12 +28,40 @@
 
 <script>
 import SearchBar from '@/components/SearchBar'
-import SlideCard from '@/components/SlideCard'
 import CourseCard from '@/components/CourseCard'
+import { studentGetCourses } from '@/server/course'
+import CButton from '@/components/CButton'
+import { SET_COURSE } from '@/store/types/main-types'
 
 export default {
-  components: { CourseCard, SlideCard, SearchBar },
-  methods: {}
+  components: { CButton, CourseCard, SearchBar },
+  data () {
+    return {
+      courses: []
+    }
+  },
+  mounted () {
+    this.updateInfo()
+  },
+  activated () {
+    this.updateInfo()
+  },
+  methods: {
+    updateInfo () {
+      studentGetCourses({ sort: 'name' }).then(res => {
+        this.courses = res.data
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    },
+    handleToCoursesManager () {
+      this.$router.push({ name: 'student-courses' })
+    },
+    handleToCourseSlideList (course) {
+      this.$store.commit(SET_COURSE, { courseInfo: course })
+      this.$router.push({ name: 'student-course-slides', params: { courseId: course.id } })
+    }
+  }
 }
 </script>
 

+ 97 - 3
src/views/student/tabs/StudentPersonal.vue

@@ -1,15 +1,109 @@
 <template>
   <div class="tab">
-    <h1 class="title">个人信息设置 <span class="tag">学生</span></h1>
-    <div class="sub-title"></div>
+    <h1 class="title">个人信息设置 <span class="tag">教师</span></h1>
+    <div class="sub-title">修改姓名</div>
+    <c-input placeholder="姓名" class="mb-16" v-model="newName"></c-input>
+    <c-button class="warning full-width mb-32" @click="handleModifyName">确认修改姓名</c-button>
+    <div class="sub-title">修改密码</div>
+    <div class="verified-code-wrapper">
+      <c-input placeholder="邮箱验证码" type="number"></c-input>
+      <c-button class="secondary" :disabled="typeof this.emailButtonContent === typeof 0"
+                @click="handleSendEmailCode">{{ emailButtonContent }}
+      </c-button>
+    </div>
+    <c-input placeholder="新密码" type="password" class="mb-16" v-model="password"></c-input>
+    <c-input placeholder="确认新密码" type="password" class="mb-16" v-model="passwordEnsure"></c-input>
+    <c-button class="warning full-width mb-32" @click="handleModifyPassword">确认修改密码</c-button>
+    <c-button class="error full-width" @click="handleLogout">登出</c-button>
   </div>
 </template>
 
 <script>
-export default {}
+import CButton from '@/components/CButton'
+import CInput from '@/components/CInput'
+import { sendEmailCode } from '@/server/code'
+import { logout, modifyUser, resetPasswordByEmail } from '@/server/user'
+
+export default {
+  components: { CInput, CButton },
+  data () {
+    return {
+      email: '',
+      emailButtonContent: '发送验证码',
+      newName: '',
+      password: '',
+      passwordEnsure: ''
+    }
+  },
+  methods: {
+    handleSendEmailCode () {
+      this.$showMessage({ message: '正在尝试发送验证码', type: 'info' })
+      if (this.email.match(/.+@(nju.edu.cn|smail.nju.edu.cn)/)) {
+        this.setEmailCodeCd()
+        sendEmailCode({ email: this.email })
+      } else {
+        this.$showMessage({ message: '本邮箱不符合南京大学校邮格式', type: 'error' })
+      }
+    },
+    setEmailCodeCd () {
+      this._emailCodeCdInterval && clearInterval(this._emailCodeCdInterval)
+      this.emailButtonContent = 60
+      this._emailCodeCdInterval = setInterval(() => {
+        this.emailButtonContent--
+        if (typeof this.emailButtonContent !== typeof 0 || this.emailButtonContent <= 0) {
+          clearInterval(this._emailCodeCdInterval)
+          this.emailButtonContent = '发送验证码'
+        }
+      }, 1000)
+    },
+    handleLogout () {
+      logout()
+      this.$router.push({ name: 'login' })
+    },
+    handleModifyPassword () {
+      if (!this.password || this.password !== this.passwordEnsure) {
+        this.$setInfoMessage('两次输入的密码不一致')
+        return
+      }
+
+      resetPasswordByEmail({ email: this.email, code: this.code, password: this.password }).then(res => {
+        this.$setInfoMessage('密码修改成功')
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    },
+    handleModifyName () {
+      if (!this.newName) {
+        this.$setInfoMessage('输入的新用户名为空')
+        return
+      }
+
+      modifyUser({ name: this.newName }).then(res => {
+        this.$setSuccessMessage('修改用户名成功')
+      })
+    }
+  }
+}
 </script>
 
 <style lang="scss" scoped>
 @import "~@/style/theme.scss";
 @import "~@/views/shared/tab-common";
+
+.verified-code-wrapper {
+  margin-bottom: 16px;
+  display: flex;
+  align-items: center;
+  flex: 1;
+
+  .c-input {
+    margin-bottom: unset;
+    flex: 1;
+  }
+
+  .c-button {
+    margin-left: 12px;
+  }
+}
+
 </style>

+ 8 - 0
src/views/student/tabs/StudentQuiz.vue

@@ -2,7 +2,15 @@
   <div class="tab">
     <h1 class="title">课程测验 <span class="tag">学生</span></h1>
     <div class="sub-title">待完成的测验</div>
+    <div class="no-content-warning">
+      本页面目前尚未完成 <br>
+      请前往相应课件详情处进入测验界面
+    </div>
     <div class="sub-title">已完成的测验 & 结果</div>
+    <div class="no-content-warning">
+      本页面目前尚未完成 <br>
+      请前往相应课件详情处进入测验界面
+    </div>
   </div>
 </template>
 

+ 46 - 6
src/views/teacher/components/TeacherCourseDetail.vue

@@ -1,12 +1,18 @@
 <template>
   <tab-side-component>
     <back-title>课程详情</back-title>
+    <div class="detail">
+      <div>课程名称:{{course.name}}</div>
+      <div>课程简介:{{course.bio}}</div>
+      <div>创建时间:{{createTime}}</div>
+      <div>教师:{{course.teacherName}}</div>
+    </div>
     <div class="sub-title">修改选课码:</div>
     <div class="change-wrapper mb-24">
-      <c-input placeholder="不会影响已选学生"/>
-      <c-button class="secondary">确认修改</c-button>
+      <c-input placeholder="修改不会影响已选学生" v-model="newCode"/>
+      <c-button class="secondary" @click="handleChangeCode">确认修改</c-button>
     </div>
-    <c-button class="secondary full-width mb-16" @click="handleCheckSlideList(123)">点击查看课件列表</c-button>
+    <c-button class="secondary full-width mb-16" @click="handleCheckSlideList">点击查看课件列表</c-button>
     <c-button class="error full-width">删除课程</c-button>
   </tab-side-component>
 </template>
@@ -16,23 +22,57 @@ import TabSideComponent from '@/components/TabSideComponent'
 import CInput from '@/components/CInput'
 import CButton from '@/components/CButton'
 import BackTitle from '@/components/BackTitle'
+import { mapState } from 'vuex'
+import { modifyCourse } from '@/server/course'
 
 export default {
   components: { BackTitle, CButton, CInput, TabSideComponent },
   data () {
     return {
-      courseId: this.$route.params.courseId
+      newCode: ''
+    }
+  },
+  computed: {
+    ...mapState({
+      course: state => state.main.courseInfo
+    }),
+    createTime () {
+      return this.$moment(this.course.createAt).fromNow()
     }
   },
   methods: {
-    handleCheckSlideList (courseId) {
-      this.$router.push({ name: 'teacher-course-slides', params: { courseId: courseId } })
+    handleCheckSlideList () {
+      this.$router.push({ name: 'teacher-course-slides' })
+    },
+    handleChangeCode () {
+      modifyCourse({ ...this.course, code: this.newCode }).then(res => {
+        this.$setSuccessMessage('选课码修改成功')
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
+.detail {
+  font-weight: bold;
+  font-size: 14px;
+  margin-bottom: 24px;
+  color: #7a8a9a;
+
+  div {
+    margin-top: 2px;
+
+  }
+
+  div:not(:last-child) {
+    border-bottom: 1px dashed #aabaca;
+    padding-bottom: 2px;
+  }
+}
+
 .c-input {
   margin-bottom: 24px;
 }

+ 13 - 6
src/views/teacher/components/TeacherCourseSlides.vue

@@ -20,41 +20,48 @@ import SlideCard from '@/components/SlideCard'
 import CButton from '@/components/CButton'
 import { teacherGetSlidesByCourse } from '@/server/slide'
 import { SET_SLIDE_INFO } from '@/store/types/pdf-types'
+import { SET_COURSE } from '@/store/types/main-types'
+import { mapState } from 'vuex'
 
 export default {
   components: { CButton, SlideCard, BackTitle, TabSideComponent },
   data () {
     return {
-      courseId: this.$route.params.courseId,
       slides: []
     }
   },
   watch: {
     $route () {
-      this.courseId = this.$route.params.courseId
       this.init()
     }
   },
+  computed: {
+    ...mapState({
+      course: state => state.main.courseInfo
+    })
+  },
   mounted () {
     this.init()
   },
   methods: {
     init () {
-      teacherGetSlidesByCourse({ courseId: this.courseId, sort: 'name' }).then(res => {
+      teacherGetSlidesByCourse({ courseId: this.course.id, sort: 'name' }).then(res => {
         this.slides = res.data
       }).catch(err => {
+        this.slides = []
         this.$setErrorMessage('获取课程课件列表过程中:' + err.response.data.msg)
       })
     },
     handleSlideCardClick (slide) {
       this.$store.commit(SET_SLIDE_INFO, { slideInfo: slide })
-      this.$router.push({ name: 'teacher-explore-slide-detail' })
+      this.$router.push({ name: 'teacher-explore-slide-detail', params: { slideId: slide.id } })
     },
     handleToManager () {
-      this.$router.push({ name: 'teacher-course-detail', params: { courseId: this.courseId } })
+      this.$store.commit(SET_COURSE, { courseInfo: this.course })
+      this.$router.push({ name: 'teacher-course-detail', params: { courseId: this.course.id } })
     },
     handleToCreateCourse () {
-      this.$router.push({ name: 'create-slide', params: { courseId: this.courseId } })
+      this.$router.push({ name: 'create-slide' })
     }
   }
 }

+ 21 - 3
src/views/teacher/components/TeacherSlideDetail.vue

@@ -7,6 +7,7 @@
       <div>创建时间:{{createTime}}</div>
       <div>最后更新:{{updateTime}}</div>
       <div>总页面数:{{slide.pages}} 页</div>
+      <div>课件状态:{{slideStateTranslation[slide.state]}}</div>
     </div>
     <c-button class="full-width mb-16" @click="handleReadSlide">浏览课件</c-button>
     <a :href="slideUrl">
@@ -15,6 +16,9 @@
       </c-button>
     </a>
     <c-button class="full-width secondary mb-16" @click="handleQuizSetting">习题设置 & 习题完成统计</c-button>
+    <c-button v-if="nextState" class="warning full-width mb-16" @click="handleToNextState">
+      将幻灯片状态设置为:{{slideStateTranslation[nextState]}}
+    </c-button>
     <c-button class="error full-width mb-16">删除课件</c-button>
   </tab-side-component>
 </template>
@@ -24,14 +28,17 @@ import TabSideComponent from '@/components/TabSideComponent'
 import CButton from '@/components/CButton'
 import BackTitle from '@/components/BackTitle'
 import { mapState } from 'vuex'
-import { getSlideUrl } from '@/server/slide'
-import { SET_SRC } from '@/store/types/pdf-types'
+import { getSlideUrl, modifySlideState } from '@/server/slide'
+import { SET_SLIDE_INFO, SET_SRC } from '@/store/types/pdf-types'
+import slideStateTranslation from '@/server/enums/slide-state-translation'
+import getSlideNextState from '@/utils/get-slide-next-state'
 
 export default {
   components: { BackTitle, CButton, TabSideComponent },
   data () {
     return {
-      slideUrl: ''
+      slideUrl: '',
+      slideStateTranslation: slideStateTranslation
     }
   },
   computed: {
@@ -43,6 +50,9 @@ export default {
     },
     createTime () {
       return this.$moment(this.slide.createAt).fromNow()
+    },
+    nextState () {
+      return getSlideNextState(this.slide.state)
     }
   },
   mounted () {
@@ -59,6 +69,14 @@ export default {
     },
     handleQuizSetting () {
       this.$router.push({ name: 'teacher-quiz-setting', params: { slideId: '123' } })
+    },
+    handleToNextState () {
+      modifySlideState({ id: this.slide.id, state: this.nextState }).then(res => {
+        this.$store.commit(SET_SLIDE_INFO, { slideInfo: { ...this.slide, state: this.nextState } })
+        this.$setSuccessMessage('课件状态修改成功')
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
     }
   }
 }

+ 6 - 3
src/views/teacher/tabs/TeacherCoursesManager.vue

@@ -7,7 +7,8 @@
         <div class="no-content-warning-tip">你还没有过开设课程~</div>
       </div>
       <template v-for="(course, index) of courses">
-        <course-card :key="course.id" :course="course" :index="index + 1" :clickHandler="handleCourseCardClick"></course-card>
+        <course-card :key="course.id" :course="course" :index="index + 1"
+                     :clickHandler="handleCourseCardClick"></course-card>
       </template>
       <c-button @click="handleCreateNewCourse" class="full-width secondary">新建课程</c-button>
     </div>
@@ -19,6 +20,7 @@
 import CButton from '@/components/CButton'
 import CourseCard from '@/components/CourseCard'
 import { teacherGetCourses } from '@/server/course'
+import { SET_COURSE } from '@/store/types/main-types'
 
 export default {
   components: { CButton, CourseCard },
@@ -44,8 +46,9 @@ export default {
     handleCreateNewCourse () {
       this.$router.push('/teacher/courses/create-new')
     },
-    handleCourseCardClick (courseId) {
-      this.$router.push({ name: 'teacher-course-detail', params: { courseId: courseId } })
+    handleCourseCardClick (course) {
+      this.$store.commit(SET_COURSE, { courseInfo: course })
+      this.$router.push({ name: 'teacher-course-detail', params: { courseId: course.id } })
     }
   }
 }

+ 7 - 4
src/views/teacher/tabs/TeacherExplore.vue

@@ -20,7 +20,8 @@
           <c-button class="full-width secondary" @click="handleToCreateCourse">去创建课程</c-button>
         </div>
         <template v-for="(course, index) of courses">
-          <course-card :key="course.id" :course="course" :index="index + 1" :clickHandler="handleCourseCardClick"></course-card>
+          <course-card :key="course.id" :course="course" :index="index + 1"
+                       :clickHandler="handleCourseCardClick"></course-card>
         </template>
       </section>
     </div>
@@ -36,6 +37,7 @@ import { teacherGetSlides } from '@/server/slide'
 import { teacherGetCourses } from '@/server/course'
 import CButton from '@/components/CButton'
 import { SET_SLIDE_INFO } from '@/store/types/pdf-types'
+import { SET_COURSE } from '@/store/types/main-types'
 
 export default {
   components: { CButton, CourseCard, SlideCard, SearchBar },
@@ -63,10 +65,11 @@ export default {
     },
     handleSlideCardClick (slide) {
       this.$store.commit(SET_SLIDE_INFO, { slideInfo: slide })
-      this.$router.push({ name: 'teacher-explore-slide-detail' })
+      this.$router.push({ name: 'teacher-explore-slide-detail', params: { slideId: slide.id } })
     },
-    handleCourseCardClick (courseId) {
-      this.$router.push({ name: 'teacher-course-slides', params: { courseId: courseId } })
+    handleCourseCardClick (course) {
+      this.$store.commit(SET_COURSE, { courseInfo: course })
+      this.$router.push({ name: 'teacher-course-slides', params: { courseId: course.id } })
     }
   }
 }

+ 37 - 6
src/views/teacher/tabs/TeacherPersonal.vue

@@ -2,7 +2,8 @@
   <div class="tab">
     <h1 class="title">个人信息设置 <span class="tag">教师</span></h1>
     <div class="sub-title">修改姓名</div>
-    <c-input placeholder="姓名" class="mb-32"></c-input>
+    <c-input placeholder="姓名" class="mb-16" v-model="newName"></c-input>
+    <c-button class="warning full-width mb-32" @click="handleModifyName">确认修改姓名</c-button>
     <div class="sub-title">修改密码</div>
     <div class="verified-code-wrapper">
       <c-input placeholder="邮箱验证码" type="number"></c-input>
@@ -10,10 +11,10 @@
                 @click="handleSendEmailCode">{{ emailButtonContent }}
       </c-button>
     </div>
-    <c-input placeholder="新密码" type="password" class="mb-16"></c-input>
-    <c-input placeholder="确认新密码" type="password" class="mb-16"></c-input>
-    <c-button class="secondary full-width mb-32">确认修改密码</c-button>
-    <c-button class="error full-width">登出</c-button>
+    <c-input placeholder="新密码" type="password" class="mb-16" v-model="password"></c-input>
+    <c-input placeholder="确认新密码" type="password" class="mb-16" v-model="passwordEnsure"></c-input>
+    <c-button class="warning full-width mb-32" @click="handleModifyPassword">确认修改密码</c-button>
+    <c-button class="error full-width" @click="handleLogout">登出</c-button>
   </div>
 </template>
 
@@ -21,13 +22,17 @@
 import CButton from '@/components/CButton'
 import CInput from '@/components/CInput'
 import { sendEmailCode } from '@/server/code'
+import { logout, modifyUser, resetPasswordByEmail } from '@/server/user'
 
 export default {
   components: { CInput, CButton },
   data () {
     return {
       email: '',
-      emailButtonContent: '发送验证码'
+      emailButtonContent: '发送验证码',
+      newName: '',
+      password: '',
+      passwordEnsure: ''
     }
   },
   methods: {
@@ -50,6 +55,32 @@ export default {
           this.emailButtonContent = '发送验证码'
         }
       }, 1000)
+    },
+    handleLogout () {
+      logout()
+      this.$router.push({ name: 'login' })
+    },
+    handleModifyPassword () {
+      if (!this.password || this.password !== this.passwordEnsure) {
+        this.$setInfoMessage('两次输入的密码不一致')
+        return
+      }
+
+      resetPasswordByEmail({ email: this.email, code: this.code, password: this.password }).then(res => {
+        this.$setInfoMessage('密码修改成功')
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    },
+    handleModifyName () {
+      if (!this.newName) {
+        this.$setInfoMessage('输入的新用户名为空')
+        return
+      }
+
+      modifyUser({ name: this.newName }).then(res => {
+        this.$setSuccessMessage('修改用户名成功')
+      })
     }
   }
 }