ChenYangfan 6 лет назад
Родитель
Сommit
bd9a706d8e

+ 3 - 1
src/animations/ShowTransition.vue

@@ -5,7 +5,9 @@
 </template>
 
 <script>
-export default {}
+export default {
+  name: 'ShowTransition'
+}
 </script>
 
 <style lang="scss" scoped>

+ 3 - 1
src/animations/ShowTransitionLarge.vue

@@ -5,7 +5,9 @@
 </template>
 
 <script>
-export default {}
+export default {
+  name: 'ShowTransitionLarge'
+}
 </script>
 
 <style lang="scss" scoped>

+ 3 - 1
src/animations/ShowTransitionSmall.vue

@@ -5,7 +5,9 @@
 </template>
 
 <script>
-export default {}
+export default {
+  name: 'ShowTransitionSmall'
+}
 </script>
 
 <style lang="scss" scoped>

+ 3 - 2
src/animations/SlideInDifferentTransition.vue

@@ -23,14 +23,15 @@ export default {}
 
 .slide-in-difference-enter {
   opacity: 0;
+  transform: translateY(1.2%);
 }
 
 @media (min-width: 900px) {
   .slide-in-difference-leave-to {
-    transform: translateX(3%);
+    transform: translateX(4%);
   }
   .slide-in-difference-enter {
-    transform: translateX(3%);
+    transform: translateX(4%);
   }
 }
 </style>

+ 1 - 6
src/animations/SlideUpTransition.vue

@@ -23,11 +23,6 @@ export default {}
 
 .slide-up-enter {
   opacity: 0;
-}
-
-@media (min-width: 900px) {
-  .slide-up-enter {
-    transform: translateY(1.2%);
-  }
+  transform: translateY(1.2%);
 }
 </style>

+ 0 - 23
src/components/Basic/FullScreenForm.vue

@@ -1,23 +0,0 @@
-<template>
-  <div class="full-screen-form">
-    <slot></slot>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'FullScreenForm'
-}
-</script>
-
-<style lang="scss" scoped>
-.full-screen-form {
-  display: flex;
-  position: fixed;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  background: white;
-}
-</style>

+ 111 - 0
src/components/Cards/NoticeCard.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="notice-card" @click="showMore = !showMore">
+    <div class="notice-card-title" :class="showMore ? 'cancel-bottom-radius' : null">
+      <c-icon>notifications</c-icon>
+      课程公告:{{notice.title}}
+      <c-icon bold class="more-icon">{{showMore ? 'expand_less' : 'expand_more'}}</c-icon>
+    </div>
+    <show-transition>
+      <div v-if="showMore" class="notice-card-info">
+        {{notice.content}}
+      </div>
+    </show-transition>
+    <show-transition-small>
+      <div v-if="showMore" class="notice-card-time">
+        最后编辑:{{momentFromNow(notice.updateAt)}}
+        <c-button @click.native.stop="handleEdit" v-if="canEdit" :right="-3" theme="green" icon="edit">编辑</c-button>
+      </div>
+    </show-transition-small>
+  </div>
+</template>
+
+<script>
+import ShowTransitionSmall from '@/animations/ShowTransitionSmall'
+import ShowTransition from '@/animations/ShowTransition'
+import momentFromNow from '@/utils/moment-from-now'
+import CIcon from '@/components/Basic/CIcon'
+import CButton from '@/components/Basic/CButton'
+
+export default {
+  name: 'NoticeCard',
+  components: { CButton, CIcon, ShowTransition, ShowTransitionSmall },
+  props: {
+    notice: Object,
+    canEdit: Boolean
+  },
+  data () {
+    return {
+      showMore: false
+    }
+  },
+  methods: {
+    momentFromNow,
+    handleEdit () {
+      this.$router.push({
+        name: 'teacher-edit-notice',
+        params: { noticeId: this.notice.id, courseId: this.notice.courseId }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@/style/theme.scss';
+
+.notice-card {
+  margin: 12px 0;
+  border-radius: 12px;
+  transition: .2s;
+  cursor: pointer;
+  background: $theme-green-transparent;
+}
+
+.notice-card-title {
+  background-color: $theme-green;
+  border-radius: 12px;
+  font-size: 14px;
+  color: white;
+  padding: 8px 12px;
+
+  .c-icon {
+    transform-origin: top;
+    transform: scale(1.25);
+    color: white;
+  }
+}
+
+.notice-card-info {
+  font-size: 13px;
+  color: #839a91;
+  line-height: 1.7;
+  margin: 6px 12px;
+}
+
+.notice-card-time {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 12px;
+  color: #8a9aaa;
+  padding: 4px 12px 10px 12px;
+
+  .c-button {
+    padding: 4px 12px;
+    background: rgba(255, 255, 255, .8);
+
+    &:hover {
+      background: white;
+    }
+  }
+}
+
+.cancel-bottom-radius {
+  border-radius: 12px 12px 0 0;
+}
+
+.more-icon {
+  display: inline-block;
+  float: right;
+}
+</style>

+ 27 - 0
src/components/Layouts/FullScreenForm.vue

@@ -0,0 +1,27 @@
+<template>
+  <div class="full-screen-form">
+    <div class="content">
+      <back-title>{{backTitle}}</back-title>
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+import BackTitle from '@/components/Basic/BackTitle'
+export default {
+  name: 'FullScreenForm',
+  props: {
+    backTitle: String
+  },
+  components: { BackTitle }
+}
+</script>
+
+<style lang="scss" scoped>
+.full-screen-form {
+  max-width: 575px;
+  margin: 0 auto;
+  padding: 64px 24px;
+}
+</style>

+ 1 - 7
src/components/Layouts/TabSideComponent.vue

@@ -60,8 +60,7 @@ export default {
   flex: 1;
   padding: 24px;
   align-items: center;
-  backdrop-filter: saturate(180%) blur(15px);
-  background: rgba(255, 255, 255, 0.5);
+  background: white;
   & > .content {
     min-height: 60vh;
   }
@@ -85,9 +84,4 @@ export default {
   }
 }
 
-@supports (not(backdrop-filter: saturate(180%) blur(15px))) {
-  .small {
-    background: white;
-  }
-}
 </style>

+ 10 - 0
src/router/student.js

@@ -5,44 +5,54 @@ export default [{
   redirect: '/student/explore',
   component: () => import(/* webpackChunkName: "student" */ '../views/student/StudentHome.vue'),
   children: [{
+    // 学生主页
     path: 'explore',
     name: 'student-explore',
     component: () => import(/* webpackChunkName: "student" */ '../views/student/tabs/StudentExplore.vue'),
     children: [{
+      // 课程所属的所有课件信息
       path: 'course-slides/:courseId',
       name: 'student-course-slides',
       component: () => import(/* webpackChunkName: "student" */ '../views/student/components/StudentCourseSlides.vue')
     }, {
+      // 课件信息
       path: 'slide-detail/:slideId',
       name: 'student-explore-slide-detail',
       component: () => import(/* webpackChunkName: "student" */ '../views/student/components/StudentSlideDetail.vue')
     }, {
+      // 附件信息
       path: 'course-file-detail/:courseFileId',
       name: 'student-explore-course-file-detail',
       component: () => import(/* webpackChunkName: "student" */ '../views/student/components/StudentCourseFileDetail.vue')
     }]
   }, {
+    // 学生查看正在进行的和已经结束的所有测验(一个汇总的地方))
     path: 'quiz',
     name: 'student-quiz',
     component: () => import(/* webpackChunkName: "student" */ '../views/student/tabs/StudentQuiz.vue')
   }, {
+    // 学生课程管理界面
     path: 'courses',
     name: 'student-courses',
     component: () => import(/* webpackChunkName: "student" */ '../views/student/tabs/StudentCoursesManager.vue'),
     children: [{
+      // 加入课程
       path: 'join-course/:courseId',
       name: 'join-course',
       component: () => import(/* webpackChunkName: "student" */ '../views/student/components/JoinCourse.vue')
     }, {
+      // 课程详情,在这里可进行退课操作
       path: 'course-detail/:courseId',
       name: 'student-course-detail',
       component: () => import(/* webpackChunkName: "student" */ '../views/student/components/StudentCourseDetail.vue')
     }]
   }, {
+    // 消息汇总区域
     path: 'discussion',
     name: 'student-discussion',
     component: () => import(/* webpackChunkName: "student" */ '../views/shared/Discussion.vue')
   }, {
+    // 个人信息区域
     path: 'personal',
     name: 'student-personal',
     component: () => import(/* webpackChunkName: "student" */ '../views/student/tabs/StudentPersonal.vue')

+ 8 - 0
src/router/teacher.js

@@ -36,13 +36,21 @@ export default [{
     name: 'teacher-courses',
     component: () => import(/* webpackChunkName: "teacher" */ '../views/teacher/tabs/TeacherCoursesManager.vue'),
     children: [{
+      // 创建新课程
       path: 'create-new',
       name: 'create-new-course',
       component: () => import(/* webpackChunkName: "teacher" */ '../views/teacher/components/CreateNewCourse.vue')
     }, {
+      // 课程详情
       path: 'course-detail/:courseId',
       name: 'teacher-course-detail',
       component: () => import(/* webpackChunkName: "teacher" */ '../views/teacher/components/TeacherCourseDetail.vue')
+    }, {
+      // 教师填写公告内容的页面
+      meta: { roles: ['teacher'] },
+      path: 'teacher-edit-notice/:noticeId/:courseId',
+      name: 'teacher-edit-notice',
+      component: () => import(/* webpackChunkName: "teacher" */ '../views/teacher/components/EditNotice.vue')
     }]
   }, {
     // 教师讨论区页面

+ 3 - 11
src/server/comment.js

@@ -1,5 +1,6 @@
 import { useCommentServer } from '@/config/server-info'
 import axios from '@/plugins/axios'
+import generateSortParams from '@/server/utils/generate-sort-params'
 
 export function createComment ({ slideId, pageNumber, title, content }) {
   const data = {
@@ -32,18 +33,9 @@ export function unTopComment ({ commentId }) {
 }
 
 export function getComments ({ slideId, pageNumber, key = '', page = 1, size = 10, sort = '' }) {
-  const params = new URLSearchParams()
+  const params = generateSortParams({ key, page, size, sort })
   params.append('pageNumber', pageNumber)
-  params.append('key', key)
-  params.append('page', page.toString())
-  params.append('size', size.toString())
-  if (typeof sort !== typeof '') {
-    sort.forEach(_ => {
-      params.append('sort', _)
-    })
-  } else {
-    params.append('sort', sort)
-  }
+
   return axios
     .get(useCommentServer('/slide/' + slideId), {
       params: params

+ 13 - 1
src/server/notice.js

@@ -1,5 +1,6 @@
 import axios from '@/plugins/axios'
 import { useNoticeServer } from '@/config/server-info'
+import generateSortParams from '@/server/utils/generate-sort-params'
 
 export function createNotice ({ courseId, title, content }) {
   const data = {
@@ -23,14 +24,25 @@ export function modifyNotice ({ id, courseId, title, content }) {
 
   return axios
     .put(useNoticeServer(), data)
+    .then(res => res.data)
 }
 
 export function deleteNotice ({ noticeId }) {
   return axios
     .delete(useNoticeServer('/' + noticeId))
+    .then(res => res.data)
 }
 
-export function getNotices ({ courseId }) {
+export function getNotices ({ courseId, key = '', page = 1, size = 100, sort = '' }) {
+  const params = generateSortParams({ key, page, size, sort })
+  params.append('courseId', courseId)
   return axios
     .get(useNoticeServer('/course/' + courseId))
+    .then(res => res.data)
+}
+
+export function getNoticeByNoticeId ({ noticeId }) {
+  return axios
+    .get(useNoticeServer('/' + noticeId))
+    .then(res => res.data)
 }

+ 14 - 0
src/server/utils/generate-sort-params.js

@@ -0,0 +1,14 @@
+export default function generateSortParams ({ key = '', page = 1, size = 10, sort = '' }) {
+  const params = new URLSearchParams()
+  params.append('key', key)
+  params.append('page', page.toString())
+  params.append('size', size.toString())
+  if (typeof sort !== typeof '') {
+    sort.forEach(_ => {
+      params.append('sort', _)
+    })
+  } else {
+    params.append('sort', sort)
+  }
+  return params
+}

+ 18 - 3
src/views/student/components/StudentCourseDetail.vue

@@ -10,7 +10,12 @@
     <show-transition>
       <course-detail v-if="showMore" :course="course"></course-detail>
     </show-transition>
-    <c-button icon="list" theme="blue" block class="mb-16" @click="handleCheckSlideList">课件列表</c-button>
+    <template v-for="notice of notices">
+      <notice-card :key="'notice-' + notice.id"
+                   :notice="notice">
+      </notice-card>
+    </template>
+    <c-button icon="list" theme="blue" block class="mt-24 mb-16" @click="handleCheckSlideList">课件列表</c-button>
     <c-button icon="close" theme="red" block @click="handleQuitCourse">退出课程</c-button>
   </tab-side-component>
 </template>
@@ -25,13 +30,16 @@ import CourseDetail from '@/components/Details/CourseDetail'
 import { RESET_COURSE } from '@/store/types/main-types'
 import momentFromNow from '@/utils/moment-from-now'
 import ShowTransition from '@/animations/ShowTransition'
+import NoticeCard from '@/components/Cards/NoticeCard'
+import { getNotices } from '@/server/notice'
 
 export default {
-  components: { ShowTransition, CourseDetail, BackTitle, CButton, TabSideComponent },
+  components: { NoticeCard, ShowTransition, CourseDetail, BackTitle, CButton, TabSideComponent },
   data () {
     return {
       newCode: '',
-      showMore: true
+      notices: null,
+      showMore: false
     }
   },
   computed: {
@@ -42,6 +50,13 @@ export default {
       return momentFromNow(this.course.createAt)
     }
   },
+  mounted () {
+    getNotices({ courseId: this.course.id }).then(res => {
+      this.notices = res.data
+    }).catch(err => {
+      this.$setErrorMessage(err.response.data.msg)
+    })
+  },
   methods: {
     handleCheckSlideList () {
       this.$router.push({ name: 'student-course-slides' })

+ 25 - 2
src/views/student/components/StudentCourseSlides.vue

@@ -1,5 +1,10 @@
 <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">设置
@@ -24,7 +29,7 @@
       <div class="no-content-warning-tip">本课程尚无课件~</div>
     </div>
     <template v-for="slide of slides">
-      <slide-card :key="slide.id" :hide-course="!showMore" :hide-teacher="!showMore"
+      <slide-card :key="'slide-' + slide.id" :hide-course="!showMore" :hide-teacher="!showMore"
                   :slide="slide" :hide-update-time="!showMore"
                   :click-handler="handleSlideCardClick"></slide-card>
     </template>
@@ -50,12 +55,25 @@ import ShowTransition from '@/animations/ShowTransition'
 import TabActions from '@/components/Layouts/TabActions'
 import { getOneCourseFile } from '@/server/course-file'
 import CourseFileCard from '@/components/Cards/CourseFileCard'
+import { getNotices } from '@/server/notice'
+import NoticeCard from '@/components/Cards/NoticeCard'
 
 export default {
-  components: { CourseFileCard, TabActions, CourseDetail, CButton, SlideCard, BackTitle, TabSideComponent, ShowTransition },
+  components: {
+    NoticeCard,
+    CourseFileCard,
+    TabActions,
+    CourseDetail,
+    CButton,
+    SlideCard,
+    BackTitle,
+    TabSideComponent,
+    ShowTransition
+  },
   data () {
     return {
       slides: null,
+      notices: null,
       courseFiles: null,
       showMore: false,
       showActions: false
@@ -82,6 +100,11 @@ export default {
       this.getCourseFiles(20).then(() => {
         this.getCourseFiles(10000)
       })
+      getNotices({ courseId: this.course.id }).then(res => {
+        this.notices = res.data
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
     },
     getSlides (size) {
       return studentGetSlidesByCourse({ courseId: this.course.id, size, sort: 'name' }).then(res => {

+ 105 - 0
src/views/teacher/components/EditNotice.vue

@@ -0,0 +1,105 @@
+<template>
+  <tab-side-component>
+    <back-title>{{noticeId === 0 || isEmpty(noticeId) ? '新建' : '编辑' }}课程公告</back-title>
+    <div class="sub-title">公告标题</div>
+    <c-input v-model="title" placeholder="标题长度上限为 50 个字,请保持简短" :maxlength="50" class="mb-16"></c-input>
+    <div class="sub-title">公告内容</div>
+    <textarea v-model="content" placeholder="内容长度上限为 1000 字" :maxlength="1000" class="mb-16"></textarea>
+    <c-button theme="green" standout icon="check" @click="handleSubmit">确认提交</c-button>
+    <c-button v-if="noticeId !== 0 && !isEmpty(noticeId)" :left="8" theme="red" icon="close" @click="handleDelete">删除公告</c-button>
+  </tab-side-component>
+</template>
+
+<script>
+import CButton from '@/components/Basic/CButton'
+import CInput from '@/components/Basic/CInput'
+import TabSideComponent from '@/components/Layouts/TabSideComponent'
+import BackTitle from '@/components/Basic/BackTitle'
+import { createNotice, deleteNotice, getNoticeByNoticeId, modifyNotice } from '@/server/notice'
+import { isEmpty } from '@/utils/types'
+
+export default {
+  components: { BackTitle, TabSideComponent, CInput, CButton },
+  data () {
+    return {
+      noticeId: this.$route.params.noticeId,
+      courseId: this.$route.params.courseId,
+      title: '',
+      content: ''
+    }
+  },
+  mounted () {
+    if (this.noticeId !== 0) {
+      getNoticeByNoticeId({ noticeId: this.noticeId }).then(res => {
+        console.log(res)
+        this.title = res.title
+        this.content = res.content
+      })
+    }
+  },
+  methods: {
+    isEmpty,
+    handleSubmit () {
+      // noticeId 为 0 表示notice不存在,处理逻辑按新建处理
+      if (this.noticeId === 0 || isEmpty(this.noticeId)) {
+        createNotice({ courseId: this.courseId, title: this.title, content: this.content }).then(res => {
+          this.$setSuccessMessage('新建公告成功')
+          this.$router.go(-1)
+        }).catch(err => {
+          this.$setErrorMessage(err.response.data.msg)
+        })
+      } else {
+        modifyNotice({ id: this.noticeId, title: this.title, content: this.content }).then(res => {
+          this.$setSuccessMessage('修改公告内容成功')
+          this.$router.go(-1)
+        }).catch(err => {
+          this.$setErrorMessage(err.response.data.msg)
+        })
+      }
+    },
+    handleDelete () {
+      const msg = '确定删除该公告吗?'
+      if (confirm(msg)) {
+        deleteNotice({ noticeId: this.noticeId }).then(() => {
+          this.$setSuccessMessage('删除成功')
+          this.$router.go(-1)
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@/style/theme.scss';
+
+textarea {
+  box-sizing: border-box;
+  font-size: 16px;
+  font-weight: bold;
+  color: #3a4a5a;
+  background: $input-background-color;
+  border-radius: 8px;
+  border: none;
+  outline: none;
+  width: 100%;
+  min-height: 260px;
+  height: 100%;
+  line-height: 1.5;
+  padding: 0 12px 0 12px;
+  transition: .2s;
+
+  &:hover {
+    background: $input-hover-background-color;
+  }
+
+  &::placeholder {
+    color: #8a9aaa;
+  }
+}
+
+textarea {
+  padding: 6px 12px;
+  resize: none;
+}
+</style>

+ 20 - 4
src/views/teacher/components/TeacherCourseDetail.vue

@@ -10,12 +10,20 @@
     <show-transition>
       <course-detail v-if="showMore" :course="course"></course-detail>
     </show-transition>
-    <div class="sub-title">修改选课码:</div>
+    <template v-if="notices">
+      <template v-for="notice of notices">
+        <notice-card can-edit :notice="notice" :key="'notice-' + notice.id"></notice-card>
+      </template>
+    </template>
+    <c-button icon="list" theme="blue" block class="mt-24 mb-8" @click="handleCheckSlideList">课件列表</c-button>
+    <div class="sub-title mt-24">修改选课码:</div>
     <div class="change-wrapper mb-24">
       <c-input placeholder="修改不会影响已选学生" v-model="newCode"/>
       <c-button text theme="green" icon="check">确认</c-button>
     </div>
-    <c-button icon="list" theme="blue" block class="mb-16" @click="handleCheckSlideList">课件列表</c-button>
+    <router-link :to="{ name: 'teacher-edit-notice', params: { noticeId: 0, courseId: course.id } }">
+      <c-button theme="green" block class="mb-12" icon="add">新建课程公告</c-button>
+    </router-link>
     <c-button icon="delete" theme="red" block @click="handleDeleteCourse">删除课程</c-button>
   </tab-side-component>
 </template>
@@ -30,13 +38,16 @@ import { modifyCourse, removeCourse } from '@/server/course'
 import CourseDetail from '@/components/Details/CourseDetail'
 import momentFromNow from '@/utils/moment-from-now'
 import ShowTransition from '@/animations/ShowTransition'
+import { getNotices } from '@/server/notice'
+import NoticeCard from '@/components/Cards/NoticeCard'
 
 export default {
-  components: { CourseDetail, BackTitle, CButton, CInput, TabSideComponent, ShowTransition },
+  components: { NoticeCard, CourseDetail, BackTitle, CButton, CInput, TabSideComponent, ShowTransition },
   data () {
     return {
       newCode: '',
-      showMore: true,
+      notices: null,
+      showMore: false,
       showNoticeForm: false
     }
   },
@@ -48,6 +59,11 @@ export default {
       return momentFromNow(this.course.createAt)
     }
   },
+  mounted() {
+    getNotices({ courseId: this.course.id }).then(res => {
+      this.notices = res.data
+    })
+  },
   methods: {
     handleCheckSlideList () {
       this.$router.push({ name: 'teacher-course-slides' })

+ 15 - 2
src/views/teacher/components/TeacherCourseSlides.vue

@@ -17,6 +17,11 @@
     <div class="sub-title">
       {{course.name}}
     </div>
+    <template v-for="notice of notices">
+      <notice-card :key="'notice-' + notice.id" can-edit
+                   :notice="notice">
+      </notice-card>
+    </template>
     <show-transition>
       <course-detail v-if="showMore" :course="course"></course-detail>
     </show-transition>
@@ -53,15 +58,18 @@ import CourseDetail from '@/components/Details/CourseDetail'
 import ShowTransition from '@/animations/ShowTransition'
 import { getOneCourseFile } from '@/server/course-file'
 import CourseFileCard from '@/components/Cards/CourseFileCard'
+import NoticeCard from '@/components/Cards/NoticeCard'
+import { getNotices } from '@/server/notice'
 
 export default {
-  components: { CourseFileCard, CourseDetail, ShowTransition, TabActions, CButton, SlideCard, BackTitle, TabSideComponent },
+  components: { NoticeCard, CourseFileCard, CourseDetail, ShowTransition, TabActions, CButton, SlideCard, BackTitle, TabSideComponent },
   data () {
     return {
       slides: null,
       courseFiles: null,
       showActions: false,
-      showMore: false
+      showMore: false,
+      notices: null
     }
   },
   watch: {
@@ -85,6 +93,11 @@ export default {
       this.getCourseFiles(20).then(() => {
         this.getCourseFiles(10000)
       })
+      getNotices({ courseId: this.course.id }).then(res => {
+        this.notices = res.data
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
     },
     getSlides (size) {
       return teacherGetSlidesByCourse({ courseId: this.course.id, sort: 'name', size }).then(res => {