Explorar el Código

feat: 置顶功能

ChenYangfan hace 6 años
padre
commit
8cba15446d

+ 6 - 6
src/App.vue

@@ -46,12 +46,12 @@ export default {
 
 @font-face {
   font-family: 'iconfont';  /* project id 1598214 */
-  src: url('//at.alicdn.com/t/font_1598214_jzjp1vaj3t.eot');
-  src: url('//at.alicdn.com/t/font_1598214_jzjp1vaj3t.eot?#iefix') format('embedded-opentype'),
-  url('//at.alicdn.com/t/font_1598214_jzjp1vaj3t.woff2') format('woff2'),
-  url('//at.alicdn.com/t/font_1598214_jzjp1vaj3t.woff') format('woff'),
-  url('//at.alicdn.com/t/font_1598214_jzjp1vaj3t.ttf') format('truetype'),
-  url('//at.alicdn.com/t/font_1598214_jzjp1vaj3t.svg#iconfont') format('svg');
+  src: url('//at.alicdn.com/t/font_1598214_5pufddythnd.eot');
+  src: url('//at.alicdn.com/t/font_1598214_5pufddythnd.eot?#iefix') format('embedded-opentype'),
+  url('//at.alicdn.com/t/font_1598214_5pufddythnd.woff2') format('woff2'),
+  url('//at.alicdn.com/t/font_1598214_5pufddythnd.woff') format('woff'),
+  url('//at.alicdn.com/t/font_1598214_5pufddythnd.ttf') format('truetype'),
+  url('//at.alicdn.com/t/font_1598214_5pufddythnd.svg#iconfont') format('svg');
 }
 
 body {

+ 42 - 32
src/components/CComment.vue

@@ -1,45 +1,42 @@
 <template>
   <div class="comment">
-    <div @click="showActions = !showActions">
-      <div class="comment-title">主题:{{comment.title}}</div>
+    <div>
+      <div class="comment-title">
+        <span v-if="comment.topNumber" class="icon blue">{{iconMap['put_on_top']}}</span>
+        主题:{{comment.title}}
+      </div>
       <div class="comment-content">{{comment.content}}</div>
       <div class="comment-author">{{createTime}} | 作者:{{comment.userName}} <span class="role">{{role}}</span></div>
+      <comment-tool-bar
+        :top-number="comment.topNumber || ''"
+        :user-id="comment.userId" :has-reply="comment.reply || ''"
+        @reply="handleReply"
+        @delete-reply="handleDeleteReply"
+        @delete-comment="handleDeleteComment"
+        @to-top="handleToTop"
+        @cancel-to-top="handleCancelToTop"
+      >
+      </comment-tool-bar>
       <div v-if="comment.reply" class="comment-reply">
         <div>教师回复:{{comment.reply.content}}</div>
         <div class="comment-reply-time">{{replyTime}}</div>
       </div>
     </div>
-    <template v-if="showActions && hasActions">
-      <div v-if="isTeacher && !comment.reply" class="mt-8 mb-8">
-        <c-input textarea="true" placeholder="在此输入回复" class="mb-8" v-model="replyContent"></c-input>
-        <c-button class="full-width secondary" @click="handleCreateReply">提交回复</c-button>
-      </div>
-      <template v-if="isTeacher && comment.reply">
-        <c-button class="full-width warning mt-16 mb-8" @click="handleDeleteReply">删除回复</c-button>
-      </template>
-      <c-button v-if="canDelete" class="error full-width mb-8" @click="handleDeleteComment">删除评论</c-button>
-    </template>
-    <div v-if="hasActions" class="icon" @click="showActions = !showActions">
-      {{iconMap[showActions ? 'up' : 'down']}}
-      <span>{{showActions ? '隐藏操作' : '更多操作'}}</span>
-    </div>
   </div>
 </template>
 
 <script>
 import { mapState } from 'vuex'
 import iconMap from '@/utils/icon-map'
-import CButton from '@/components/CButton'
-import CInput from '@/components/CInput'
 import { createReply, removeReply } from '@/server/reply'
-import { removeComment } from '@/server/comment'
+import { removeComment, topComment, unTopComment } from '@/server/comment'
+import CommentToolBar from '@/components/CommentToolBar'
 
 export default {
-  components: { CInput, CButton },
+  components: { CommentToolBar },
   props: ['comment'],
   data () {
     return {
-      showActions: false,
       replyContent: '',
       iconMap: iconMap
     }
@@ -48,15 +45,6 @@ export default {
     ...mapState({
       userInfo: state => state.main.userInfo
     }),
-    canDelete () {
-      return this.userInfo.id === this.comment.userId
-    },
-    isTeacher () {
-      return this.userInfo.type === 'TEACHER'
-    },
-    hasActions () {
-      return this.isTeacher || this.canDelete
-    },
     createTime () {
       return this.$moment(this.comment.createAt).fromNow()
     },
@@ -73,8 +61,8 @@ export default {
     }
   },
   methods: {
-    handleCreateReply () {
-      createReply({ commentId: this.comment.id, content: this.replyContent }).then(res => {
+    handleReply (reply) {
+      createReply({ commentId: this.comment.id, content: reply }).then(res => {
         this.$setSuccessMessage('回复成功')
         this.$emit('need-refresh')
       }).catch(err => {
@@ -102,6 +90,22 @@ export default {
           this.$setErrorMessage(err.response.data.msg)
         })
       }
+    },
+    handleToTop () {
+      topComment({ commentId: this.comment.id }).then(res => {
+        this.$setSuccessMessage('置顶成功')
+        this.$emit('need-refresh')
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    },
+    handleCancelToTop () {
+      unTopComment({ commentId: this.comment.id }).then(res => {
+        this.$setSuccessMessage('取消置顶成功')
+        this.$emit('need-refresh')
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
     }
   }
 }
@@ -160,4 +164,10 @@ export default {
     font-size: 12px;
   }
 }
+
+.blue {
+  color: $theme-blue;
+  font-size: 16px;
+  transform: scale(1.2);
+}
 </style>

+ 139 - 7
src/components/CommentToolBar.vue

@@ -1,26 +1,158 @@
 <template>
-  <div>
-    <icon :unicode="iconMap['delete']" title="删除本条讨论"></icon>
-    <icon :unicode="iconMap['reply']" title="回复本条讨论"></icon>
-    <icon :unicode="iconMap['send_to_top']" title="置顶本条消息"></icon>
-    <icon :unicode="iconMap['delete']" title="删除本条讨论"></icon>
+  <div class="comment-tool-bar">
+    <div class="actions">
+      <div v-if="isTeacher && !topNumber" class="action-button-wrapper green" @click="handleToTop">
+        <icon :unicode="iconMap['put_on_top']" title="置顶本条消息" ripple="green"></icon>
+        <span>置顶</span>
+      </div>
+      <div v-if="isTeacher && topNumber" class="action-button-wrapper green" @click="handleCancelToTop">
+        <icon :unicode="iconMap['close']" title="置顶本条消息" ripple="green"></icon>
+        <span>取消置顶</span>
+      </div>
+      <div v-if="isTeacher && !hasReply" class="action-button-wrapper blue" @click="handleShowHideReplyInput">
+        <icon :unicode="iconMap['reply']" title="回复本条讨论" ripple="blue"></icon>
+        <span>回复</span>
+      </div>
+      <div v-if="isTeacher && hasReply" class="action-button-wrapper red" @click="handleDeleteReply">
+        <icon :unicode="iconMap['remove']" title="回复本条讨论" ripple="red"></icon>
+        <span>删除回复</span>
+      </div>
+      <div v-if="canDelete" class="action-button-wrapper red" @click="handleDeleteComment">
+        <icon :unicode="iconMap['delete']" title="删除本条讨论" ripple="red"></icon>
+        <span>删除讨论</span>
+      </div>
+    </div>
+    <div v-show="replyInputShow && !hasReply" class="reply-input-wrapper">
+      <c-input placeholder="在此输入回复内容" v-model="replyContent"></c-input>
+      <icon :unicode="iconMap['send']" @click="handleReply"></icon>
+    </div>
   </div>
 </template>
 
 <script>
 import Icon from '@/components/Icon'
 import iconMap from '@/utils/icon-map'
+import { mapState } from 'vuex'
+import CInput from '@/components/CInput'
 
 export default {
-  components: { Icon },
+  props: ['userId', 'hasReply', 'topNumber'],
+  components: { CInput, Icon },
   data () {
     return {
-      iconMap
+      iconMap,
+      replyInputShow: false,
+      replyContent: ''
+    }
+  },
+  computed: {
+    ...mapState({
+      userInfo: state => state.main.userInfo
+    }),
+    canDelete () {
+      return this.userId === this.userInfo.id
+    },
+    isTeacher () {
+      return this.userInfo.type === 'TEACHER'
+    }
+  },
+  methods: {
+    handleShowHideReplyInput () {
+      this.replyInputShow = !this.replyInputShow
+    },
+    handleReply () {
+      if (!this.replyContent) {
+        this.$setInfoMessage('无法回复空消息')
+      }
+      this.$emit('reply', this.replyContent)
+    },
+    handleDeleteReply () {
+      this.$emit('delete-reply')
+    },
+    handleDeleteComment () {
+      this.$emit('delete-comment')
+    },
+    handleToTop () {
+      this.$emit('to-top')
+    },
+    handleCancelToTop () {
+      this.$emit('cancel-to-top')
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
+@import '~@/style/theme.scss';
+
+.comment-tool-bar {
+  margin: 0 -12px 0 -12px;
+}
+
+.actions {
+  display: flex;
+  align-items: center;
+}
+
+.action-button-wrapper {
+  margin-right: 12px;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  transition: .2s;
+
+  span {
+    font-size: 14px;
+    color: #7a8a9a;
+  }
+
+  &:hover {
+    opacity: .8;
+  }
+}
+
+.blue {
+  .icon {
+    color: $theme-blue;
+  }
+
+  span {
+    font-size: 14px;
+    color: $theme-blue;
+  }
+}
+
+.green {
+  .icon {
+    color: $theme-green;
+  }
 
+  span {
+    font-size: 14px;
+    color: $theme-green;
+  }
+}
+
+.red {
+  .icon {
+    color: $theme-red;
+  }
+
+  span {
+    font-size: 14px;
+    color: $theme-red;
+  }
+}
+
+.reply-input-wrapper {
+  padding: 0 12px;
+  box-sizing: border-box;
+  width: 100%;
+  display: flex;
+
+  .c-input {
+    margin-right: 8px;
+    width: 100%;
+  }
+}
 </style>

+ 2 - 2
src/components/Icon.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="icon" :title="title ? title : ''" @click="$emit('click')">
     <span>{{unicode}}</span>
-    <ripple></ripple>
+    <ripple :color="ripple || ''"></ripple>
   </div>
 </template>
 
@@ -10,7 +10,7 @@ import Ripple from '@/components/Ripple'
 
 export default {
   components: { Ripple },
-  props: ['unicode', 'title']
+  props: ['unicode', 'title', 'ripple']
 }
 </script>
 

+ 22 - 4
src/components/Ripple.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="ripple-wrapper">
+  <div class="ripple-wrapper" :class="color || ''">
     <div class="ripple" @click="handleRipple" :class="className" :style="size ? `width: ${size}px; height: ${size}px;` : ''">
     </div>
   </div>
@@ -7,7 +7,7 @@
 
 <script>
 export default {
-  props: ['size'],
+  props: ['size', 'color'],
   data () {
     return {
       className: 'hide'
@@ -30,7 +30,7 @@ export default {
 <style lang="scss" scoped>
 @import '~@/style/theme.scss';
 
-$opacity: .15;
+$opacity: 1;
 .ripple-wrapper {
   position: absolute;
   width: 100%;
@@ -49,7 +49,7 @@ $opacity: .15;
   height: 80px;
   z-index: 2;
   position: absolute;
-  background-color: $icon-hover-color;
+  background-color: $theme-blue-transparent;
   border-radius: 100000px;
   transform-origin: center;
 }
@@ -59,6 +59,24 @@ $opacity: .15;
   animation: ripple .5s forwards ease-out;
 }
 
+.red {
+  .ripple {
+    background: $theme-red-transparent;
+  }
+}
+
+.green {
+  .ripple {
+    background: $theme-green-transparent;
+  }
+}
+
+.blue {
+  .ripple {
+    background: $theme-blue-transparent;
+  }
+}
+
 @keyframes ripple {
   0% {
     opacity: $opacity;

+ 3 - 1
src/utils/icon-map.js

@@ -22,7 +22,9 @@ const iconMap = {
   'put_on_top': '\ue779',
   'reply': '\ue76f',
   'delete': '\ue67e',
-  'send': '\ue763'
+  'send': '\ue763',
+  'remove': '\ue774',
+  'close': '\ue6ec'
 }
 
 export default iconMap

+ 10 - 0
src/views/reader/PdfReader.vue

@@ -16,6 +16,8 @@ import PdfViewer from '@/components/PDF/PdfViewer'
 import { mapState } from 'vuex'
 import BackTitle from '@/components/BackTitle'
 import DiscussionArea from '@/components/DiscussionArea'
+import { getSlideUrl } from '@/server/slide'
+import { SET_SRC } from '@/store/types/pdf-types'
 
 export default {
   components: { DiscussionArea, BackTitle, PdfViewer },
@@ -25,6 +27,14 @@ export default {
       slide: state => state.pdf.slideInfo,
       showDiscussion: state => state.pdf.showDiscussion
     })
+  },
+  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)
+    })
   }
 }
 </script>

+ 6 - 0
src/views/teacher/components/CreateNewCourse.vue

@@ -50,9 +50,15 @@ export default {
         this.$setInfoMessage(this.message)
         return
       }
+      this.$setInfoMessage('创建课程中...')
+      const interval = setInterval(() => {
+        this.$setInfoMessage('创建课程中...')
+      }, 500)
       createCourse({ name: this.name, bio: this.bio, code: this.code }).then(res => {
+        clearInterval(interval)
         this.$setSuccessMessage('创建课程成功')
       }).catch(err => {
+        clearInterval(interval)
         this.$setErrorMessage(err.response.data.msg)
       })
     }

+ 7 - 0
src/views/teacher/tabs/CreateSlide.vue

@@ -72,10 +72,17 @@ export default {
         this.$setInfoMessage(this.message)
         return
       }
+
+      this.$setInfoMessage('上传课件可能比较耗时,请稍等片刻')
+      const interval = setInterval(() => {
+        this.$setInfoMessage('上传课件可能比较耗时,请稍等片刻')
+      }, 500)
       createSlide({ courseId: this.selectedCourseId, name: this.slideName, file: this.file }).then(res => {
+        clearInterval(interval)
         this.$setSuccessMessage('创建课件成功!')
         this.$router.push({ name: 'teacher-explore' })
       }).catch(err => {
+        clearInterval(interval)
         this.$setErrorMessage(err.response.data.msg)
       })
     },

+ 2 - 0
src/views/teacher/tabs/TeacherPersonal.vue

@@ -86,6 +86,8 @@ export default {
 
       modifyUser({ name: this.newName }).then(res => {
         this.$setSuccessMessage('修改用户名成功')
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
       })
     }
   }