Procházet zdrojové kódy

feat: 消息通知功能

ChenYangfan před 6 roky
rodič
revize
a366816745

+ 6 - 0
public/manifest.json

@@ -0,0 +1,6 @@
+{
+  "short_name": "课堂助手",
+  "name": "SEEC 课堂助手",
+  "background_color": "#5485e5",
+  "theme_color": "#5485e5"
+}

+ 18 - 10
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_ugzzjpfsaup.eot');
-  src: url('//at.alicdn.com/t/font_1598214_ugzzjpfsaup.eot?#iefix') format('embedded-opentype'),
-  url('//at.alicdn.com/t/font_1598214_ugzzjpfsaup.woff2') format('woff2'),
-  url('//at.alicdn.com/t/font_1598214_ugzzjpfsaup.woff') format('woff'),
-  url('//at.alicdn.com/t/font_1598214_ugzzjpfsaup.ttf') format('truetype'),
-  url('//at.alicdn.com/t/font_1598214_ugzzjpfsaup.svg#iconfont') format('svg');
+  src: url('//at.alicdn.com/t/font_1598214_qpa79fv4wuj.eot');
+  src: url('//at.alicdn.com/t/font_1598214_qpa79fv4wuj.eot?#iefix') format('embedded-opentype'),
+  url('//at.alicdn.com/t/font_1598214_qpa79fv4wuj.woff2') format('woff2'),
+  url('//at.alicdn.com/t/font_1598214_qpa79fv4wuj.woff') format('woff'),
+  url('//at.alicdn.com/t/font_1598214_qpa79fv4wuj.ttf') format('truetype'),
+  url('//at.alicdn.com/t/font_1598214_qpa79fv4wuj.svg#iconfont') format('svg');
 }
 
 body {
@@ -108,6 +108,14 @@ body {
   margin-bottom: 16px;
 }
 
+.mb-24 {
+  margin-bottom: 24px;
+}
+
+.mb-32 {
+  margin-bottom: 32px;
+}
+
 .mt-8 {
   margin-top: 8px;
 }
@@ -116,11 +124,11 @@ body {
   margin-top: 16px;
 }
 
-.mb-24 {
-  margin-bottom: 24px;
+.mt-24 {
+  margin-top: 16px;
 }
 
-.mb-32 {
-  margin-bottom: 32px;
+.mt-32 {
+  margin-top: 16px;
 }
 </style>

+ 189 - 0
src/components/DiscussionTab/CMessage.vue

@@ -0,0 +1,189 @@
+<template>
+  <div class="c-message" :class="edit ? 'c-editing-message' : ''" @click="handleCMessageClick">
+    <div v-show="!edit">
+      <div class="left-content">{{createTime}}</div>
+    </div>
+    <div v-show="edit">
+      <div class="left-content">
+        <icon v-if="!read" class="archive-button" :class="toArchive ? 'archive-button--active' : ''"
+              :unicode="iconMap['archive']"
+              title="设置为已读" @click.native.stop="handleArchiveButtonClick"></icon>
+        <icon class="delete-button" :class="toDelete ? 'delete-button--active' : ''" :unicode="iconMap['delete']"
+              title="删除" @click.native.stop="handleDeleteButtonClick" ripple="red"></icon>
+      </div>
+    </div>
+    <div class="message-content">
+      <span class="type">{{messageTypeMap[message.type]}}</span>
+      {{message.content}}
+    </div>
+    <div class="link" v-if="message.slideId" title="点击前往相应幻灯片页面">
+      <icon :unicode="iconMap.forward" @click.native.stop="handlePush"></icon>
+    </div>
+  </div>
+</template>
+
+<script>
+import momentFromNow from '@/utils/moment-from-now'
+import messageTypeMap from '@/utils/message-type-map'
+import iconMap from '@/utils/icon-map'
+import Icon from '@/components/Icon'
+import { getSlideInfo } from '@/server/slide'
+import { SET_SLIDE_INFO } from '@/store/types/pdf-types'
+
+export default {
+  components: { Icon },
+  props: ['message', 'edit', 'deleteProp', 'archiveProp', 'read'],
+  data () {
+    return {
+      messageTypeMap,
+      iconMap,
+      toDelete: this.deleteProp,
+      toArchive: this.archiveProp
+    }
+  },
+  computed: {
+    createTime () {
+      return momentFromNow(this.message.createAt)
+    }
+  },
+  methods: {
+    handleArchiveButtonClick () {
+      this.toArchive = !this.toArchive
+      this.$emit('archive-state-change', { value: this.toArchive, id: this.message.id })
+    },
+    handleDeleteButtonClick () {
+      this.toDelete = !this.toDelete
+      this.$emit('delete-state-change', { value: this.toDelete, id: this.message.id })
+    },
+    handlePush () {
+      if (this.message.slideId) {
+        const handler = getSlideInfo({ slideId: this.message.slideId }).then(res => {
+          this.$store.commit(SET_SLIDE_INFO, { slideInfo: res })
+          this.$router.push({ name: 'pdf-reader' })
+        }).catch(err => {
+          this.$setErrorMessage('跳转过程出错,可能是相关幻灯片已不存在:' + err.response.data.msg)
+        })
+
+        if (this.edit) {
+          const msg = '编辑模式跳转到幻灯片页面将会丢失选中的内容,您确定继续跳转吗。'
+          if (confirm(msg)) {
+            handler()
+          }
+        } else {
+          handler()
+        }
+      }
+    },
+    handleCMessageClick () {
+      if (this.edit) {
+        this.handleArchiveButtonClick()
+      } else {
+        this.handlePush()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@/style/theme.scss';
+
+.c-message {
+  cursor: pointer;
+  display: flex;
+  transition: all .4s, transform .5s cubic-bezier(0, 0.5, 0.5, 1);
+  padding: 8px 12px 8px 4px;
+  margin: 12px 0;
+  border-radius: 8px;
+  align-items: center;
+  min-height: 50px;
+  box-sizing: border-box;
+  border: 1px solid transparent;
+  background-color: #f8fafd;
+}
+
+.c-editing-message {
+  cursor: pointer;
+  transform: translateY(60px);
+  background-color: $theme-green-transparent;
+}
+
+.c-editing-message:last-child {
+  margin-bottom: 60px;
+}
+
+.left-content {
+  opacity: 0;
+  display: flex;
+  text-align: center;
+  justify-content: center;
+  box-sizing: border-box;
+  font-family: 'Courier New', 'Courier', monospace;
+  width: 100%;
+  font-size: 12px;
+  font-weight: bold;
+  color: #7a8a9a;
+  min-width: 11ch;
+  padding-right: 8px;
+  animation: show .2s .3s forwards;
+
+  .icon {
+    background-color: transparent;
+    max-height: 16px;
+    max-width: 16px;
+    font-size: 12px;
+    color: #8a9aaa;
+  }
+
+  .archive-button {
+    color: lighten($theme-blue, 30);
+
+    &--active {
+      color: $theme-blue;
+    }
+  }
+
+  .delete-button {
+    color: lighten($theme-red, 30);
+
+    &--active {
+      color: $theme-red;
+    }
+  }
+}
+
+.type {
+  font-weight: bold;
+  box-sizing: border-box;
+  text-align: center;
+  margin-right: 4px;
+  color: $theme-blue;
+  font-size: 14px;
+  background-color: $theme-blue-transparent;
+}
+
+.message-content {
+  font-size: 14px;
+  line-height: 20px;
+  font-weight: bold;
+  color: #5a6a7a;
+}
+
+@media (max-width: 600px) {
+  .link {
+    display: none;
+  }
+  .c-message {
+    background-color: #f0f4f8;
+  }
+}
+
+@keyframes show {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+</style>

+ 90 - 0
src/components/DiscussionTab/DiscussionTabToolBar.vue

@@ -0,0 +1,90 @@
+<template>
+  <div>
+    <div class="condition">
+      <div :class="!showRead ? 'active' : ''" class="unread-button" title="点击展示未读消息"
+           @click="handleUnreadButtonClick"
+      >
+        未读消息
+      </div>
+      <div :class="showRead ? 'active' : ''" class="read-button" title="点击展示已读消息"
+           @click="handleReadButtonClick"
+      >
+        已读消息
+      </div>
+    </div>
+    <search-bar placeholder="按消息内容关键字搜索" v-model="key"></search-bar>
+  </div>
+</template>
+
+<script>
+import SearchBar from '@/components/SearchBar'
+import debounce from '@/utils/debounce'
+
+export default {
+  props: ['read'],
+  components: { SearchBar },
+  data () {
+    return {
+      showRead: !!this.read,
+      key: ''
+    }
+  },
+  watch: {
+    key () {
+      this.search()
+    }
+  },
+  methods: {
+    handleUnreadButtonClick () {
+      this.showRead = false
+      this.refresh()
+    },
+    handleReadButtonClick () {
+      this.showRead = true
+      this.refresh()
+    },
+    search: debounce(function () {
+      this.refresh()
+    }, 500),
+    refresh () {
+      this.$emit('refresh', { showRead: this.showRead, key: this.key })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@/style/theme.scss';
+
+.condition {
+  display: flex;
+  font-weight: bold;
+  margin: 16px -4px;
+
+  & > div {
+    cursor: pointer;
+    border-radius: 8px;
+    flex: 1;
+    padding: 6px 12px;
+    margin: 0 4px;
+    text-align: center;
+    background: rgba(255, 255, 255, .1);
+    color: #7a8a9a;
+    transition: .1s;
+
+    &:hover {
+      background: rgba(180, 180, 180, .1);
+    }
+  }
+
+  & > .active {
+    background: $theme-green-transparent;
+    color: $theme-green;
+
+    &:hover {
+      background: darken($theme-green-transparent, 5);
+      color: darken($theme-green, 5);
+    }
+  }
+}
+</style>

+ 1 - 4
src/components/Icon.vue

@@ -10,7 +10,7 @@ import Ripple from '@/components/Ripple'
 
 export default {
   components: { Ripple },
-  props: ['unicode', 'title', 'ripple']
+  props: ['unicode', 'title', 'ripple', 'small']
 }
 </script>
 
@@ -22,11 +22,8 @@ export default {
   display: flex;
   align-items: center;
   justify-content: center;
-  background: rgba(255, 255, 255, .8);
   padding: 8px;
   border-radius: 100px;
-  min-width: 28px;
-  min-height: 28px;
   width: 28px;
   height: 28px;
   user-select: none;

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

@@ -23,6 +23,10 @@ const commentUrl = host + '/api/comment'
 
 const useCommentServer = postfix => postfix ? commentUrl + postfix : commentUrl
 
+const messageUrl = host + '/api/message'
+
+const useMessageServer = postfix => postfix ? messageUrl + postfix : messageUrl
+
 const replyUrl = host + '/api/reply'
 
 const useReplyServer = postfix => postfix ? replyUrl + postfix : replyUrl
@@ -38,5 +42,6 @@ export {
   useUserServer,
   useCommentServer,
   useReplyServer,
-  useCommentWsServer
+  useCommentWsServer,
+  useMessageServer
 }

+ 1 - 2
src/main.js

@@ -6,11 +6,10 @@ import router from './router'
 import uuid from 'uuid'
 import store from './store'
 import { SET_MESSAGE } from '@/store/types/message-types'
-import moment from 'moment'
+import moment from '@/plugins/moment'
 
 Vue.config.productionTip = false
 Vue.prototype.$uuid = uuid
-moment.locale('zh-cn')
 Vue.prototype.$moment = moment
 
 Vue.prototype.$setMessage = function ({ message, type = 'info' }) {

+ 5 - 0
src/plugins/moment.js

@@ -0,0 +1,5 @@
+import moment from 'moment'
+
+moment.locale('zh-cn')
+
+export default moment

+ 2 - 2
src/router/index.js

@@ -74,7 +74,7 @@ const routes = [{
   }, {
     path: 'discussion',
     name: 'teacher-discussion',
-    component: () => import(/* webpackChunkName: "teacher" */ '../views/teacher/tabs/TeacherDiscussion.vue')
+    component: () => import(/* webpackChunkName: "teacher" */ '../views/shared/Discussion.vue')
   }, {
     path: 'personal',
     name: 'teacher-personal',
@@ -119,7 +119,7 @@ const routes = [{
   }, {
     path: 'discussion',
     name: 'student-discussion',
-    component: () => import(/* webpackChunkName: "student" */ '../views/student/tabs/StudentDiscussion.vue')
+    component: () => import(/* webpackChunkName: "student" */ '../views/shared/Discussion.vue')
   }, {
     path: 'personal',
     name: 'student-personal',

+ 46 - 0
src/server/message.js

@@ -0,0 +1,46 @@
+import axios from '@/plugins/axios'
+import { useMessageServer } from '@/config/server-info'
+
+export function readMessages ({ ids }) {
+  const data = {
+    ids
+  }
+
+  return axios
+    .put(useMessageServer('/read'), data)
+}
+
+export function deleteMessages ({ ids }) {
+  const data = {
+    ids
+  }
+
+  return axios
+    .put(useMessageServer('/delete'), data)
+}
+
+export function getUnreadMessages ({ key = '', page = 0, size = 100, sort = '' }) {
+  return axios
+    .get(useMessageServer('/unread'), {
+      params: {
+        key,
+        page,
+        size,
+        sort
+      }
+    })
+    .then(res => res.data)
+}
+
+export function getReadMessages ({ key = '', page = 0, size = 10, sort = '' }) {
+  return axios
+    .get(useMessageServer('/read'), {
+      params: {
+        key,
+        page,
+        size,
+        sort
+      }
+    })
+    .then(res => res.data)
+}

+ 6 - 0
src/server/slide.js

@@ -89,3 +89,9 @@ export function getSlideUrl({ slideId }) {
     .get(useSlideServer('/' + slideId + '/url'))
     .then(res => res.data)
 }
+
+export function getSlideInfo({ slideId }) {
+  return axios
+    .get(useSlideServer('/' + slideId))
+    .then(res => res.data)
+}

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

@@ -15,7 +15,9 @@ const iconMap = {
   'search': '\ue6a2',
   'upload': '\ue6c8',
   'back': '\ue714',
+  'forward': '\ue711',
   'quiz': '\ue6d1',
+  'list_check': '\ue77f',
   'down': '\ue7db',
   'up': '\ue7d9',
   'slide_show': '\ue74e',
@@ -28,7 +30,9 @@ const iconMap = {
   'exit': '\ue685',
   'new': '\ue679',
   'history': '\ue68c',
-  'download': '\ue6ca'
+  'download': '\ue6ca',
+  'archive': '\ue715',
+  'done': '\ue680'
 }
 
 export default iconMap

+ 7 - 0
src/utils/message-type-map.js

@@ -0,0 +1,7 @@
+const messageTypeMap = {
+  'SLIDE_IN_CLASS': '开课通知',
+  'COMMENT_NEW': '新讨论帖',
+  'COMMENT_REPLY': '教师回复'
+}
+
+export default messageTypeMap

+ 5 - 0
src/utils/moment-from-now.js

@@ -0,0 +1,5 @@
+import moment from '@/plugins/moment'
+
+export default function momentFromNow (time) {
+  return moment(time).fromNow()
+}

+ 277 - 0
src/views/shared/Discussion.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="tab">
+    <h1 class="title">
+      消息通知
+      <template
+        v-if="(unreadMessage && !showRead && unreadMessage.length) || (readMessage && showRead && readMessage.length)">
+        <icon ripple="red" :class="editMode ? 'icon-active' : ''" @click="handleToggleEditMode"
+              :unicode="iconMap['list_check']" title="点击启动 / 关闭编辑模式"></icon>
+        <span>{{editMode ? '编辑模式' : '浏览模式'}}</span>
+      </template>
+    </h1>
+    <discussion-tab-tool-bar :read="false" @refresh="handleRefresh"></discussion-tab-tool-bar>
+    <div v-show="editMode" class="confirm-area">
+      <!--      <c-button class="secondary" @click="handleChooseAllArchive"><span>{{iconMap['archive']}}</span> 全选</c-button>-->
+      <!--      <c-button class="error" @click="handleChooseAllDelete"><span>{{iconMap['delete']}}</span> 全选</c-button>-->
+      <div class="confirm" @click="handleSubmit">
+        <icon :unicode="iconMap['done']" ripple="green"></icon>
+        确定
+      </div>
+      <div class="cancel" @click="cancel">
+        <icon :unicode="iconMap['close']" ripple="red"></icon>
+        取消
+      </div>
+    </div>
+    <template v-if="!showRead">
+      <div class="no-content-warning" v-if="unreadMessage === null">
+        加载中...
+      </div>
+      <div class="no-content-warning" v-else-if="unreadMessage.length === 0">
+        无未读消息
+      </div>
+      <template v-else>
+        <template v-for="message of unreadMessage">
+          <c-message :delete-prop="deleteIds.has(message.id)"
+                     :archive-prop="archiveIds.has(message.id)"
+                     @archive-state-change="handleArchiveStateChange"
+                     @delete-state-change="handleDeleteStateChange"
+                     :key="message.id"
+                     :message="message"
+                     :edit="editMode"></c-message>
+        </template>
+      </template>
+    </template>
+    <template v-if="showRead">
+      <div class="no-content-warning" v-if="readMessage === null">
+        加载中...
+      </div>
+      <div class="no-content-warning" v-else-if="readMessage.length === 0">
+        无已读消息
+      </div>
+      <template v-for="message of readMessage">
+        <c-message :delete-prop="deleteIds.has(message.id)"
+                   :archive-prop="archiveIds.has(message.id)"
+                   @delete-state-change="handleDeleteStateChange"
+                   :key="message.id"
+                   :message="message"
+                   :read="true"
+                   :edit="editMode">
+        </c-message>
+      </template>
+    </template>
+  </div>
+</template>
+
+<script>
+import iconMap from '@/utils/icon-map'
+import DiscussionTabToolBar from '@/components/DiscussionTab/DiscussionTabToolBar'
+import CMessage from '@/components/DiscussionTab/CMessage'
+import { deleteMessages, getReadMessages, getUnreadMessages, readMessages } from '@/server/message'
+import Icon from '@/components/Icon'
+
+export default {
+  components: { Icon, DiscussionTabToolBar, CMessage },
+  data () {
+    return {
+      iconMap,
+      unreadMessage: null,
+      readMessage: null,
+      showRead: false,
+      editMode: false,
+      archiveIds: new Set([]),
+      deleteIds: new Set([])
+    }
+  },
+  mounted () {
+    this.getUnread(20, '').then(() => {
+      this.getUnread(10000)
+    })
+  },
+  methods: {
+    handleRefresh (condition) {
+      this.cancel()
+      this.showRead = condition.showRead
+
+      if (condition.showRead) {
+        this.readMessage = null
+        this.getRead(20).then(() => {
+          this.getRead(10000, condition.key)
+        })
+      } else {
+        this.unreadMessage = null
+        this.getUnread(20).then(() => {
+          this.getUnread(10000, condition.key)
+        })
+      }
+    },
+    handleToggleEditMode () {
+      this.editMode = !this.editMode
+    },
+    handleArchiveStateChange ({ value, id }) {
+      value ? (this.archiveIds.add(id)) : (this.archiveIds.delete(id))
+      console.log(Array.from(this.archiveIds))
+    },
+    handleDeleteStateChange ({ value, id }) {
+      value ? (this.deleteIds.add(id)) : (this.deleteIds.delete(id))
+      console.log(Array.from(this.archiveIds))
+    },
+    // handleChooseAllArchive () {
+    //   this.unreadMessage.map(message => this.archiveIds.add(message.id))
+    // },
+    // handleChooseAllDelete () {
+    //   this.unreadMessage.map(message => this.deleteIds.add(message.id))
+    // },
+    getUnread (size, key) {
+      return getUnreadMessages({ size, key }).then(res => {
+        this.unreadMessage = res.data
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    },
+    getRead (size, key) {
+      return getReadMessages({ size, key }).then(res => {
+        this.readMessage = res.data
+      }).catch(err => {
+        this.$setErrorMessage(err.response.data.msg)
+      })
+    },
+    handleSubmit () {
+      const deleteArr = Array.from(this.deleteIds)
+      const readArr = Array.from(this.archiveIds)
+      const handler = () => {
+        readArr.length > 0 && readMessages({ ids: readArr }).then(res => {
+          this.$setSuccessMessage('设置已读成功')
+          this.handleRefresh({ showRead: this.showRead })
+        }).catch(err => {
+          this.$setErrorMessage('设置已读过程中:' + err.response.data.msg)
+        })
+        deleteArr.length > 0 && deleteMessages({ ids: deleteArr }).then(res => {
+          this.$setSuccessMessage('删除成功')
+          this.handleRefresh({ showRead: this.showRead })
+        }).catch(err => {
+          this.$setErrorMessage('删除过程中:' + err.response.data.msg)
+        })
+      }
+
+      if (deleteArr.length > 0) {
+        const msg = '即将删除' + deleteArr.length + '条信息,请确认。'
+        if (confirm(msg)) {
+          handler()
+        }
+      } else {
+        handler()
+      }
+    },
+    cancel () {
+      this.editMode = false
+      this.archiveIds = new Set([])
+      this.deleteIds = new Set([])
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "~@/views/shared/tab-common.scss";
+@import "~@/style/theme.scss";
+
+@media (min-width: 800px) {
+  .tab {
+    max-width: 520px;
+    min-width: 520px;
+    position: relative;
+    box-sizing: border-box;
+    min-height: 450px;
+  }
+}
+
+.no-content-warning {
+  height: 180px;
+}
+
+.title {
+  display: flex;
+  align-items: center;
+
+  .icon {
+    margin-left: 8px;
+    color: #aabaca;
+  }
+
+  .icon-active {
+    color: $theme-red;
+  }
+
+  span {
+    margin-left: 4px;
+    font-size: 16px;
+    color: #aabaca;
+    transition: .2s;
+  }
+}
+
+.confirm-area {
+  opacity: 0;
+  position: absolute;
+  width: 100%;
+  margin: 20px -6px 0 -6px;
+  box-sizing: border-box;
+  display: flex;
+  justify-content: left;
+  animation: .2s .2s show forwards;
+
+  .c-button {
+    margin: 0 6px;
+    width: unset;
+    min-width: unset;
+
+    span {
+      display: inline-block;
+      margin-right: 2px;
+      font-family: 'iconfont', sans-serif;
+      transform: scale(1.2);
+    }
+  }
+
+  .confirm, .cancel {
+    display: flex;
+    align-items: center;
+    font-weight: bold;
+    color: #6a7a8a;
+    cursor: pointer;
+  }
+
+  .confirm {
+    color: $theme-green;
+
+    &:hover {
+      opacity: .8;
+    }
+
+    .icon {
+      color: $theme-green;
+    }
+  }
+
+  .cancel {
+    color: $theme-red;
+
+    &:hover {
+      opacity: .8;
+    }
+
+    .icon {
+      color: $theme-red;
+    }
+  }
+}
+
+@keyframes show {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+</style>

+ 0 - 25
src/views/student/tabs/StudentDiscussion.vue

@@ -1,25 +0,0 @@
-<template>
-  <div class="tab">
-    <h1 class="title">讨论区消息通知</h1>
-    <div class="no-content-warning">
-      本页面目前尚未完成 <br>
-      请前往相应课件评论区讨论
-    </div>
-  </div>
-</template>
-
-<script>
-import iconMap from '@/utils/icon-map'
-
-export default {
-  data () {
-    return {
-      iconMap: iconMap
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-@import "~@/views/shared/tab-common";
-</style>

+ 0 - 44
src/views/teacher/tabs/TeacherDiscussion.vue

@@ -1,44 +0,0 @@
-<template>
-  <div class="tab">
-    <div class="left">
-      <h1 class="title">讨论 & 问答</h1>
-      <div class="sub-title">待回答问题</div>
-      <div class="no-content-warning">
-        待回答问题部分目前尚未完成 <br>
-        请前往幻灯片阅读界面讨论区查看
-      </div>
-      <div class="mb-16"></div>
-      <div class="sub-title">热门讨论</div>
-      <div class="no-content-warning">
-        热门讨论部分目前尚未完成
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import iconMap from '@/utils/icon-map'
-
-export default {
-  data () {
-    return {
-      iconMap: iconMap
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-@import "~@/views/shared/tab-common";
-
-.tip {
-  font-weight: bold;
-  color: #7a8a9a;
-  margin-bottom: 16px;
-
-  & > span {
-    font-family: 'iconfont', sans-serif;
-    color: #ffc33b;
-  }
-}
-</style>