Browse Source

feat: websocket 加持评论区

ChenYangfan 6 years ago
parent
commit
c22171115c

+ 6 - 6
src/App.vue

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

+ 36 - 9
src/components/CComment.vue

@@ -1,12 +1,15 @@
 <template>
   <div class="comment">
     <div>
-      <div class="comment-title">
-        <span v-if="comment.topNumber" class="icon blue">{{iconMap['put_on_top']}}</span>
+      <div class="comment-title" :class="isNew ? 'new-comment' : ''">
+        <span v-if="isNew" class="icon state-icon-new" title="这条评论属于实时更新的内容">{{iconMap['new']}}</span>
+<!--        <span v-if="!isNew" class="icon state-icon-history" title="这条评论属于实时更新的内容">{{iconMap['history']}}</span>-->
+        <span v-if="comment.topNumber" class="icon top-icon">{{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>
+      <div class="comment-author">{{isNew ? '刚刚' : createTime}} | 作者:{{comment.userName}} <span
+        class="role">{{role}}</span></div>
       <comment-tool-bar
         :top-number="comment.topNumber || ''"
         :user-id="comment.userId" :has-reply="comment.reply || ''"
@@ -34,7 +37,7 @@ import CommentToolBar from '@/components/CommentToolBar'
 
 export default {
   components: { CommentToolBar },
-  props: ['comment'],
+  props: ['comment', 'isNew'],
   data () {
     return {
       replyContent: '',
@@ -115,13 +118,21 @@ export default {
 @import '~@/style/theme.scss';
 
 .comment {
+  box-sizing: border-box;
+  position: relative;
   width: 100%;
-  padding: 16px 0 16px 0;
+  margin: 0 -16px;
+  padding: 12px 16px;
   font-weight: bold;
 }
 
+.new-comment {
+  background: $theme-green-transparent;
+}
+
 .comment-title {
-  margin-bottom: 12px;
+  padding: 6px;
+  margin: 6px -6px;
 }
 
 .comment-content {
@@ -141,6 +152,8 @@ export default {
     color: $theme-blue;
     background: $theme-blue-transparent;
     font-size: 12px;
+    position: relative;
+    bottom: 1px;
   }
 }
 
@@ -165,9 +178,23 @@ export default {
   }
 }
 
-.blue {
+.top-icon {
+  display: inline-block;
   color: $theme-blue;
-  font-size: 16px;
-  transform: scale(1.2);
+  transform: scale(1.4);
+}
+
+.state-icon {
+  display: inline-block;
+  margin-right: 4px;
+  transform: scale(1.4);
+
+  &-new {
+    color: $theme-green;
+  }
+
+  &-history {
+    color: $theme-blue;
+  }
 }
 </style>

+ 30 - 5
src/components/DiscussionArea.vue

@@ -2,14 +2,21 @@
   <div class="discussion-area">
     <h1 class="title">讨论 & 问答</h1>
     <div class="discussion-area-content">
-      <write-new-comment ref="writeComment" class="mb-24" @comment-submit="handleCommentSubmit" :placeholder="writingAreaPlaceholder"></write-new-comment>
+      <write-new-comment ref="writeComment" class="mb-24" @comment-submit="handleCommentSubmit"
+                         :placeholder="writingAreaPlaceholder"></write-new-comment>
       <div class="sub-title mb-8">本页讨论区</div>
       <div class="no-content-warning" v-if="comments === null">
         <div class="no-content-warning-tip">加载中...</div>
       </div>
-      <div class="no-content-warning" v-else-if="comments.length === 0">
+      <div class="no-content-warning" v-else-if="comments.length === 0 && wsComments.length === 0">
         本页还没有讨论
       </div>
+      <template v-for="comment of wsComments">
+        <c-comment v-if="comment.pageNumber === currentPage" :key="comment.id" :comment="comment" is-new="true"
+                   @need-refresh="updateComments"
+        >
+        </c-comment>
+      </template>
       <template v-for="comment of comments">
         <c-comment :key="comment.id" :comment="comment"
                    @need-refresh="updateComments"
@@ -25,12 +32,15 @@ import CComment from '@/components/CComment'
 import WriteNewComment from '@/components/WriteNewComment'
 import { mapState } from 'vuex'
 import { createComment, getComments } from '@/server/comment'
+import WebsocketPublisher from '@/server/websocket/ws'
+import { useCommentWsServer } from '@/config/server-info'
 
 export default {
   components: { WriteNewComment, CComment },
   data () {
     return {
-      comments: null
+      comments: null,
+      wsComments: []
     }
   },
   mounted () {
@@ -55,9 +65,25 @@ export default {
       this.updateComments()
     }
   },
+  created () {
+    this._ws = new WebsocketPublisher(useCommentWsServer('/slide/') + this.slide.id)
+    this._ws.subscribe({
+      onNext: data => {
+        this.wsComments.unshift(data)
+      },
+      onError: err => {
+        this.$setErrorMessage(err)
+      },
+      onClose: () => {}
+    })
+  },
+  beforeDestroy () {
+    this._ws.close()
+  },
   methods: {
     updateComments () {
-      getComments({ slideId: this.slide.id, pageNumber: this.currentPage }).then(res => {
+      getComments({ slideId: this.slide.id, pageNumber: this.currentPage, sort: 'createAt,desc' }).then(res => {
+        this.wsComments = []
         this.comments = res.data
       }).catch(err => {
         this.$setErrorMessage('获取本页评论区的过程中:' + err.response.data.msg)
@@ -72,7 +98,6 @@ export default {
       }).then(res => {
         this.$refs.writeComment.resetContent()
         this.$setSuccessMessage('您的讨论帖发表成功')
-        this.updateComments()
       }).catch(err => {
         this.$setErrorMessage(err.response.data.msg)
       })

+ 7 - 1
src/config/server-info.js

@@ -1,6 +1,7 @@
 // const host = 'http://106.14.178.184:8080/seec_helper'
 // const host = 'http://192.168.3.70:18080'
 const host = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : ''
+const wsHost = process.env.NODE_ENV === 'development' ? 'ws://localhost:8080' : ''
 
 const slideUrl = host + '/api/slide'
 
@@ -26,11 +27,16 @@ const replyUrl = host + '/api/reply'
 
 const useReplyServer = postfix => postfix ? replyUrl + postfix : replyUrl
 
+const commentWsUrl = wsHost + '/api/websocket/comment'
+
+const useCommentWsServer = postfix => postfix ? commentWsUrl + postfix : commentWsUrl
+
 export {
   useSlideServer,
   useCourseServer,
   useCodeServer,
   useUserServer,
   useCommentServer,
-  useReplyServer
+  useReplyServer,
+  useCommentWsServer
 }

+ 0 - 8
src/server/utils/catch-handler.js

@@ -1,8 +0,0 @@
-import showMessage from '@/server/utils/show-message'
-
-export default function catchHandler () {
-  showMessage({
-    message: '请检查网络情况,若网络正常请联系管理员:chenyangfan1998@icloud.com',
-    type: 'error'
-  })
-}

+ 0 - 8
src/server/utils/show-error-message.js

@@ -1,8 +0,0 @@
-import showMessage from '@/server/utils/show-message'
-
-export default function showErrorMessage (msg) {
-  showMessage({
-    message: msg,
-    type: 'error'
-  })
-}

+ 0 - 8
src/server/utils/show-message.js

@@ -1,8 +0,0 @@
-import store from '@/store/index'
-import { SET_MESSAGE } from '@/store/types/message-types'
-
-const showMessage = function ({ message, type = 'info' }) {
-  store.commit(SET_MESSAGE, { message, type })
-}
-
-export default showMessage

+ 81 - 0
src/server/websocket/ws.js

@@ -0,0 +1,81 @@
+// 使用案例
+// const ws = WebsocketPublisher(useCommentWsServer('/slide/') + this.slide.id)
+// ws.subscribe({
+//   onNext: () => {},
+//   onError: () => {},
+//   onClose: () => {}
+// })
+
+export default class WebsocketPublisher {
+  constructor (url) {
+    this.url = url
+    this.subscribers = []
+    this._init()
+  }
+
+  _init () {
+    this.websocket = new WebSocket(this.url)
+    this.websocket.onmessage = this._handleReceiveMessage
+    this.websocket.onerror = this._handleError
+    this.websocket.onclose = this._handleClose
+  }
+
+  _handleClose = () => {
+    this.subscribers
+      .filter((s) => typeof s.onClose === 'function')
+      .forEach((s) => {
+        s.onClose()
+      })
+  }
+
+  _handleError = (e) => {
+    this.subscribers
+      .filter((s) => typeof s.onError === 'function')
+      .forEach((subscriber) => {
+        subscriber.onError(e)
+      })
+  }
+
+  _handleReceiveMessage = (ev) => {
+    try {
+      const msg = JSON.parse(ev.data)
+      this.subscribers
+        .filter((s) => typeof s.onNext === 'function')
+        .forEach((subscriber) => {
+          subscriber.onNext(msg)
+        })
+    } catch (e) {
+      this._handleError(e)
+    }
+  }
+
+  /**
+   * 订阅消息
+   * @param subscriber
+   */
+  subscribe (subscriber) {
+    this.subscribers.findIndex(s => s === subscriber) === -1 && this.subscribers.push(subscriber)
+  }
+
+  /**
+   * 取消
+   * @param subscriber
+   */
+  unsubscribe (subscriber) {
+    const index = this.subscribers.findIndex(s => s === subscriber)
+    if (index !== -1) {
+      this.subscribers.splice(index, 1)
+    }
+  }
+
+  /**
+   * 手动关闭
+   */
+  close () {
+    try {
+      this.websocket.close()
+    } catch (e) {
+
+    }
+  }
+}

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

@@ -25,7 +25,9 @@ const iconMap = {
   'send': '\ue763',
   'remove': '\ue774',
   'close': '\ue6ec',
-  'exit': '\ue685'
+  'exit': '\ue685',
+  'new': '\ue679',
+  'history': '\ue68c'
 }
 
 export default iconMap