vue 文章评论组件

项目地址
https://gitee.com/biboheart/huip-vue/tree/master/src/mock/comments

今天用到文章评论的功能,觉得这样常用的功能适合写个组件。不多说,开始

最终效果图

效果图

建立文件夹

目录结构

从外到内文件内容如下

BhComments/index.js

import CommentsItem from './packages/comments-item/index.js'
import ReplyItem from './packages/reply-item/index.js'

const components = [
  CommentsItem,
  ReplyItem
]

const install = function (Vue) {
  components.map(component => {
    Vue.component(component.name, component)
  })
}

if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default {
  version: '1.0.1',
  name: 'BhComments',
  install,
  CommentsItem,
  ReplyItem
}

BhComments/packages/comments-item/index.js

import CommentsItem from './src/main'

CommentsItem.install = function (Vue) {
  Vue.component(CommentsItem.name, CommentsItem)
}

export default CommentsItem

BhComments/packages/comments-item/src/main.vue

<template>
  <div class="comments-item">
    <div class="pull-left">
      <img class="avatar-32" :src="avatar" alt="" v-if="avatar" @click="handleClickAvatar">
    </div>
    <div class="comments-box">
      <div class="comments-trigger">
        <div class="pull-right comments-option">
          <a href="javascript:void(0)" class="ml10" data-placement="top" :title="item.title" v-for="item in tools" :key="item.name" @click="handleClickTool($event, item)">
            <i :class="item.icon" v-if="item.icon"></i>
            <span v-if="item.text">{{item.text}}</span>
          </a>
        </div>
        <strong><a target="_blank" href="javascript:void(0)" @click="handleClickAuthor">{{author}}</a></strong>
        <span class="comments-date">  ·  {{time | filterTime}}</span>
      </div>
      <div class="comments-content">
        <p>{{content}}</p>
      </div>
      <p class="comments-ops">
        <span class="coments-ops-item ml15" v-for="item in ops" :key="item.name" v-if="item.name">
          <i :class="item.icon + ' coments-ops-icon'" v-if="item.icon"></i>
          <span class="coments-ops-text">{{item.name}}</span>
        </span>
        <span class="comments-reply-btn ml15" @click="handleAddReply">回复</span>
      </p>
      <div class="reply-list" v-show="hasReply">
        <slot></slot>
        <div class="reply-item reply-item--ops">
          <a class="reply-inner-btn" href="javascript:void(0);" @click="handleAddReply">添加回复</a>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CommentsItem',
  props: {
    avatar: String,
    author: String,
    content: String,
    ops: Array,
    tools: Array,
    time: [String, Number],
    hasReply: Boolean
  },
  data () {
    return {
    }
  },
  computed: {
  },
  methods: {
    handleClickAvatar (event) {
      event.stopPropagation()
      this.$emit('clickAvatar', this)
    },
    handleClickTool (event, tool) {
      event.stopPropagation()
      this.$emit('clickTool', this, tool)
    },
    handleClickAuthor (event) {
      event.stopPropagation()
      this.$emit('clickAuthor', this)
    },
    handleAddReply (event) {
      event.stopPropagation()
      this.$emit('addReply', this)
    }
  },
  filters: {
    filterTime (value) {
      if (!value) {
        return '未知时间'
      }
      if (Object.prototype.toString.call(value) === '[object String]') {
        return value
      }
      if (value === '' || isNaN(value)) {
        return '未知时间'
      }
      if (value <= 0) {
        return '未知时间'
      }
      if (value < 10000000000) {
        value *= 1000
      }
      let time = new Date(value)
      let tY = time.getFullYear()
      let tM = time.getMonth() + 1 < 10 ? '0' + (time.getMonth() + 1) : time.getMonth() + 1
      let tD = time.getDate() < 10 ? '0' + time.getDate() : time.getDate()
      let th = time.getHours() < 10 ? '0' + time.getHours() : time.getHours()
      let tm = time.getMinutes() < 10 ? '0' + time.getMinutes() : time.getMinutes()
      let ts = time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds()
      let now = new Date()
      let nY = now.getFullYear()
      let nM = now.getMonth() + 1 < 10 ? '0' + (now.getMonth() + 1) : now.getMonth() + 1
      let nD = now.getDate() < 10 ? '0' + now.getDate() : now.getDate()
      let result = ''
      if (tY !== nY) {
        result += tY + '年'
      }
      if (tM !== nM || tD !== nD) {
        result += tM + '月'
        result += tD + '日'
      }
      if (result === '') {
        result = th + ':' + tm + ':' + ts
      }
      return result
    }
  }
}
</script>

<style scoped>
img {
  border: 0;
  vertical-align: middle;
}
.ml10 {
  margin-left: 10px !important;
}
.ml15 {
  margin-left: 15px !important;
}
.comments-item {
  padding: 15px 0;
  border-bottom: 1px solid rgba(0,0,0,0.09);
  box-sizing: border-box;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  -o-box-sizing: border-box;
  -ms-box-sizing: border-box;
  font-size: 14px;
}
.pull-left {
  float: left !important;
}
.pull-right {
  float: right !important;
}
.avatar-32 {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}
.comments-item a {
  color: #009a61;
  text-decoration: none;
  background: transparent;
}
.comments-item a:hover,
.comments-item a:active,
.comments-item a:focus {
  outline: 0;
}
.comments-box {
  padding-left: 47px;
}
.comments-box strong {
  font-weight: bold;
}
.comments-trigger {
  margin-bottom: 10px;
  color: #999;
  font-size: 13px;
}
.comments-option {
  /*visibility: hidden;*/
}
.comments-content {
  line-height: 1.6;
  word-wrap: break-word;
  margin-bottom: 10px !important;
}
.comments-content::before,
.comments-content::after {
  display: table;
}
.comments-content::after {
  content: "";
  clear: both;
}
.comments-ops {
  margin: 0;
  color: #999;
  font-size: 13px;
}
.comments-reply-btn {
  cursor: pointer;
}
.reply-list {
  margin-top: 10px;
  font-size: 13px;
  background-color: #FAFAFA;
  padding: 0 10px;
  color: #666;
  box-sizing: border-box;
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  -o-box-sizing: border-box;
  -ms-box-sizing: border-box;
}
.reply-item--ops {
  border-bottom: none;
}
.reply-item {
  padding-bottom: 10px;
  padding-top: 10px;
  word-break: break-word;
}
</style>

BhComments/packages/reply-item/index.js

import ReplyItem from './src/main'

ReplyItem.install = function (Vue) {
  Vue.component(ReplyItem.name, ReplyItem)
}

export default ReplyItem

BhComments/packages/reply-item/src/main.vue

<template>
  <div class="reply-item">
    <div class="reply-content-block">
      <div class="reply-content">
        <p>{{content}}</p>
      </div>
      <div class="comment-func inline-block">
        <span class="pull-right comment-tools ml15">
          <a href="javascript:void(0)" class="ml10" data-placement="top" :title="item.title" v-for="item in tools" :key="item.name" @click="handleClickTool($event, item)">
            <i :class="item.icon" v-if="item.icon"></i>
            <span v-if="item.text">{{item.text}}</span>
          </a>
        </span>
        <span class="comment-meta inline-block">
          <span> — </span>
          <a target="_blank" href="javascript:void(0)" @click="handleClickAuthor($event)">{{author}}</a>
          <span class="comments-date">  ·  {{time | filterTime}}</span>
        </span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ReplyItem',
  props: {
    author: String,
    content: String,
    tools: Array,
    time: [String, Number]
  },
  data () {
    return {
    }
  },
  computed: {
  },
  methods: {
    handleClickTool (event, tool) {
      event.stopPropagation()
      this.$emit('clickTool', this, tool)
    },
    handleClickAuthor (event) {
      event.stopPropagation()
      this.$emit('clickAuthor', this)
    }
  },
  filters: {
    filterTime (value) {
      if (!value) {
        return '未知时间'
      }
      if (Object.prototype.toString.call(value) === '[object String]') {
        return value
      }
      if (value === '' || isNaN(value)) {
        return '未知时间'
      }
      if (value <= 0) {
        return '未知时间'
      }
      if (value < 10000000000) {
        value *= 1000
      }
      let time = new Date(value)
      let tY = time.getFullYear()
      let tM = time.getMonth() + 1 < 10 ? '0' + (time.getMonth() + 1) : time.getMonth() + 1
      let tD = time.getDate() < 10 ? '0' + time.getDate() : time.getDate()
      let th = time.getHours() < 10 ? '0' + time.getHours() : time.getHours()
      let tm = time.getMinutes() < 10 ? '0' + time.getMinutes() : time.getMinutes()
      let ts = time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds()
      let now = new Date()
      let nY = now.getFullYear()
      let nM = now.getMonth() + 1 < 10 ? '0' + (now.getMonth() + 1) : now.getMonth() + 1
      let nD = now.getDate() < 10 ? '0' + now.getDate() : now.getDate()
      let result = ''
      if (tY !== nY) {
        result += tY + '年'
      }
      if (tM !== nM || tD !== nD) {
        result += tM + '月'
        result += tD + '日'
      }
      if (result === '') {
        result = th + ':' + tm + ':' + ts
      }
      return result
    }
  }
}
</script>

<style scoped>
.ml10 {
  margin-left: 10px !important;
}
.ml15 {
  margin-left: 15px !important;
}
.pull-left {
  float: left !important;
}
.pull-right {
  float: right !important;
}
.reply-item {
  padding-bottom: 10px;
  padding-top: 10px;
  border-bottom: 1px dashed rgba(0,0,0,0.09);
  word-break: break-word;
}
.reply-item a {
  color: #009a61;
  text-decoration: none;
  background: transparent;
}
.reply-item a:hover,
.reply-item a:active,
.reply-item a:focus {
  outline: 0;
}
.reply-item p {
  margin-bottom: 5px;
}
.comment-tools {
  /*visibility: hidden;*/
}
.comment-meta {
  color: #999;
}
.inline-block {
  display: inline-block;
}
</style>

使用组件

<script>
import Vue from 'vue'
import BhComments from '@/components/BhComments'
import CommentService from '@/request/comments/comment'
import ReplyService from '@/request/comments/reply'

Vue.use(BhComments)

export default {
  name: 'Dashboard',
  data () {
    return {
      comments: [],
      replys: {}
    }
  },
  created: function () {
    this.listComments()
  },
  watch: {
  },
  methods: {
    listComments () {
      let self = this
      CommentService.list({
        target: 2
      }).then(data => {
        data = data.result
        self.comments = data ? [].concat(data) : []
        if (self.comments.length > 0) {
          self.listReply()
        }
      })
    },
    listReply () {
      let self = this
      self.replys = {}
      if (self.comments.length < 0) {
        return
      }
      for (let i = 0; i < self.comments.length; i++) {
        let value = self.comments[i]
        ReplyService.list({
          cid: value.id
        }).then(data => {
          data = data.result
          self.$set(self.replys, value.id, data ? [].concat(data) : [])
        })
      }
    },
    handleClickAvatar (item) {
      console.log('点击了头像')
    },
    handleClickAuthor (item) {
      console.log('点击了用户')
    },
    handleAddReply (item) {
      console.log(item)
    }
  }
}
</script>

<template>
  <el-main>
    <comments-item
      v-for="comment in comments"
      :key="comment.id"
      :avatar="comment.headimg"
      :author="comment.author"
      :content="comment.content"
      :time="comment.createTime"
      :hasReply="replys[comment.id] && replys[comment.id].length > 0"
      @clickAvatar="handleClickAvatar(comment)"
      @clickAuthor="handleClickAuthor(comment)"
      @addReply="handleAddReply(comment)">
      <reply-item v-for="reply in replys[comment.id]" :key="reply.id" :author="reply.author" :content="reply.content" :time="reply.createTime">
      </reply-item>
    </comments-item>
  </el-main>
</template>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

数据格式

有同学问数据格式,不去整理成json了,把项目中的java对象贴上来供参考,写这个组件的时候用了两个数据,后来我用了一个数据结构包含了评论与回复,即评论与回复的数据放在一起了,target为0时顶层评论,大于0时表示为id=target的评论的回复。

CommentService 取到的格式

/**
 * 病历评论(点评)
 * @author crj
 *
 */
@Data
@Entity
@Table(name = "eber_main_record_record_comment")
public class RecordComment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long recordId; // 病历ID
    private Long uid; // 用户ID
    private String uname; // 用户姓名
    private Long time; // 时间
    @Lob
    private String content; // 评论内容
    private String headimg; // 头像
    private String oname; // 组织名称
    private Long target; // 评论的目标,如果是0,评论病历,如果是大于0,评论对象是target指向的ID的评论
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342