前言
这两天在用vue重构之前写的一个社区博客项目,之前评论的样式和效果都差强人意
在写完这个功能后,由心觉得Vue真的非常好用。
话不多说,先上效果图
代码
html代码:
1 <template> 2 <div> 3 <div v-clickoutside="hideReplyBtn" @click="inputFocus" class="my-reply"> 4 <el-avatar class="header-img" :size="40" :src="myHeader"></el-avatar> 5 <div class="reply-info" > 6 <div 7 tabindex="0" 8 contenteditable="true" 9 id="replyInput" 10 spellcheck="false" 11 placeholder="输入评论..." 12 class="reply-input" 13 @focus="showReplyBtn" 14 @input="onDivInput($event)" 15 > 16 </div> 17 </div> 18 <div class="reply-btn-box" v-show="btnShow"> 19 <el-button class="reply-btn" size="medium" @click="sendComment" type="primary">发表评论</el-button> 20 </div> 21 </div> 22 <div v-for="(item,i) in comments" :key="i" class="author-title reply-father"> 23 <el-avatar class="header-img" :size="40" :src="item.headImg"></el-avatar> 24 <div class="author-info"> 25 <span class="author-name">{{item.name}}</span> 26 <span class="author-time">{{item.time}}</span> 27 </div> 28 <div class="icon-btn"> 29 <span @click="showReplyInput(i,item.name,item.id)"><i class="iconfont el-icon-s-comment"></i>{{item.commentNum}}</span> 30 <i class="iconfont el-icon-caret-top"></i>{{item.like}} 31 </div> 32 <div class="talk-box"> 33 <p> 34 <span class="reply">{{item.comment}}</span> 35 </p> 36 </div> 37 <div class="reply-box"> 38 <div v-for="(reply,j) in item.reply" :key="j" class="author-title"> 39 <el-avatar class="header-img" :size="40" :src="reply.fromHeadImg"></el-avatar> 40 <div class="author-info"> 41 <span class="author-name">{{reply.from}}</span> 42 <span class="author-time">{{reply.time}}</span> 43 </div> 44 <div class="icon-btn"> 45 <span @click="showReplyInput(i,reply.from,reply.id)"><i class="iconfont el-icon-s-comment"></i>{{reply.commentNum}}</span> 46 <i class="iconfont el-icon-caret-top"></i>{{reply.like}} 47 </div> 48 <div class="talk-box"> 49 <p> 50 <span>回复 {{reply.to}}:</span> 51 <span class="reply">{{reply.comment}}</span> 52 </p> 53 </div> 54 <div class="reply-box"> 55 56 </div> 57 </div> 58 </div> 59 <div v-show="_inputShow(i)" class="my-reply my-comment-reply"> 60 <el-avatar class="header-img" :size="40" :src="myHeader"></el-avatar> 61 <div class="reply-info" > 62 <div tabindex="0" contenteditable="true" spellcheck="false" placeholder="输入评论..." @input="onDivInput($event)" class="reply-input reply-comment-input"></div> 63 </div> 64 <div class=" reply-btn-box"> 65 <el-button class="reply-btn" size="medium" @click="sendCommentReply(i,j)" type="primary">发表评论</el-button> 66 </div> 67 </div> 68 </div> 69 </div> 70 </template>
Js 代码如下
我把模拟的数据写在了data里面,显得js有点长。如果要更改数据的格式的话,记得也要改Html不然会出错。
1 <script> 2 const clickoutside = { 3 // 初始化指令 4 bind(el, binding, vnode) { 5 function documentHandler(e) { 6 // 这里判断点击的元素是否是本身,是本身,则返回 7 if (el.contains(e.target)) { 8 return false; 9 } 10 // 判断指令中是否绑定了函数 11 if (binding.expression) { 12 // 如果绑定了函数 则调用那个函数,此处binding.value就是handleClose方法 13 binding.value(e); 14 } 15 } 16 // 给当前元素绑定个私有变量,方便在unbind中可以解除事件监听 17 el.vueClickOutside = documentHandler; 18 document.addEventListener('click', documentHandler); 19 }, 20 update() {}, 21 unbind(el, binding) { 22 // 解除事件监听 23 document.removeEventListener('click', el.vueClickOutside); 24 delete el.vueClickOutside; 25 }, 26 }; 27 export default { 28 name:'ArticleComment', 29 data(){ 30 return{ 31 btnShow: false, 32 index:'0', 33 replyComment:'', 34 myName:'Lana Del Rey', 35 myHeader:'https://ae01.alicdn.com/kf/Hd60a3f7c06fd47ae85624badd32ce54dv.jpg', 36 myId:19870621, 37 to:'', 38 toId:-1, 39 comments:[ 40 { 41 name:'Lana Del Rey', 42 id:19870621, 43 headImg:'https://ae01.alicdn.com/kf/Hd60a3f7c06fd47ae85624badd32ce54dv.jpg', 44 comment:'我发布一张新专辑Norman Fucking Rockwell,大家快来听啊', 45 time:'2019年9月16日 18:43', 46 commentNum:2, 47 like:15, 48 inputShow:false, 49 reply:[ 50 { 51 from:'Taylor Swift', 52 fromId:19891221, 53 fromHeadImg:'https://ae01.alicdn.com/kf/H94c78935ffa64e7e977544d19ecebf06L.jpg', 54 to:'Lana Del Rey', 55 toId:19870621, 56 comment:'我很喜欢你的新专辑!!', 57 time:'2019年9月16日 18:43', 58 commentNum:1, 59 like:15, 60 inputShow:false 61 }, 62 { 63 from:'Ariana Grande', 64 fromId:1123, 65 fromHeadImg:'https://ae01.alicdn.com/kf/Hf6c0b4a7428b4edf866a9fbab75568e6U.jpg', 66 to:'Lana Del Rey', 67 toId:19870621, 68 comment:'别忘记宣传我们的合作单曲啊', 69 time:'2019年9月16日 18:43', 70 commentNum:0, 71 like:5, 72 inputShow:false 73 74 } 75 ] 76 }, 77 { 78 name:'Taylor Swift', 79 id:19891221, 80 headImg:'https://ae01.alicdn.com/kf/H94c78935ffa64e7e977544d19ecebf06L.jpg', 81 comment:'我发行了我的新专辑Lover', 82 time:'2019年9月16日 18:43', 83 commentNum:1, 84 like:5, 85 inputShow:false, 86 reply:[ 87 { 88 from:'Lana Del Rey', 89 fromId:19870621, 90 fromHeadImg:'https://ae01.alicdn.com/kf/Hd60a3f7c06fd47ae85624badd32ce54dv.jpg', 91 to:'Taylor Swift', 92 toId:19891221, 93 comment:'新专辑和speak now 一样棒!', 94 time:'2019年9月16日 18:43', 95 commentNum:25, 96 like:5, 97 inputShow:false 98 99 } 100 ] 101 }, 102 { 103 name:'Norman Fucking Rockwell', 104 id:20190830, 105 headImg:'https://ae01.alicdn.com/kf/Hdd856ae4c81545d2b51fa0c209f7aa28Z.jpg', 106 comment:'Plz buy Norman Fucking Rockwell on everywhere', 107 time:'2019年9月16日 18:43', 108 commentNum:0, 109 like:5, 110 inputShow:false, 111 reply:[] 112 }, 113 ] 114 } 115 }, 116 directives: {clickoutside}, 117 methods: { 118 inputFocus(){ 119 var replyInput = document.getElementById('replyInput'); 120 replyInput.style.padding= "8px 8px" 121 replyInput.style.border ="2px solid blue" 122 replyInput.focus() 123 }, 124 showReplyBtn(){ 125 this.btnShow = true 126 }, 127 hideReplyBtn(){ 128 this.btnShow = false 129 replyInput.style.padding= "10px" 130 replyInput.style.border ="none" 131 }, 132 showReplyInput(i,name,id){ 133 this.comments[this.index].inputShow = false 134 this.index =i 135 this.comments[i].inputShow = true 136 this.to = name 137 this.toId = id 138 }, 139 _inputShow(i){ 140 return this.comments[i].inputShow 141 }, 142 sendComment(){ 143 if(!this.replyComment){ 144 this.$message({ 145 showClose: true, 146 type:'warning', 147 message:'评论不能为空' 148 }) 149 }else{ 150 let a ={} 151 let input = document.getElementById('replyInput') 152 let timeNow = new Date().getTime(); 153 let time= this.dateStr(timeNow); 154 a.name= this.myName 155 a.comment =this.replyComment 156 a.headImg = this.myHeader 157 a.time = time 158 a.commentNum = 0 159 a.like = 0 160 this.comments.push(a) 161 this.replyComment = '' 162 input.innerHTML = ''; 163 164 } 165 }, 166 sendCommentReply(i,j){ 167 if(!this.replyComment){ 168 this.$message({ 169 showClose: true, 170 type:'warning', 171 message:'评论不能为空' 172 }) 173 }else{ 174 let a ={} 175 let timeNow = new Date().getTime(); 176 let time= this.dateStr(timeNow); 177 a.from= this.myName 178 a.to = this.to 179 a.fromHeadImg = this.myHeader 180 a.comment =this.replyComment 181 a.time = time 182 a.commentNum = 0 183 a.like = 0 184 this.comments[i].reply.push(a) 185 this.replyComment = '' 186 document.getElementsByClassName("reply-comment-input")[i].innerHTML = "" 187 } 188 }, 189 onDivInput: function(e) { 190 this.replyComment = e.target.innerHTML; 191 }, 192 dateStr(date){ 193 //获取js 时间戳 194 var time=new Date().getTime(); 195 //去掉 js 时间戳后三位,与php 时间戳保持一致 196 time=parseInt((time-date)/1000); 197 //存储转换值 198 var s; 199 if(time<60*10){//十分钟内 200 return '刚刚'; 201 }else if((time<60*60)&&(time>=60*10)){ 202 //超过十分钟少于1小时 203 s = Math.floor(time/60); 204 return s+"分钟前"; 205 }else if((time<60*60*24)&&(time>=60*60)){ 206 //超过1小时少于24小时 207 s = Math.floor(time/60/60); 208 return s+"小时前"; 209 }else if((time<60*60*24*30)&&(time>=60*60*24)){ 210 //超过1天少于30天内 211 s = Math.floor(time/60/60/24); 212 return s+"天前"; 213 }else{ 214 //超过30天ddd 215 var date= new Date(parseInt(date)); 216 return date.getFullYear()+"/"+(date.getMonth()+1)+"/"+date.getDate(); 217 } 218 } 219 }, 220 } 221 </script>
css 代码
1 <style lang="scss" scoped> 2 .my-reply { 3 padding: 10px; 4 background-color: #fafbfc; 5 .header-img { 6 display: inline-block; 7 vertical-align: top; 8 } 9 10 .reply-info { 11 display: inline-block; 12 margin-left: 5px; 13 width: 90%; 14 @media screen and (max-width: 1200px) { 15 width: 80%; 16 } 17 .reply-input { 18 min-height: 20px; 19 line-height: 22px; 20 padding: 10px 10px; 21 color: #ccc; 22 background-color: #fff; 23 border-radius: 5px; 24 &:empty:before { 25 content: attr(placeholder); 26 } 27 &:focus:before { 28 content: none; 29 } 30 &:focus { 31 padding: 8px 8px; 32 border: 2px solid blue; 33 box-shadow: none; 34 outline: none; 35 } 36 } 37 } 38 .reply-btn-box { 39 height: 25px; 40 margin: 10px 0; 41 .reply-btn { 42 position: relative; 43 float: right; 44 margin-right: 15px; 45 } 46 } 47 } 48 .my-comment-reply { 49 margin-left: 50px; 50 .reply-input { 51 width: flex; 52 } 53 } 54 55 .author-title:not(:last-child) { 56 border-bottom: 1px solid rgba(178, 186, 194, 0.3); 57 } 58 59 .author-title { 60 padding: 10px; 61 .header-img { 62 display: inline-block; 63 vertical-align: top; 64 } 65 .author-info { 66 display: inline-block; 67 margin-left: 5px; 68 width: 60%; 69 height: 40px; 70 line-height: 20px; 71 > span { 72 display: block; 73 cursor: pointer; 74 overflow: hidden; 75 white-space: nowrap; 76 text-overflow: ellipsis; 77 } 78 .author-name { 79 color: #000; 80 font-size: 18px; 81 font-weight: bold; 82 } 83 84 .author-time { 85 font-size: 14px; 86 } 87 } 88 .icon-btn { 89 width: 30%; 90 padding: 0 !important ; 91 float: right; 92 @media screen and (max-width: 1200px) { 93 width: 20%; 94 padding: 7px; 95 } 96 > span { 97 cursor: pointer; 98 } 99 .iconfont { 100 margin: 0 5px; 101 } 102 } 103 .talk-box { 104 margin: 0 50px; 105 > p { 106 margin: 0; 107 } 108 .reply { 109 font-size: 16px; 110 color: #000; 111 } 112 } 113 .reply-box { 114 margin: 10px 0 0 50px; 115 background-color: #efefef; 116 } 117 } 118 </style>