有需要的小伙伴直接拿代码即可,不需要下载依赖。
评论组件如下:
创建 comment.vue 文件。
表情组件 VueEmoji.vue 在评论组件中使用。
<template>
<div class="comment">
<div class="flex_box">
<h2>评论 {{total}}</h2>
<p>文明上网理性发言</p>
</div>
<div class="comment-header">
<div class="header_children">
<i class="el-icon-user-solid img_width"></i>
<el-input
:placeholder="placeholderText"
v-model="context"
class="input"
type="textarea"
resize="none"
size="mini"
clearable
:maxlength="contentLength"
@focus="isShowSecReply(undefined)"
></el-input>
</div>
<div class="header_btn">
<!-- 表情 -->
<vue-emoji @chooseEmoji="chooseEmoji"></vue-emoji>
<el-button
type="primary"
style="height: 40px"
@click="addComment()"审核
>{{ buttonText }}</el-button>
</div>
</div>
<div v-loading="bigLoading">
<div class="comment-body" v-for="(item, index) in comments" :key="item.id + '' + index">
<!-- 一级评论 -->
<div class="first-comment">
<i class="el-icon-user-solid img_width"></i>
<div class="content">
<!-- 一级评论用户昵称 -->
<div style="display: flex;align-items: center;">
<h3>{{ item.memberName }}</h3>
<span v-if="item.auditStatus == '0'" style="color: #ddab16;margin-left: 10px">(待审核)</span>
</div>
<!-- 一级评论发布时间 -->
<span>{{ item.createTime }}</span>
<!-- 一级评论评论内容 -->
<p>{{ item.commentContent }}</p>
<!-- 一级评论评论点赞 -->
<div class="comment-right">
<i
class="el-icon-chat-dot-round"
@click="isShowSecReply(item)"
>回复</i>
<i
v-if="userInfor.memberName == item.memberName"
class="el-icon-delete"
@click="deleteComment(item, undefined)"
>删除</i>
</div>
<!-- 回复一级评论 -->
<div class="reply-comment" v-show="isShowSec === item.id">
<el-input
:placeholder="placeholderText"
class="input"
v-model.trim="replyContext"
:maxlength="contentLength"
clearable
></el-input>
<div class="flex_one_box">
<!-- 表情 -->
<vue-emoji @chooseEmoji="chooseEmoji" :layer="comments"></vue-emoji>
<el-button
type="primary"
size="mini"
class="reply-button"
@click="addComment(item)"
>回复</el-button>
</div>
</div>
<!-- 次级查看更多 -->
<div class="li_top" v-if="item.commentReplyInfoList?.length > 0" @click="hanlePublicTotal(item)">
<span style="color: rgb(21 66 231);">共{{item.commentReplyCount}}条评论</span>
<i class="el-icon-caret-bottom" v-show="item.publicTotal"></i>
<i class="el-icon-caret-top" v-show="!item.publicTotal"></i>
</div>
<!-- 次级评论 -->
<ul v-infinite-scroll="load(item.commentReplyInfoList)" infinite-scroll-delay="200" style="overflow:auto" class="infinite_list" v-if="item.publicTotal">
<li v-for="(reply, index) in item.commentReplyInfoList" :key="reply.id + '' + index">
<!-- 次级评论头像,该用户没有头像则显示默认头像 -->
<template>
<div class="second-comment">
<i class="el-icon-user-solid img_width"></i>
<div class="content">
<!-- 次级评论用户昵称 -->
<div style="display: flex;align-items: center;">
<h3>{{ reply.memberName }}</h3>
<span v-if="reply.auditStatus == '0'" style="color: #ddab16;margin-left: 10px">(待审核)</span>
</div>
<!-- 次级评论评论时间 -->
<span>{{ reply.createTime }}</span>
<span class="to_reply">{{ reply.memberName }}</span>
回复
<span class="to_reply">{{ reply.replyMemberName }}</span>:
<p>{{ reply.commentContent }}</p>
<!-- 次级评论评论点赞 -->
<div class="comment-right">
<i
class="el-icon-chat-dot-round"
@click="isShowSecReply(reply)"
>回复</i>
<i
v-if="userInfor.memberName == reply.memberName"
class="el-icon-delete"
@click="deleteComment(item, reply)"
>删除</i>
</div>
<div class="reply-comment" v-show="isShowSec === reply.id">
<el-input
:placeholder="placeholderText"
class="input"
v-model.trim="replyContext"
:maxlength="contentLength"
clearable
></el-input>
<div class="flex_one_box">
<!-- 表情 -->
<vue-emoji @chooseEmoji="chooseEmoji" :layer="item.commentReplyInfoList"></vue-emoji>
<el-button
type="primary"
size="mini"
class="reply-button"
@click="addComment(item,reply)"
>回复</el-button>
</div>
</div>
</div>
</div>
</template>
</li>
<el-button class="loading_p" type="primary" size="mini" v-if="item.moreValue" :loading="childrenMorelist.loading" @click="changeChildrenMore(item)">加载更多</el-button>
</ul>
</div>
</div>
</div>
</div>
<!-- 一级查看更多 -->
<el-button type="info" class="moreBtn" :loading="morelist.loading" @click="changeMore" v-if="morelist.value">
查看更多评论
<i class="el-icon-caret-bottom"></i>
</el-button>
<!-- <div v-else class="more_div">暂无更多评论</div> -->
<!-- 暂无评论的空状态 -->
<el-empty :description="emptyText" v-if="comments.length === 0"></el-empty>
</div>
</template>
<script>
export default {
props: {
sourceType: {
type: String,
require: true
},
articleId: {
//评论所属文章 id
type: String
},
emptyText: {
// 评论为空的时候显示的文字
type: String,
default: "期待你的评论!"
},
buttonText: {
// 按钮文字
type: String,
default: "评论"
},
contentLength: {
// 评论长度
type: Number,
default: 150
},
placeholderText: {
// 默认显示文字
type: String,
default: "请输入最多150字的评论..."
}
},
data() {
return {
bigLoading: false,
userInfor: {},
params: {
pageNum: "1",
pageSize: "5",
orderByColumn: "createTime",
isAsc: "asc",
sourceType: this.sourceType,
mainId: "",
},
pantPageNum: "1", // 父级查看更多
total: 0,
comments: [
{
auditStatus: "1", // 审核状态
auditTime: "2024-05-27", // 审核时间
auditUserId: null,
commentContent: "999", // 内容
commentReplyCount: 1, // 二级回复条数
commentReplyInfoList: [], // 二级回复数组
createBy: "admin", //
createTime: "2024-05-27 01:39:18",
id: 242,
ids: null,
mainId: 0,
memberId: 1, // 哪条详情下评论的详情id
memberName: "admin", // 用户名
operationUserName: "自动通过",
parentId: 0, // 父级id
remark: null,
replyMemberName: null, // 回复的用户名
sourceId: "20",
sourceType: "1", // 类型
title: null,
updateBy: "系统"
}
], // 获取得到的评论
context: "", // 评论内容
replyContext: "", //一级评论回复
isShowSec: "", //是否显示次级回复框
isClickId: "", //记录点击回复的评论id
morelist: {
index: '2',
value: false,
loading: false,
},
childrenMorelist: {
index: '2',
childrenId: "",
childrenPageNum: "1", // 子级查看更多
childrenTotal: 0,
value: false,
loading: false,
},
};
},
mounted() {
this.userInfor = localStorage.getItem("userInfor") ? JSON.parse(localStorage.getItem("userInfor")) : {};
// 获取评论数据
this.getCommentList();
},
computed: {
isLogin() {
return this.$store.getters.isLogin;
},
},
methods: {
chooseEmoji(layer,val) {
if(layer) {
this.replyContext += val;
return;
}
this.context += val;
},
// 查看更多评论
async changeMore() {
this.morelist.loading = true;
this.pantPageNum++;
this.params.pageNum = this.pantPageNum;
console.log("this.params.pageNum",this.params.pageNum);
let res = await this.getNewList();
res.rows.map(item => {
this.$set(item,'publicTotal',false)
})
this.comments.push(...res.rows);
this.morelist.loading = false;
this.total = res.total;
if(res.total > this.params.pageNum * this.params.pageSize) {
this.morelist.value = true;
}else {
this.morelist.value = false;
}
},
// 加载更多
async changeChildrenMore(item) {
if(this.childrenMorelist.childrenId && this.childrenMorelist.childrenId != item.id) {
this.childrenMorelist.childrenPageNum = "1";
}
this.childrenMorelist.childrenId = item.id;
this.childrenMorelist.loading = true;
this.childrenMorelist.childrenPageNum++;
this.params.pageNum = this.childrenMorelist.childrenPageNum;
this.params.mainId = item.id;
let res = await this.getNewList();
this.childrenMorelist.childrenTotal = res.total;
console.log("res",res);
item.commentReplyInfoList.push(...res.rows);
this.childrenMorelist.loading = false;
if(res.total > this.params.pageNum * this.params.pageSize) {
item.moreValue = true;
}else {
item.moreValue = false;
}
},
load(list) {
list += 2
},
// 唤起文件选择
handleClick() {
this.$refs.avatar.click();
},
// 获取本篇文章所有评论
async getCommentList() {
try {
this.comments = [];
this.bigLoading = true;
// 获取某篇文章下的所有评论
const res = await this.$get(`/api/comment/list?pageNum=${this.params.pageNum}&pageSize=${this.params.pageSize}&orderByColumn=${this.params.orderByColumn} desc,auditStatus&isAsc=${this.params.isAsc}&sourceType=${this.params.sourceType}&mainId=${this.params.mainId}&sourceId=${this.$route.query.id}`)
this.bigLoading = false;
this.comments = res.rows; //评论列表
this.total = res.total; //评论总数
if(this.total > this.params.pageSize) {
this.morelist.value = true;
}else {
this.morelist.value = false;
}
this.comments.map(item => {
if(item.commentReplyCount > 5) {
item.moreValue = true;
}else {
item.moreValue = false;
}
this.$set(item,'publicTotal',false)
})
} catch (err) {
this.bigLoading = false;
this.$message.error(err);
}
},
async getNewList() {
let res = await this.$get(`/api/comment/list?pageNum=${this.params.pageNum}&pageSize=${this.params.pageSize}&orderByColumn=${this.params.orderByColumn} desc,auditStatus&isAsc=${this.params.isAsc}&sourceType=${this.params.sourceType}&mainId=${this.params.mainId}&sourceId=${this.$route.query.id}`);
console.log("res1111",res);
return res;
},
// 控制二级条数显示盒子
hanlePublicTotal(item) {
item.publicTotal = !item.publicTotal;
console.log("item",item);
},
isShowSecReply(item) {
console.log("一级input获取焦点",item);
let id = item?.id;
if (id) {
this.isShowSec = id;
if (this.isClickId === this.isShowSec) {
this.isShowSec = "";
} else {
this.isShowSec = id;
}
this.isClickId = this.isShowSec;
} else {
this.isShowSec = this.isClickId = "";
}
},
// 删除
deleteComment(item, reply) {
if(!this.isLogin) {
this.$message.warning("请先登录!");
return;
}
let _id = item.id;
let replyId = reply?.id;
if (replyId) {
// 删除二级评论,提交请求到后端
this.$confirm('确认删除此评论?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$put(`/api/comment/${replyId}`).then(async (res) => {
if (res.code === 200) {
this.params.pageNum = '1';
let data = await this.getNewList();
console.log("data",data);
item.commentReplyCount = data.total;
item.commentReplyInfoList.splice(0);
item.commentReplyInfoList.push(...data.rows);
//
this.$message({
message: "删除成功",
type: "success",
});
}
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
})
} else {
// 删除一级评论,提交请求到后端
this.$confirm('确认删除此评论?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$put(`/api/comment/${_id}`).then((res) => {
if (res.code === 200) {
this.$message({
message: "删除成功",
type: "success",
});
this.params.pageNum = '1';
this.getCommentList();
}
});
this.total--;
if(this.total == this.params.pageSize) {
this.morelist.value = false;
}
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
})
}
},
// 评论添加成功,返回的数据
async addComment(item,reply) {
console.log(">>>>item",item );
if(!this.isLogin) {
this.$message.warning("请先登录!");
return;
}
//本地更新评论列表
if (item) {
// 添加二级评论
if (!this.replyContext) {
this.$message.warning("评论或留言不能为空哦!");
return;
}
this.addSecond(item,reply);
} else {
// 添加一级评论,提交数据到后端
if (!this.context) {
this.$message.warning("评论或留言不能为空哦!");
return;
}
this.addFirst();
}
this.isShowSec = this.isClickId = "";
},
addSecond(item,reply) {
// 模拟数据提交成功后返回数据
let data = {
mainId: item.id,
parentId: item.id,
sourceType: item.sourceType,
sourceId: this.$route.query.id,
commentContent: this.replyContext //评论内容
};
if(reply) {
data.parentId = reply.id;
}
this.$post("/api/comment",data).then(async(res) => {
if(res.code == 200) {
this.params.mainId = item.id;
let res = await this.getNewList();
if(!item.commentReplyInfoList) {
item.commentReplyInfoList = [];
}
item.commentReplyInfoList.splice(0);
item.commentReplyInfoList.push(...res.rows);
item.commentReplyCount = res.total;
if(res.total > 5) {
item.moreValue = true;
}
this.replyContext = "";
console.log("comments",this.comments);
}
})
},
addFirst() {
// 模拟数据提交成功后返回数据
let data = {
mainId: '',
parentId: '',
sourceType: this.sourceType,
sourceId: this.$route.query.id,
commentContent: this.context //评论内容
};
this.$post("/api/comment",data).then(async(res) => {
if(res.code == 200) {
this.params.mainId = '';
let res = await this.getNewList();
res.rows.map(item => {
this.$set(item,'publicTotal',false)
})
this.comments = res.rows;
this.context = "";
this.total = res.total;
if(res.total > 5) {
this.morelist.value = true;
}
}
})
},
}
};
</script>
<style lang="scss" scoped>
.comment {
min-height: 26vh;
border-radius: 5px;
margin-top: 2px;
overflow: hidden;
h3 {
margin: 5px 0;
}
p {
margin: 3px 0;
}
ul {
list-style-type: none;
}
.img_width {
max-width: 50px;
max-height: 50px;
border-radius: 50px;
font-size: 30px;
color: #3339;
}
.flex_box {
padding: 0 5px;
display: flex;
align-items: center;
margin-bottom: 30px;
h2 {
margin: 0;
}
p {
font-size: 16px;
color: #666;
margin: 0 0 0 20px;
}
}
.active {
color: rgb(202, 4, 4);
}
.comment-header {
position: relative;
// height: 50px;
padding: 10px 5px;
// display: flex;
// align-items: center;
.header_children {
display: flex;
align-items: center;
}
.header_btn {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px 0 60px;
margin-top: 10px;
}
.input {
margin-left: 10px;
margin-right: 20px;
flex: 1;
font-size: 14px;
::v-deep .el-input__inner:focus {
border-color: #dcdfe6;
}
}
}
.comment-body {
font-size: 14px;
.first-comment {
display: flex;
padding: 10px 20px;
.input {
::v-deep.el-input__inner:focus {
border-color: #dcdfe6;
}
}
i {
margin-right: 5px;
margin-left: 1vw;
cursor: pointer;
// &:nth-child(3) {
// color: rgb(202, 4, 4);
// }
}
.content {
margin-left: 10px;
position: relative;
flex: 1;
& > span {
font-size: 12px;
color: rgb(130, 129, 129);
}
.comment-right {
position: absolute;
right: 0;
top: 0;
}
.reply-comment {
// height: 60px;
// display: flex;
// align-items: center;
.flex_one_box {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.reply-button {
margin-left: 20px;
height: 35px;
}
}
.li_top {
width: 200px;
cursor: pointer;
padding-left: 50px;
}
.infinite_list {
max-height: 333px;
}
.second-comment {
display: flex;
padding: 10px 0 10px 5px;
border-radius: 20px;
background: #ffffff;
.to_reply {
// color: rgb(126, 127, 128);
color: #0d74e1;
}
}
.loading_p {
cursor: pointer;
margin: 3px auto;
display: flex;
justify-content: center;
}
}
}
}
.moreBtn {
width: 100%;
i {
font-size: 16px;
}
}
.more_div {
display: flex;
display: flex;
justify-content: center;
color: #66666694;
font-size: 14px;
}
::-webkit-scrollbar
{
width: 8px;
height: 5px;
background-color: #F5F5F5;
}
/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track
{
-webkit-box-shadow: inset 0 0 6px #c0c0c0;
border-radius: 10px;
background-color: #f9f9f9;
}
/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb
{
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px #c0c0c0;
background-color: #c0c0c0;
}
}
</style>
表情组件如下:
1. 创建 VueEmoji.vue 文件。
<template>
<div class="emoji">
<ul class="emoji-default" v-show="isShowEmoji">
<li v-for="(item, index) in emojiJson" :key="index"
@click.stop="chooseEmojiDefault(item)">
{{item}}
</li>
</ul>
<div class="emoji-tabs">
<div :class="!isShowEmoji ? 'emoji-tab' : 'new_tab'" @click="isShowEmoji = !isShowEmoji">
{{ isShowEmoji ? '收起' : emojiJson[4]}}
</div>
</div>
</div>
</template>
<script>
const emojiJson = require("../utils/emoji.json");
export default {
props: {
keyId: {
type: String,
required: false
},
layer: {
type: Array || Object,
required: false
}
},
data() {
return {
emojiJson: emojiJson.data.split(','),
isShowEmoji: false,
}
},
methods: {
chooseEmojiDefault(item) {
console.log("item",item);
this.$emit("chooseEmoji",this.keyId,this.layer,item);
this.isShowEmoji = false;
}
}
}
</script>
<style lang="scss" scoped>
// 纵向滚动条
@mixin scroll-bar($width: 10px) {
&::-webkit-scrollbar-track {
border-radius: 10px;
background-color: #ffffff;
}
&::-webkit-scrollbar {
width: $width;
height: 10px;
background-color: #ffffff;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: rgba(0, 0, 0, 0.2);
}
}
.emoji {
text-align: left;
// width: 100%;
// height: 100%;
background: #fff;
// border: 1px solid #dcdfe6;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 12%), 0 0 6px 0 rgb(0 0 0 / 4%);
.emoji-tabs {
// border-top: 1px solid #DCDFE6;
background: #f8f8f8;
.emoji-tab {
cursor: pointer;
background: #ffffff;
/* height: 100%; */
/* padding: 5px 5px; */
/* overflow: hidden; */
text-align: center;
/* line-height: 15px; */
font-size: 25px
}
.new_tab {
cursor: pointer;
font-size: 14px;
color: #1c33e5;
text-align: center;
padding: 5px 0;
}
}
.emoji-default {
width: calc(100% - 40px);
height: 202px;
overflow-y: auto;
@include scroll-bar();
padding: 0px 20px;
li {
display: inline-block;
padding: 5px;
font-size: 24px;
width: 29px;
height: 29px;
overflow: hidden;
cursor: pointer;
}
li:hover {
background-color: #d5d5d5;
}
}
}
</style>
2. 在写公共方法的地方创建 emoji.json 文件,里面是表情的数据,内容如下:
{
"data": "😀,😁,😂,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,😇,😐,😑,😶,😏,😣,😥,😮,😯,😪,😫,😴,😌,😛,😜,😝,😒,😓,😔,😕,😲,😷,😖,😞,😟,😤,😢,😭,😦,😧,😨,😬,😰,😱,😳,😵,😡,😠,💘,❤,💓,💔,💕,💖,💗,💙,💚,💛,💜,💝,💞,💟,❣,💪,👈,👉,☝,👆,👇,✌,✋,👌,👍,👎,✊,👊,👋,👏,👐,✍,🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🍅,🍆,🌽,🍄,🌰,🍞,🍖,🍗,🍔,🍟,🍕,🍳,🍲,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🍫,🍬,🍭,🍮,🍯,🍼,☕,🍵,🍶,🍷,🍸,🍹,🍺,🍻,🍴,🌹,🍀,🍎,💰,📱,🌙,🍁,🍂,🍃,🌷,💎,🔪,🔫,🏀,⚽,⚡,👄,👍,🔥,🙈,🙉,🙊,🐵,🐒,🐶,🐕,🐩,🐺,🐱,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🐈,🐯,🐅,🐆,🐴,🐎,🐮,🐂,🐃,🐄,🐷,🐖,🐗,🐽,🐏,🐑,🐐,🐪,🐫,🐘,🐭,🐁,🐀,🐹,🐰,🐇,🐻,🐨,🐼,🐾,🐔,🐓,🐣,🐤,🐥,🐦,🐧,🐸,🐊,🐢,🐍,🐲,🐉,🐳,🐋,🐬,🐟,🐠,🐡,🐙,🐚,🐌,🐛,🐜,🐝,🐞,🦋,😈,👿,👹,👺,💀,☠,👻,👽,👾,💣"
}
在 Vue 文件中使用,如下:
<template>
<div>
<comment :buttonText="'发表评论'"/>
</div>
</template>