vue.js实现带表情评论功能前后端实现(仿B站评论)
实现在vue项目中通过滚动条来滑动加载数据
IntersectionObserver与无限滚动加载
每次加载2条数据
要实现滚动加载,就是当滚动条滚动到底部的时候,再去请求后端评论数据,然后把数据添加到响应式数据数组中,然后由vue更新dom。
因此,我们需要能有方式能够监测的到滚动条是否滚动到了底部,一般有两种方式:
可以通过 网页被卷去的头部高度 scrollTop 加上 浏览器客户端的高度 clientHeight 和 整个网页的高度 scrollHeight 做比较, 如果scrollTop + clientHeight == scrollHeight,
那么就可以说明滚动条已经到底了。
为了避免误差, 可能还需要给scrollHeight预留一个比较小的范围。
比如 scrollHeight - (scrollTop + clientHeight) <= 10, 这也就是说网页高度在仅剩10像素还在屏幕下面时的条件。
IntersectionObserver
这个浏览器提供的api, 在评论的下面,添加一个显示正在加载的div,当这个div出现在屏幕中时,此时需要请求后台数据,这个IntersectionObserver浏览器的api还不太熟悉,如果加载两条之后,那个div如果还在页面,没有被挤到屏幕外面,还会回调观察函数吗?
在发起请求前,我把isLoading置为了true,它如果为true的话,这个正在加载中的div的display就会为none,也就相当于它不被看见,然后,请求完了之后,我又把它置为了false,它又能被看见了,这就意味着,这个过程它从没被看见到看见又是一个变化,那么有可能这个变化也能被浏览器的IntersectionObserver这个api所监测到,所以很快速的引发的一连串的请求
。好像误打误撞的无意中解决了这个问题,因为,对于这个问题,我觉得如果div还在页面的话,那就不算它又进入了,也就不会触发观察函数。就好像给了一个元素动画,但是这个元素是display:none,但是当这个元素不是display:none时,这个元素会立即有个动画。当元素在视口范围内,对display:none进行切换时,它也会触发观察函数
。所以这个isIntersecting就是看有没有超过指定的阈值,超过了就是true,没超过就是false
。当前滚动在第二页时,我发表了一个一级评论,
<style lang="scss">
/* 封面图下移效果 */
@keyframes slidedown {
0% {
opacity: 0.3;
transform: translateY(-60px);
}
100% {
opacity: 1;
transform: translateY(0px);
}
}
.slidedown {
animation: slidedown 1s;
}
/* 内容上移效果 */
@keyframes slideup {
0% {
opacity: 0.3;
transform: translateY(60px);
}
100% {
opacity: 1;
transform: translateY(0px);
}
}
.slideup {
animation: slideup 1s;
}
.banner {
height: 400px;
background-image: url(@/assets/bg5.jpg);
background-size: cover;
background-position: center;
position: relative;
color: #eee;
.banner-content {
position: absolute;
bottom: 25%;
width: 100%;
text-align: center;
text-shadow: 0.05rem 0.05rem 0.1rem rgb(0 0 0 / 30%);
height: 108px;
font-size: 30px;
letter-spacing: 0.3em;
}
}
textarea {
outline: none;
border: none;
background: #f1f2f3;
resize: none;
border-radius: 8px;
padding: 10px 10px;
font-size: 16px;
color: #333333;
}
.height80 {
height: 80px !important;
}
.comment-wrapper {
// border: 1px solid red;
max-width: 1000px;
margin: 40px auto;
background: #fff;
padding: 40px 30px;
border-radius: 10px;
color: #90949e;
.comment-header {
font-size: 20px;
font-weight: bold;
color: #333333;
padding: 0 20px;
margin-bottom: 20px;
display: flex;
align-items: center;
i {
color: #90949e;
margin-right: 5px;
font-size: 20px;
}
}
.loading-area {
.loading-effect {
height: 50px;
// border: 1px solid red;
text-align: center;
&>div {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.loading-animation {
position: relative;
}
}
.bottom-line {
height: 40px;
text-align: center;
position: relative;
display: flex;
align-items: center;
justify-content: center;
// border: 1px solid red;
span {
padding: 0 12px;
background-color: #fff;
z-index: 1;
}
&::before {
content: '';
position: absolute;
width: 100%;
border-bottom: 1px dashed #ccc;
top: 20px;
left: 0;
}
}
}
}
style>
<template>
<div>
<navbar />
<div class="banner slidedown">
<div style="position: absolute;top: 0;left: 0;width: 100%;height: 100%;backdrop-filter: blur(5px);">div>
<div class="banner-content">
<div>
评论
div>
div>
div>
<div class="comment-wrapper shadow slideup">
<div class="comment-header">
<i class="iconfont icon-pinglun1">i>
评论
<el-button @click="switchUser(1)">用户id1-zzhua195el-button>
<el-button @click="switchUser(2)">用户id2-lsel-button>
<el-button @click="switchUser(3)">用户id3-zjel-button>
div>
<emoji-text @comment="comment" :emojiSize="20">emoji-text>
<Reply ref="commentReplyRef" @closeOtherCommentBoxExcept="closeOtherCommentBoxExcept" :index="idx"
v-for="(reply, idx) in replyList" :key="idx" :reply="reply" />
<div class="loading-area">
<div class="loading-effect" v-show="hasMore">
<div class="loading-text" v-show="!isLoading" ref="loadingTextRef">
正在加载中{{ 'isLoading=' + isLoading }} - {{ 'hasMore=' + hasMore }}
div>
<div class="loading-animation" v-show="isLoading">
<Loading/>
div>
div>
<div class="bottom-line" v-show="!hasMore">
<span>我也是有底线的span>
div>
div>
div>
div>
template>
<script>
import Talk from '@/components/Talk/Talk'
import Navbar from './Navbar.vue';
import EmojiText from '@/components/EmojiText/EmojiText'
import Reply from '@/components/Reply/Reply'
import Loading from '@/components/Loading/Loading'
import { getCommentListByPage, addComment } from '@/api/commentApi';
export default {
name: 'Comment',
data() {
return {
replyList: [],
pageNum: 0, /* 分页参数, 第几页, 默认第0页 */
pageSize: 2,/* 分页参数, 页大小 */
hasMore: true, /* 是否还有数据可供加载, 默认有数据 */
isLoading: false, /* 是否加载中 */
}
},
mounted() {
/* 加载评论数据 */
/* 首先尝试加载第一页数据 */
// getCommentListByPage({ pageNum: this.pageNum, pageSize: this.pageSize }).then(res => {
// this.replyList = res.list || []
// this.$nextTick(() => {
// if (res.totalCount > res.pageNum * res.pageSize) {
// this.hasMore = true
// let observer = new IntersectionObserver(entries => {
// for (let entry of entries) {
// console.log(entry);
// if (entry.isIntersecting) { // 当正在加载的文字出现的时候, 开始发起请求, 加载数据
// console.log(this, 'this');
// this.pageNum++ // 分页参数 +1
// this.loadCommentListByPage(observer)
// }
// }
// }, { threshold: 0.5 })
// observer.observe(this.$refs['loadingTextRef'])
// } else {
// this.hasMore = false
// }
// /* // 自动滚动到最下面(方便调试使用的代码)
// let scrollTop = document.documentElement.scrollHeight - document.documentElement.clientHeight
// window.scroll({
// top: scrollTop, // top: 表示移动到距离顶部的位置大小
// behavior: 'smooth'
// }) */
// })
// })
/* 优化: 上面是还没滚动到评论下面,就去加载; 改成等到了看评论的时候,再去加载。 */
let observer = new IntersectionObserver(entries => {
for (let entry of entries) {
console.log(entry);
if (entry.isIntersecting) { // 当正在加载的文字出现的时候, 才开始发起请求, 加载数据
console.log(this, 'this');
this.pageNum++ // 分页参数 +1
this.loadCommentListByPage(observer)
}
}
}, { threshold: 0.5 })
observer.observe(this.$refs['loadingTextRef'])
},
methods: {
loadCommentListByPage(observer) {
this.isLoading = true // 显示加载动画
getCommentListByPage({ pageNum: this.pageNum, pageSize: this.pageSize }).then(res => {
this.isLoading = false // 关闭加载动画
this.replyList.splice(this.replyList.length, 0, ...res.list) // 将数据添加到最后面, 由根据修改后的数据(响应式数据), 更新dom
this.$nextTick(() => {
if (res.totalCount > res.pageNum * res.pageSize) { // 证明还有数据, 还可以继续加载
this.hasMore = true
} else {
this.hasMore = false // 当前页已经是最后一页了, 后面没有更多数据了
}
})
})
},
/* 添加评论 */
comment(content) {
addComment({
userId: localStorage.getItem("userId"),
commentContent: content,
}).then(res => {
this.replyList.splice(0, 0, res)
this.$toast('success', '评论成功')
})
},
/* 模拟不同用户 */
switchUser(userId) {
localStorage.setItem("userId", userId)
this.$toast('success', `切换userId ${userId} 成功`)
},
/* 关闭其它一级评论的评论框 */
closeOtherCommentBoxExcept(index) {
/* 根据索引, 关闭其它的输入框, 除了指定的输入框外 */
this.$refs['commentReplyRef'].forEach((commentReplyRef, idx) => {
if (index != idx) {
commentReplyRef.hideCommentBox()
}
})
}
},
watch: {
},
components: {
Talk,
Navbar,
EmojiText,
Reply,
Loading
}
}
script>
<template>
<div class="loader">div>
template>
<script>
export default {
name: 'Loading',
components: {
}
}
script>
<style lang="scss">
$colors:
hsla(337, 84, 48, 0.75)
hsla(160, 50, 48, 0.75)
hsla(190, 61, 65, 0.75)
hsla( 41, 82, 52, 0.75);
$size: 2.5em;
$thickness: 0.5em;
// Calculated variables.
$lat: ($size - $thickness) / 2;
$offset: $lat - $thickness;
.loader {
position: relative;
width: $size;
height: $size;
transform: rotate(165deg);
&:before,
&:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
display: block;
width: $thickness;
height: $thickness;
border-radius: $thickness / 2;
transform: translate(-50%, -50%);
}
&:before {
animation: before 2s infinite;
}
&:after {
animation: after 2s infinite;
}
}
@keyframes before {
0% {
width: $thickness;
box-shadow:
$lat (-$offset) nth($colors, 1),
(-$lat) $offset nth($colors, 3);
}
35% {
width: $size;
box-shadow:
0 (-$offset) nth($colors, 1),
0 $offset nth($colors, 3);
}
70% {
width: $thickness;
box-shadow:
(-$lat) (-$offset) nth($colors, 1),
$lat $offset nth($colors, 3);
}
100% {
box-shadow:
$lat (-$offset) nth($colors, 1),
(-$lat) $offset nth($colors, 3);
}
}
@keyframes after {
0% {
height: $thickness;
box-shadow:
$offset $lat nth($colors, 2),
(-$offset) (-$lat) nth($colors, 4);
}
35% {
height: $size;
box-shadow:
$offset 0 nth($colors, 2),
(-$offset) 0 nth($colors, 4);
}
70% {
height: $thickness;
box-shadow:
$offset (-$lat) nth($colors, 2),
(-$offset) $lat nth($colors, 4);
}
100% {
box-shadow:
$offset $lat nth($colors, 2),
(-$offset) (-$lat) nth($colors, 4);
}
}
/**
* Attempt to center the whole thing!
*/
html,
body {
height: 100%;
}
.loader {
position: absolute;
top: calc(50% - #{$size / 2});
left: calc(50% - #{$size / 2});
}
style>