简易贴吧评论 (Vue)
JarryChen 发布时间:2020-12-12 文章字数:2265 预计用时:11分20秒
- 项目开发过程中,遇到一个内容是给文章评论,原有的评论样式一个是散乱,还有一个是不够直观清楚地看到互相回复的;所以在此基础上,参考了百度贴吧的样式,重新制作了一个简易版的贴吧评论。
- UI方面仅分页是使用
ant design vue
的组件,其余都是原生写的。
# 1. 优先处理分页的位置
- 分页部分代码如下:
<!-- 因为分页允许切换个数,且是不设置高度(不要滚动)的情况下,
如果内容太多,返回到顶部也难定位,
故在列表的上下各做了一个分页,上面的右对齐,下面的左对齐 -->
<template>
<div class="comment-component">
<a-pagination
:total="total"
:show-total="total => `${total} 条回复贴`"
showQuickJumper
showSizeChanger
hideOnSinglePage
v-model="pagination.pageNo"
@change="ListPageChange"
@showSizeChange="ListSizeChange"
:pageSize.sync="pagination.pageSize"
class="diy-pagination"
/>
<ul class="comment-list-data">
<!-- 评论列表内容 -->
</ul>
<a-pagination
:total="total"
:show-total="total => `${total} 条回复贴`"
showQuickJumper
showSizeChanger
hideOnSinglePage
v-model="pagination.pageNo"
:pageSize.sync="pagination.pageSize"
@change="ListPageChange"
@showSizeChange="ListSizeChange"
class="diy-pagination-bottom"
/>
</div>
</template>
<script>
/* eslint-disable */
import Editor from '@/xxx/CKEditor'
import $ from 'jquery'
export default {
name: 'DIYComment',
components: {
Editor
},
props: {
/* 富文本需要的上传url */
CKEditorUrl: {
type: String,
default: ''
},
/* 是否需要子评论面板 */
needSubPanel: {
type: Boolean,
default: false
},
/* 拉取父评论列表接口,必须是promise */
getCommentList: {
type: Function,
required: true
},
/* 拉取子评论列表接口,必须是promise */
getSubCommentList: {
type: Function
},
/* 必备字段的替换obj */
replaceFields: {
type: Object,
default: () => {}
}
},
data() {
return {
pagination: {
pageNo: 1,
pageSize: 10
},
total: 0,
commentListData: []
}
},
computed: {
/*
...
*/
diyReplace() {
return Object.assign(
{},
{
id: 'id',
content: 'comment',
creator: 'creator',
time: 'createdDate'
},
this.replaceFields
)
}
},
created() {
/* 组件创建即初始化父列表 */
this.loadCommentList()
},
methods: {
/*
...
*/
// 父评论页数大小改变
ListSizeChange(current, size) {
Object.assign(this.pagination, {
pageNo: 1,
pageSize: size
})
this.loadCommentList()
},
// 父评论页数切换
ListPageChange(pageNo, pageSize) {
Object.assign(this.pagination, {
pageNo,
pageSize
})
this.loadCommentList()
},
// 评论列表获取
loadCommentList() {
const result = this.getCommentList(this.pagination)
if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result.then(r => {
if (this.total !== r.totalCount) {
this.total = r.totalCount
}
/* 如果没有数据了,就走前一页 */
if (Array.isArray(r.data) && !r.data.length) {
this.pagination.pageNo = Math.max(this.pagination.pageNo - 1, 1)
this.loadCommentList()
}
/* 是数组了再走后面的处理 */
Array.isArray(r?.data) &&
(this.commentListData = r.data.map(item => {
const datetime =
typeof item[this.diyReplace.time] === 'string'
? item[this.diyReplace.time].slice(0, item[this.diyReplace.time].lastIndexOf('.'))
: item[this.diyReplace.time]
return {
id: item[this.diyReplace.id],
content: item[this.diyReplace.content],
showReply: false,
author: Number(item[this.diyReplace.creator]),
datetime,
reply: false,
comment: '',
isAnonymous: 0,
pagination: {
pageNo: 1,
pageSize: 10
},
total: 0,
children: [],
anonymous: item.isAnonymous
}
}))
})
}
},
// 列表刷新,提供给外部组件ref进行调用
refresh(bool = true) {
bool &&
(this.pagination = Object.assign(
{},
{
pageNo: 1,
pageSize: this.pagination.pageSize
}
))
this.loadCommentList()
}
/*
...
*/
}
}
</script>
<style lang="less" src="./style.less" />
# 2. 列表内容的安排
- 关键部分代码如下:
<!-- eslint-disable -->
<template>
<div class="comment-component">
<!-- 上分页 -->
<ul class="comment-list-data">
<li v-for="(item, index) in commentListData" :key="item.datetime" class="list-item">
<div class="left-icon">
<!-- 因为没有头像,取回复人的名字当头像 -->
<p class="author-avatar">
<span>{{ renameAuthor(item) }}</span>
</p>
<!-- 取回复人名字 -->
<p class="author-name">
<span>{{ item.anonymous ? '匿名' : $filterUserText(item.author) }}</span>
</p>
</div>
<div class="comment-content">
<!-- 回复内容 -->
<p v-html="removeInvalid(item.content)" class="main-content" />
<!-- 第几楼,回复时间,回复按钮,删除按钮 -->
<p class="tips">
<span>{{ (pagination.pageNo - 1) * pagination.pageSize + index + 1 }}楼</span>
<span>{{ item.datetime }} </span>
<span
class="reply"
v-if="needSubPanel"
@click="
() => {
item.showReply = !item.showReply
loadCommentSubList(item)
}
"
:class="item.showReply ? 'show' : 'hide'"
>
{{ item.showReply ? '收起' : '' }}回复{{ item.total || '' }}
</span>
<span class="reply" v-if="userId === item.author" @click="deleteComment(item.id)"> 删除 </span>
</p>
<!-- 子评论列表面板 -->
<!-- 来一个动画效果 -->
<transition name="slide-fade-editor">
<div class="reply-list" v-if="item.showReply && needSubPanel">
<template v-if="item.children.length">
<div v-for="subcomment in item.children" :key="subcomment.id" class="reply-item">
<!-- 特殊处理了谁回复了谁 -->
<p class="main-info">
<a href="javascript:void(0)"
><a-icon type="user" />{{
subcomment.isAnonymous ? '匿名' : $filterUserText(subcomment.author)
}}</a
>
<span style="padding-left: 5px" v-if="isReply(subcomment.content)">回复</span>
<a href="javascript:void(0)" v-if="getReplyAuthor(subcomment.content)"
><a-icon type="user" />{{ getReplyAuthor(subcomment.content) }}</a
>
:<span v-html="removeSubContentInvalid(subcomment.content)" />
</p>
<!-- 删除或者是回复这条评论 -->
<p class="bottom-info">
<span class="delete" @click="deleteComment(subcomment.id)" v-if="userId === subcomment.author"
>删除 |</span
>
<span>{{ subcomment.datetime }}</span>
<span class="reply" @click="SubCommentReply(item, subcomment)">回复</span>
</p>
</div>
</template>
<template v-else>
<p style="text-align: center; color: #aaa">———— 暂无回复 ————</p>
</template>
<p class="reply-button">
<!-- 子评论简易翻页器 -->
<a-pagination
simple
size="small"
hideOnSinglePage
@change="(pageNo, pageSize) => SubListPageChange(item, pageNo, pageSize)"
:total="item.total"
:pageSize.sync="item.pagination.pageSize"
v-model="item.pagination.pageNo"
/>
<!-- 子评论里评论 -->
<a-button
size="small"
@click="
() => {
item.reply = !item.reply
if (!item.reply) {
item.comment = ''
}
}
"
>我也说一句</a-button
>
</p>
<!-- 显示富文本编辑器和是否匿名 -->
<transition name="slide-fade">
<div id="button-reply">
<editor
v-if="item.reply && item.showReply"
key="editor"
v-model="item.comment"
:uploadUrl="CKEditorUrl"
/>
<p class="reply-by-api" v-if="item.reply && item.showReply" key="button-reply">
<span
>是否匿名:
<a-radio-group name="radioGroup" v-model="item.isAnonymous">
<a-radio :value="0">否</a-radio>
<a-radio :value="1">是</a-radio>
</a-radio-group>
</span>
<a-button size="small" type="primary" @click="SubCommentCommit(item)">发表</a-button>
</p>
</div>
</transition>
</div>
</transition>
</div>
</li>
</ul>
<!-- 下分页 -->
</div>
</template>
<script>
/* eslint-disable */
/* 省略引入部分 */
export default {
name: 'DIYComment',
components: {
Editor
},
/* 省略 props部分 */
data() {
return {
pagination: {
pageNo: 1,
pageSize: 10
},
total: 0,
commentListData: []
}
},
computed: {
userId() {
return this.$getCurrentUser()?.id
},
/* 省略diyReplace */
},
created() {
/* 调用列表刷新 */
},
methods: {
// 判断是否有回复
isReply(content) {
const newContent = this.removeInvalid(content)
return newContent.indexOf('回复') === newContent.indexOf('>') + 1
},
// 过滤非法的评论(这里只是个粗略版本)
removeInvalid(content) {
return content.includes('<title>') ? content.replace(/<[^<>]+>/g, '') : content
},
// 取出回复子评论作者
getReplyAuthor(content) {
const newContent = content.replace(/<[^<>]+>/g, '')
if (newContent.includes(':') && newContent.indexOf('回复') === 0) {
return newContent.slice(2, newContent.indexOf(':')).trim()
}
return null
},
// 子评论内容处理
removeSubContentInvalid(content) {
let newContent = this.removeInvalid(content)
if (newContent.includes(':') && this.isReply(newContent)) {
newContent = newContent.slice(newContent.indexOf(':') + 1)
}
return newContent
},
// 处理作者的名字
renameAuthor({ author, anonymous }) {
if (anonymous) {
return '匿名'
} else {
const name = this.$filterUserText(author)
return name.length >= 2 ? name.slice(name.length - 2) : '匿名'
}
},
// 子评论里回复
SubCommentReply(parent, { author, isAnonymous }) {
parent.reply = true
this.$nextTick(() => {
$('html, body').animate({ scrollTop: $('#button-reply').offset().top }, 800)
parent.comment = `回复 ${isAnonymous ? '匿名' : this.$filterUserText(author)}:`
})
},
/* 省略父评论列表翻页方法 */
// 子评论翻页
SubListPageChange(item, pageNo, pageSize) {
Object.assign(item.pagination, {
pageNo,
pageSize
})
this.loadCommentSubList(item)
},
// 子评论获取列表
loadCommentSubList(item) {
let result
if (typeof this.getSubCommentList === 'function') {
result = this.getSubCommentList(item)
}
if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result.then(r => {
console.log(r)
if (item.total !== r.totalCount) {
item.total = r.totalCount
}
if (Array.isArray(r.data) && !r.data.length) {
typeof item?.pagination?.pageNo === 'number' &&
(item.pagination.pageNo = Math.max(item.pagination.pageNo - 1, 1))
this.loadCommentSubList(item)
}
Array.isArray(r?.data) &&
(item.children = r.data.map(item => {
const datetime =
typeof item[this.diyReplace.time] === 'string'
? item[this.diyReplace.time].slice(0, item[this.diyReplace.time].lastIndexOf('.'))
: item[this.diyReplace.time]
return {
id: item[this.diyReplace.id],
content: item[this.diyReplace.content],
author: Number(item[this.diyReplace.creator]),
datetime,
isAnonymous: item.isAnonymous
}
}))
})
}
},
/* 省略拉取父评论列表方法 */
// 评论删除
deleteComment(id) {
this.$emit('deletecomment', id)
},
// 子评论提交
SubCommentCommit(item) {
const param = {
comment: item.comment,
isAnonymous: item.isAnonymous,
parentCommentId: item.id
}
const callback = (param, item) => {
item.comment = ''
item.isAnonymous = 0
this.loadCommentSubList(item)
}
this.$emit('savecomment', param, 'sub', item, callback)
},
/* 省略刷新方法 */
}
}
</script>
# 3. 整合内容及分页
- 组件vue页面代码如下:
<!-- eslint-disable -->
<template>
<div class="comment-component">
<a-pagination
:total="total"
:show-total="total => `${total} 条回复贴`"
showQuickJumper
showSizeChanger
hideOnSinglePage
v-model="pagination.pageNo"
@change="ListPageChange"
@showSizeChange="ListSizeChange"
:pageSize.sync="pagination.pageSize"
class="diy-pagination"
/>
<ul class="comment-list-data">
<li v-for="(item, index) in commentListData" :key="item.datetime" class="list-item">
<div class="left-icon">
<p class="author-avatar">
<span>{{ renameAuthor(item) }}</span>
</p>
<p class="author-name">
<span>{{ item.anonymous ? '匿名' : $filterUserText(item.author) }}</span>
</p>
</div>
<div class="comment-content">
<p v-html="removeInvalid(item.content)" class="main-content" />
<p class="tips">
<span>{{ (pagination.pageNo - 1) * pagination.pageSize + index + 1 }}楼</span>
<span>{{ item.datetime }} </span>
<span
class="reply"
v-if="needSubPanel"
@click="
() => {
item.showReply = !item.showReply
loadCommentSubList(item)
}
"
:class="item.showReply ? 'show' : 'hide'"
>
{{ item.showReply ? '收起' : '' }}回复{{ item.total || '' }}
</span>
<span class="reply" v-if="userId === item.author" @click="deleteComment(item.id)"> 删除 </span>
</p>
<transition name="slide-fade-editor">
<div class="reply-list" v-if="item.showReply && needSubPanel">
<template v-if="item.children.length">
<div v-for="subcomment in item.children" :key="subcomment.id" class="reply-item">
<p class="main-info">
<a href="javascript:void(0)"
><a-icon type="user" />{{
subcomment.isAnonymous ? '匿名' : $filterUserText(subcomment.author)
}}</a
>
<span style="padding-left: 5px" v-if="isReply(subcomment.content)">回复</span>
<a href="javascript:void(0)" v-if="getReplyAuthor(subcomment.content)"
><a-icon type="user" />{{ getReplyAuthor(subcomment.content) }}</a
>
:<span v-html="removeSubContentInvalid(subcomment.content)" />
</p>
<p class="bottom-info">
<span class="delete" @click="deleteComment(subcomment.id)" v-if="userId === subcomment.author"
>删除 |</span
>
<span>{{ subcomment.datetime }}</span>
<span class="reply" @click="SubCommentReply(item, subcomment)">回复</span>
</p>
</div>
</template>
<template v-else>
<p style="text-align: center; color: #aaa">———— 暂无回复 ————</p>
</template>
<p class="reply-button">
<a-pagination
simple
size="small"
hideOnSinglePage
@change="(pageNo, pageSize) => SubListPageChange(item, pageNo, pageSize)"
:total="item.total"
:pageSize.sync="item.pagination.pageSize"
v-model="item.pagination.pageNo"
/>
<a-button
size="small"
@click="
() => {
item.reply = !item.reply
if (!item.reply) {
item.comment = ''
}
}
"
>我也说一句</a-button
>
</p>
<transition name="slide-fade">
<div id="button-reply">
<editor
v-if="item.reply && item.showReply"
key="editor"
v-model="item.comment"
:uploadUrl="CKEditorUrl"
/>
<p class="reply-by-api" v-if="item.reply && item.showReply" key="button-reply">
<span
>是否匿名:
<a-radio-group name="radioGroup" v-model="item.isAnonymous">
<a-radio :value="0">否</a-radio>
<a-radio :value="1">是</a-radio>
</a-radio-group>
</span>
<a-button size="small" type="primary" @click="SubCommentCommit(item)">发表</a-button>
</p>
</div>
</transition>
</div>
</transition>
</div>
</li>
</ul>
<a-pagination
:total="total"
:show-total="total => `${total} 条回复贴`"
showQuickJumper
showSizeChanger
hideOnSinglePage
v-model="pagination.pageNo"
:pageSize.sync="pagination.pageSize"
@change="ListPageChange"
@showSizeChange="ListSizeChange"
class="diy-pagination-bottom"
/>
</div>
</template>
<script>
/* eslint-disable */
import Editor from '/xxx/CKEditor'
import $ from 'jquery'
export default {
name: 'DIYComment',
components: {
Editor
},
props: {
CKEditorUrl: {
type: String,
default: ''
},
needSubPanel: {
type: Boolean,
default: false
},
getCommentList: {
type: Function,
required: true
},
getSubCommentList: {
type: Function
},
replaceFields: {
type: Object,
default: () => {}
}
},
data() {
return {
pagination: {
pageNo: 1,
pageSize: 10
},
total: 0,
commentListData: []
}
},
computed: {
userId() {
return this.$getCurrentUser()?.id
},
diyReplace() {
return Object.assign(
{},
{
id: 'id',
content: 'comment',
creator: 'creator',
time: 'createdDate'
},
this.replaceFields
)
}
},
created() {
this.loadCommentList()
},
methods: {
// 判断是否有回复
isReply(content) {
const newContent = this.removeInvalid(content)
return newContent.indexOf('回复') === newContent.indexOf('>') + 1
},
// 过滤非法的评论
removeInvalid(content) {
return content.includes('<title>') ? content.replace(/<[^<>]+>/g, '') : content
},
// 取出回复子评论作者
getReplyAuthor(content) {
const newContent = content.replace(/<[^<>]+>/g, '')
if (newContent.includes(':') && newContent.indexOf('回复') === 0) {
return newContent.slice(2, newContent.indexOf(':')).trim()
}
return null
},
// 子评论内容处理
removeSubContentInvalid(content) {
let newContent = this.removeInvalid(content)
if (newContent.includes(':') && this.isReply(newContent)) {
newContent = newContent.slice(newContent.indexOf(':') + 1)
}
return newContent
},
// 处理作者的名字
renameAuthor({ author, anonymous }) {
if (anonymous) {
return '匿名'
} else {
const name = this.$filterUserText(author)
return name.length >= 2 ? name.slice(name.length - 2) : '匿名'
}
},
// 子评论里回复
SubCommentReply(parent, { author, isAnonymous }) {
parent.reply = true
this.$nextTick(() => {
$('html, body').animate({ scrollTop: $('#button-reply').offset().top }, 800)
parent.comment = `回复 ${isAnonymous ? '匿名' : this.$filterUserText(author)}:`
})
},
// 父评论页数大小改变
ListSizeChange(current, size) {
Object.assign(this.pagination, {
pageNo: 1,
pageSize: size
})
this.loadCommentList()
},
// 父评论页数切换
ListPageChange(pageNo, pageSize) {
Object.assign(this.pagination, {
pageNo,
pageSize
})
this.loadCommentList()
},
// 子评论翻页
SubListPageChange(item, pageNo, pageSize) {
Object.assign(item.pagination, {
pageNo,
pageSize
})
this.loadCommentSubList(item)
},
// 子评论获取列表
loadCommentSubList(item) {
let result
if (typeof this.getSubCommentList === 'function') {
result = this.getSubCommentList(item)
}
if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result.then(r => {
console.log(r)
if (item.total !== r.totalCount) {
item.total = r.totalCount
}
if (Array.isArray(r.data) && !r.data.length) {
typeof item?.pagination?.pageNo === 'number' &&
(item.pagination.pageNo = Math.max(item.pagination.pageNo - 1, 1))
this.loadCommentSubList(item)
}
Array.isArray(r?.data) &&
(item.children = r.data.map(item => {
const datetime =
typeof item[this.diyReplace.time] === 'string'
? item[this.diyReplace.time].slice(0, item[this.diyReplace.time].lastIndexOf('.'))
: item[this.diyReplace.time]
return {
id: item[this.diyReplace.id],
content: item[this.diyReplace.content],
author: Number(item[this.diyReplace.creator]),
datetime,
isAnonymous: item.isAnonymous
}
}))
})
}
},
// 评论列表获取
loadCommentList() {
const result = this.getCommentList(this.pagination)
if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
result.then(r => {
if (this.total !== r.totalCount) {
this.total = r.totalCount
}
if (Array.isArray(r.data) && !r.data.length) {
this.pagination.pageNo = Math.max(this.pagination.pageNo - 1, 1)
this.loadCommentList()
}
Array.isArray(r?.data) &&
(this.commentListData = r.data.map(item => {
const datetime =
typeof item[this.diyReplace.time] === 'string'
? item[this.diyReplace.time].slice(0, item[this.diyReplace.time].lastIndexOf('.'))
: item[this.diyReplace.time]
return {
id: item[this.diyReplace.id],
content: item[this.diyReplace.content],
showReply: false,
author: Number(item[this.diyReplace.creator]),
datetime,
reply: false,
comment: '',
isAnonymous: 0,
pagination: {
pageNo: 1,
pageSize: 10
},
total: 0,
children: [],
anonymous: item.isAnonymous
}
}))
})
}
},
// 评论删除
deleteComment(id) {
this.$emit('deletecomment', id)
},
// 子评论提交
SubCommentCommit(item) {
const param = {
comment: item.comment,
isAnonymous: item.isAnonymous,
parentCommentId: item.id
}
const callback = (param, item) => {
item.comment = ''
item.isAnonymous = 0
this.loadCommentSubList(item)
}
this.$emit('savecomment', param, 'sub', item, callback)
},
// 列表刷新
refresh(bool = true) {
bool &&
(this.pagination = Object.assign(
{},
{
pageNo: 1,
pageSize: this.pagination.pageSize
}
))
this.loadCommentList()
}
}
}
</script>
<style lang="less" src="./style.less" />
# 4. 评论样式
- less代码如下:
.comment-component {
.comment-list-data {
padding : 0;
margin : 20px auto;
overflow: hidden auto;
.list-item {
display: flex;
border : solid 1px #d9d9d9;
&:not(:last-child) {
border-bottom: none;
}
&:last-child {
border-bottom: solid 1px #d9d9d9;
}
.left-icon {
padding: 30px;
.author-avatar {
background-color: lighten(#1890ff, 10%);
border-radius : 50%;
font-size : 26px;
color : #fff;
span {
color: #fff;
}
padding : 20px 15px;
}
.author-name {
color : #2d64b3;
cursor : pointer;
text-align: center;
margin-top: -15px;
}
}
.comment-content {
flex : 1;
padding : 20px 10px 0;
position : relative;
display : flex;
flex-direction: column;
img {
max-width: 100%;
}
.main-content {
flex: 1;
}
.tips {
text-align : right;
line-height: 30px;
height : 30px;
span {
padding: 5px 6px;
}
.reply {
color : #2d64b3;
cursor: pointer;
&.show {
background-color: #f7f8fa;
border : solid 1px #f0f1f2;
padding : 5px 10px;
}
&.hide {
background-color: transparent;
border : none;
}
}
}
.reply-list {
background-color: #f7f8fa;
margin-top : -20px;
padding : 20px;
.reply-item {
border-bottom: solid 1px #d9d9d9;
padding : 10px 0 0;
&:hover {
.bottom-info .delete {
opacity: 1;
}
}
.main-info {
display: flex;
a {
cursor: default;
}
.anticon {
font-size: 16px;
padding : 0 4px;
}
}
.bottom-info {
margin-top: -30px;
text-align: right;
.reply,
.delete {
cursor: pointer;
}
.delete {
opacity : 0;
color : #2d64b3;
transition: all 0.3s;
}
span {
padding: 0 4px;
}
}
}
.reply-button {
margin : 15px 0;
display: flex;
.ant-pagination {
flex: 1;
}
}
.cke_contents {
height : auto !important;
min-height: 100px;
}
.reply-by-api {
display: flex;
margin : 10px auto 0;
span {
flex: 1;
}
.ant-btn {
text-align: right;
}
}
}
}
}
}
.diy-pagination,
.diy-pagination-bottom {
text-align: right;
.ant-pagination-item-link {
border : none;
background-color: transparent;
}
.ant-pagination-next,
.ant-pagination-prev {
min-width: 0;
width : fit-content;
padding : 0 5px;
margin : 0;
}
.ant-pagination-item {
background-color: transparent;
border : none;
min-width : 0;
width : fit-content;
padding : 0 5px;
margin : 0;
}
}
.diy-pagination-bottom {
text-align: left;
margin : 20px 0;
}
}
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .5s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter,
.slide-fade-leave-to {
transform: translateY(10px);
opacity : 0;
}
.slide-fade-editor-enter-active {
transition: all .3s ease;
}
.slide-fade-editor-leave-active {
transition: all .1s;
}
.slide-fade-editor-enter,
.slide-fade-editor-leave-to {
transform: translateY(10px);
opacity : 0;
}
# 5. 运行结果
- 运行结果如下图:
结语
以上只是针对原有评论的一部分改进(需要后端的配合),本网站是静态页面,故评论还是采取畅言的api,如有其它更好的实现方式,也欢迎各位评论区留言指教