使用vue.js实现的留言板组件
效果图如下:
web端
app端
父组件:
<template>
<div class="msg-all-contain">
<div class="msg-board-title">留言板</div>
<div class="msg-board">
<div class="msg-board-contain">
<div class="msg-head">
<img v-if="userAvatar" :src="userAvatar" alt="" />
<img v-else :src="require('@/assets/default.jpg')" alt="" />
<textarea
type="textarea"
:class="inputStatusClass"
placeholder="请输入内容..."
ref="input"
v-model="newComment"
cols="60"
rows="5"
>
</textarea>
<button @click="submit">发表</button>
</div>
<div class="msg-content">
<comments-child
:comments="comments"
:count="layerCount"
></comments-child>
</div>
</div>
</div>
</div>
</template>
<script>
import CommentsChild from "@/components/comments/CommentsChild";
import dayjs from "dayjs";
export default {
name: "commentsMessage",
data() {
return {
// 评论列表
comments: [],
// 新评论
newComment: "",
// 用户头像
userAvatar: "",
// 非空判断
hasNoConent: false,
// 输入栏状态
inputStatusClass: "",
// 计数
layerCount: 0,
};
},
created() {},
mounted() {
this.initData();
},
components: {
"comments-child": CommentsChild,
},
methods: {
// 提交
submit() {
var self = this;
// 提交信息非空验证
if (!self.newComment || self.newComment === "") {
self.hasNoConent = true;
self.inputStatusClass = "no-content-warn";
return;
}
self.inputStatusClass = "";
// 生成comment对象
var comment = {
// 父评论Id
parentId: "",
// 评论内容
text: self.newComment,
// 发送者id
senderId: "1",
// 接收者id
receiverId: "",
// 发送时间
postDate: dayjs().format("YYYY-MM-DD HH:mm"),
// 发送者头像
senderAvatar: "",
// 子评论
children: [],
// 发送者姓名
senderName: "Wendy",
// comment id
id: "",
// 点赞
likes: 0
};
// 新添加的评论插入数组最前面
self.comments.unshift(comment);
// 清空评论内容
self.newComment = "";
},
initData() {
var self = this;
self.getComments();
},
// 获取数据
getComments() {
var self = this;
// 这是自己伪造的数据
// 要根据接口要求进行修改
self.comments = [
{
children: [
{
parentId: "55824b1c",
text: "回复信息",
senderId: "25d85a0f",
receiverId: "25d85a0f",
postDate: "2022-08-05 12:10",
senderAvatar: "",
children: [],
senderName: "Wendy",
id: "f0e3a81b",
likes: 0,
receiverName: "Irene",
receiverAvatar: "",
},
],
id: "55824b1c",
postDate: "2022-08-05 8:09",
senderName: "Wendy",
senderAvatar: "",
receiverName: null,
receiverAvatar: null,
parentId: null,
text: "测试信息",
senderId: "25d85a0f",
receiverId: null,
likes: 0,
},
];
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* 评论头像 */
.msg-head img {
width: 55px;
height: 55px;
border-radius: 50%;
position: absolute;
top: 22px;
left: 13px;
}
.msg-all-contain {
width: 100%;
height: 100%;
overflow-y: auto;
}
.msg-board-contain {
letter-spacing: 1px;
padding: 0 10px;
}
/* 信息栏标题 */
.msg-board-title {
width: auto;
text-align: center;
font-size: 28px;
font-weight: 300;
margin: 0 0 1.8rem 0;
min-height: 20px;
color: #000 !important;
font-family: "Lato", Verdana, sans-serif !important;
}
.msg-head {
background-color: rgb(248, 248, 248);
position: relative;
height: 130px;
border-radius: 5px;
}
/* 评论输入 */
.msg-head textarea {
position: absolute;
top: 13px;
left: 85px;
max-height: 60px;
border-radius: 5px;
outline: none;
width: calc(100% - 300px);
font-size: 16px;
padding: 20px;
border: 2px solid #f8f8f8;
resize: none;
}
/* 发布评论按钮 */
.msg-head button {
position: absolute;
top: 13px;
right: 35px;
width: 100px;
height: 100px;
border: 0;
border-radius: 5px;
font-size: 18px;
font-weight: 500;
color: #fff !important;
background-color: #00a1d6;
transition: 0.1s;
cursor: pointer;
letter-spacing: 2px;
}
/* 鼠标经过字体加粗 */
.msg-head button:hover {
/*font-weight: 600;*/
}
.msg-content {
overflow-y: auto;
}
/* 评论内容区域 */
.msg-content .child-comment {
display: flex;
position: relative;
padding: 18px 10px 18px 10px;
}
@media (max-width: 900px) {
.msg-head img {
width: 40px;
height: 40px;
border-radius: 50%;
position: absolute;
top: 22px;
left: 13px;
}
.msg-head textarea {
position: absolute;
top: 13px;
left: 70px;
height: 55px;
border-radius: 5px;
outline: none;
width: calc(100% - 200px);
font-size: 15px;
padding: 10px;
border: 2px solid #f8f8f8;
resize: none;
}
.msg-head button {
position: absolute;
top: 13px;
right: 16px;
width: 80px;
height: 77px;
border: 0;
border-radius: 5px;
font-size: 14px;
font-weight: 500;
color: #fff !important;
background-color: #00a1d6;
transition: 0.1s;
cursor: pointer;
letter-spacing: 2px;
}
}
</style>
子组件
<template>
<div>
<div
:class="count > 0 ? '' : 'comments-child-contain'"
v-for="(item, index) in comments"
:key="index"
>
<!--style 根据层级缩进-->
<div class="comments-child" :style="{ paddingLeft: 30 * count + 'px' }">
<div
:class="count > 0 ? 'comments-child-img-sm' : 'comments-child-img'"
>
<img v-if="item.senderAvatar" :src="item.senderAvatar" alt="" />
<img v-else :src="require('@/assets/default.jpg')" alt="" />
</div>
<div class="comments-child-content">
<!-- 用户信息 -->
<div class="comments-child-username-contain">
<h3 class="comments-child-username">{{ item.senderName }}</h3>
<div
v-if="item.receiverId && item.receiverId !== ''"
class="comments-child-replay"
>
<span class="reply-text">回复</span>
<h4 class="comments-child-at-username">
@{{ item.receiverName }}
</h4>
</div>
</div>
<!-- 评论内容 -->
<p class="comments-comments-child">
{{ item.text }}
</p>
<div class="comments-child-bottom-contain">
<!-- 发布时间 -->
<span class="comments-child-time"> {{ item.postDate }} </span>
<!--删除和回复-->
<div class="comments-child-right">
<span class="fa fa-thumbs-up delete" @click="commentLike(item)">{{
item.likes
}}</span>
<span
class="fa fa-trash-o delete"
@click="commentDelete(item, $event)"
v-show="false"
>删除</span
>
<span
v-if="layerCount"
class="fa fa-comment-o comments"
@click="goReply(item, $event)"
>回复</span
>
</div>
</div>
<div class="reply-comment">
<img v-if="userAvatar" :src="userAvatar" alt="" />
<img v-else :src="require('@/assets/default.jpg')" alt="" />
<input
:class="inputStatusClass"
type="text"
v-model="replyComment"
@keyup.enter="replySumbit(item, $event)"
/>
<button @click="replySumbit(item, $event)">回复</button>
</div>
</div>
</div>
<!--递归调用-->
<div v-if="item.children">
<comments-child
:comments="item.children"
:count="layerCount"
></comments-child>
</div>
</div>
</div>
</template>
<script>
export default {
name: "CommentsChild",
data() {
return {
// 回复评论
replyComment: "",
// 非空验证
hasNoConent: false,
inputStatusClass: "",
layerCount: 0,
userAvatar: "",
// 点赞数
like: 0,
};
},
created() {
var _this = this;
_this.layerCount = _this.count;
_this.layerCount++;
// 子评论限制为一层
// if (_this.layerCount < 1) {
// _this.layerCount++;
// } else {
// _this.layerCount = 1;
// }
},
mounted() {
},
props: {
// 卡片内容
comments: {
type: Array,
required: true,
},
// 子评论计数
count: {
type: Number,
default: 0,
},
},
watch: {},
methods: {
commentDelete(obj) {},
// 点赞
commentLike(obj) {
var _this = this;
obj.likes++;
},
// 显示回复输入框
goReply(obj, event) {
var _this = this;
_this.inputStatusClass = "";
_this.replyComment = "";
var _thisDom = event.currentTarget;
// 注意 nextElementSibling
var replyDom = _thisDom.parentNode.parentNode.nextElementSibling;
// 显示回复输入
if (replyDom.style.display === "" || replyDom.style.display === "none") {
replyDom.style.display = "flex";
var replyInput = replyDom.getElementsByTagName("input")[0];
// 添加回复人信息
var placeContent = "回复" + " @ " + obj.senderName;
replyInput.setAttribute("placeholder", placeContent);
} else {
replyDom.style.display = "none";
}
},
// 回复信息提交
replySumbit(obj, event) {
var _thisDom = event.currentTarget;
var replyDom = _thisDom.parentNode;
var _this = this;
// 回复内容非空验证
if (!_this.replyComment || _this.replyComment === "") {
_this.hasNoConent = true;
_this.inputStatusClass = "no-content-warn";
return;
}
_this.inputStatusClass = "";
var reply = {
objectId: obj.objectId,
parentId: obj.id,
type: "Class",
text: _this.replyComment,
senderId: obj.userId,
receiverId: obj.senderId,
senderAvatar: "",
children: [],
receiverName: obj.senderName,
senderName: obj.senderName,
objectId: obj.objectId,
receiverId: obj.receiverId,
senderId: obj.senderId,
postDate: obj.postDate,
id: obj.id,
likes: 0
};
// 更新回复数组,将新回复添加到数组尾部
_this.$set(obj.children, obj.children.length, reply);
// 清空内容
_this.replyComment = "";
replyDom.style.display = "none";
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
/* 评论内容区域 */
.msg-content .comments-child {
display: flex;
position: relative;
padding: 18px 10px 18px 10px;
}
.comments-child-contain {
border-bottom: 1px solid #d3d9e1;
padding: 0 25px;
}
/* 子评论头像 */
.comments-child .comments-child-img {
/*flex: 1;*/
text-align: center;
padding: 0 20px 0 0;
}
/* 子评论头像 */
.comments-child-img > img {
width: 50px;
height: 50px;
border-radius: 50%;
}
/* 子评论小头像 */
.comments-child .comments-child-img-sm {
/*flex: 1;*/
text-align: center;
padding: 0 20px 0 0;
}
/* 子评论小头像 */
.comments-child-img-sm > img {
width: 35px;
height: 35px;
border-radius: 50%;
}
/* 子评论用户名 */
.comments-child-username {
color: #504f4f;
margin: 0;
font-size: 15px;
width: auto;
text-align: left;
}
/* 子评论回复用户名 */
.comments-child-at-username {
margin: 0;
color: #00a1d6;
}
.comments-child-username-contain {
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: nowrap;
/*margin-bottom: 15px;*/
}
/* 回复内容 */
.reply-text {
margin: 0 10px;
font-size: 16px;
font-weight: 400;
color: #000 !important;
font-family: "Lato", Verdana, sans-serif !important;
}
.comments-child-replay {
display: flex;
align-items: center;
font-size: 15px;
margin: 0;
}
.comments-child-content {
flex: 9;
}
/* 回复时间 */
.comments-child-time {
color: #767575;
font-size: 12px;
white-space: nowrap;
}
.comments-comments-child {
font-size: 16px;
margin-top: 10px;
margin-bottom: 10px;
font-weight: 400;
color: #000 !important;
font-family: "Lato", Verdana, sans-serif !important;
text-align: left;
}
.comments-child-bottom-contain {
display: flex;
align-items: center;
}
/* 右边点赞和评论 */
.comments-child-right {
position: absolute;
right: 1.5%;
top: 10px;
white-space: nowrap;
}
.comments-child-right span {
font-weight: 400;
font-size: 15px;
margin: 0 20px;
cursor: pointer;
color: #333 !important;
}
/* 删除评论 */
.delete:hover {
color: red;
}
.delete::before {
/* 想使用的icon的十六制编码,去掉&#x之后的 */
margin-right: 4px;
font-size: 16px;
}
/* 评论字体图标 */
.comments::before {
/* 想使用的icon的十六制编码,去掉&#x之后的 */
margin-right: 4px;
font-size: 16px;
}
/* 点赞字体图标 */
.praise::before {
/* 想使用的icon的十六制编码,去掉&#x之后的 */
content: "\ec7f";
/* 必须加 */
font-family: "iconfont";
margin-right: 4px;
font-size: 19px;
}
.to_reply {
color: rgb(106, 106, 106);
}
/* 评论 */
.reply-comment {
margin: 10px 0 0 0;
align-items: center;
justify-content: space-around;
display: none;
}
/* 评论输入框头像 */
.reply-comment > img {
width: 50px;
height: 50px;
border-radius: 50%;
}
/* 评论输入框 */
.reply-comment input {
height: 40px;
border-radius: 5px;
outline: none;
width: 70%;
font-size: 16px;
padding: 0 10px;
/* border: 2px solid #f8f8f8; */
border: 2px solid skyblue;
}
/* 发布评论按钮 */
.reply-comment button {
width: 100px;
height: 43px;
border: 0;
border-radius: 5px;
font-size: 16px;
font-weight: 500;
letter-spacing: 2px;
color: #fff !important;
background-color: #00a1d6;
cursor: pointer;
}
/* 鼠标经过字体加粗 */
.reply-comment button:hover {
}
/* 评论点赞颜色 */
.comment-like {
color: red;
}
.no-content-warn {
border: 1px solid red !important;
}
@media (max-width: 900px) {
.comments-child-right {
position: inherit;
margin-left: 10px;
}
.comments-child > img {
width: 40px;
height: 40px;
border-radius: 50%;
}
.reply-comment button {
width: 50px;
height: 43px;
border: 0;
border-radius: 5px;
font-size: 14px;
font-weight: 500;
color: #fff !important;
background-color: #00a1d6;
cursor: pointer;
}
.reply-comment input {
height: 40px;
border-radius: 5px;
outline: none;
width: 50%;
font-size: 16px;
padding: 0 10px;
margin: 0 10px;
/* border: 2px solid #f8f8f8; */
border: 2px solid skyblue;
}
.comments-child-right span {
font-weight: 400;
font-size: 12px;
margin: 0 5px;
cursor: pointer;
color: #333 !important;
}
.reply-comment {
justify-content: flex-start;
}
.container-fluid {
position: relative;
}
.comments-child-username-contain {
flex-wrap: wrap;
}
.comments-child-username {
width: 100%;
}
.comments-child-replay {
margin-top: 10px;
}
.reply-text {
margin: 0 10px 0 0;
}
.msg-class {
font-size: 25px;
line-height: 26px;
}
}
</style>
主要注意点是:
- 递归组件
- 通过count检测子组件递归次数
- 接口返回的字段
项目代码在下面
项目地址:https://gitee.com/joeyan3/joe-vue-demo-project.git