vue.js实现带表情评论功能前后端实现 (滚动加载效果)

news2025/1/19 6:51:10

学习链接

vue.js实现带表情评论功能前后端实现(仿B站评论)
实现在vue项目中通过滚动条来滑动加载数据
IntersectionObserver与无限滚动加载

效果图

每次加载2条数据

在这里插入图片描述

思路

要实现滚动加载,就是当滚动条滚动到底部的时候,再去请求后端评论数据,然后把数据添加到响应式数据数组中,然后由vue更新dom。
因此,我们需要能有方式能够监测的到滚动条是否滚动到了底部,一般有两种方式:

scrollTop + clientHeight == scrollHeight

  • 可以通过 网页被卷去的头部高度 scrollTop 加上 浏览器客户端的高度 clientHeight 和 整个网页的高度 scrollHeight 做比较, 如果scrollTop + clientHeight == scrollHeight, 那么就可以说明滚动条已经到底了。

  • 为了避免误差, 可能还需要给scrollHeight预留一个比较小的范围。
    比如 scrollHeight - (scrollTop + clientHeight) <= 10, 这也就是说网页高度在仅剩10像素还在屏幕下面时的条件。

    • 然后监听window的scroll事件即可

IntersectionObserver

  • 也可以通过IntersectionObserver这个浏览器提供的api, 在评论的下面,添加一个显示正在加载的div,当这个div出现在屏幕中时,此时需要请求后台数据,
  • 数据请求回来后,更新dom,新添加的dom会把这个显示正在加载的div挤到最下面,当用户把新添加的内容浏览完,又去滚动,又看到这个显示正在加载的div,然后又去请求数据,直到把数据请求完

问题

  1. 这个IntersectionObserver浏览器的api还不太熟悉,如果加载两条之后,那个div如果还在页面,没有被挤到屏幕外面,还会回调观察函数吗?

    • 为了测试这个问题,我尝试把loading-area放到comment-wrapper的第一个元素的位置,这样它一开始就是出现的,然后一直都在这个位置,然后发现它很快速的连着发请求,直到把所有分页数据都请求完了。
    • 咦,我是不是忽略了一个东西了,我好像在这个正在加载中的div刚进入的比例达到0.5时,触发的请求分页数据,在发起请求前,我把isLoading置为了true,它如果为true的话,这个正在加载中的div的display就会为none,也就相当于它不被看见,然后,请求完了之后,我又把它置为了false,它又能被看见了,这就意味着,这个过程它从没被看见到看见又是一个变化,那么有可能这个变化也能被浏览器的IntersectionObserver这个api所监测到,所以很快速的引发的一连串的请求。好像误打误撞的无意中解决了这个问题,因为,对于这个问题,我觉得如果div还在页面的话,那就不算它又进入了,也就不会触发观察函数。就好像给了一个元素动画,但是这个元素是display:none,但是当这个元素不是display:none时,这个元素会立即有个动画。
    • 经过又一波测试,发现就是第二点说的那样,当元素在视口范围内,对display:none进行切换时,它也会触发观察函数
    • mdn中介绍:在创建IntersectionObserver时,可以指定第二个参数,第二个参数是一个配置对象,其中有一个threshold属性,默认值为0,意思是只要超过1个像素出现在root元素(默认是视口)中,那就执行观察函数,并且如果是超过了,那么isIntersecting就是true,那又分成2种情况,一种是从在视口外进入视口,刚好元素有一个像素出现在视口,此时,观察函数回调,并且isIntersecting为true(因为超过了0),此时,这个元素又向反方向离开视口,又会触发观察函数回调,此时isIntersecting为false(因为没超过0)。如果设置为0.2,当元素刚好超过20%的像素时,isIntersecting为true,此时,当元素反方向离开视口,此时,观察函数又会回调,isIntersecting为false。所以这个isIntersecting就是看有没有超过指定的阈值,超过了就是true,没超过就是false
  2. 当前滚动在第二页时,我发表了一个一级评论,

代码

Comment.vue

<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-zzhua195</el-button>
                <el-button @click="switchUser(2)">用户id2-ls</el-button>
                <el-button @click="switchUser(3)">用户id3-zj</el-button>
            </div>

            <!-- 主评论表情输入框 -->
            <emoji-text @comment="comment" :emojiSize="20"></emoji-text>

            <!-- 此处为渲染 评论列表, (所有的一级评论渲染列表) -->
            <!-- 还有一个比较麻烦的一点:每个一级评论的最下面都有一个评论输入框,
                                                               当点击这个一级评论的回复或者这个一级评论的任一子评论的回复时,
                                                               应当把其它一级评论下的输入框给隐藏掉。
                                         因此, 必须要能拿到所有的Reply, 并且需要知道哪个不关闭(其它的都要关掉), 所以用ref和标记index解决
                                         所以, 只能在父组件中收集所有的Reply, 然后子组件告诉父组件如何操作。
                                         在风宇博客中, 他是直接通过$ref拿到所有的子组件后, 通过子组件的$el属性, 通过修改$el属性的display来隐藏元素的 -->
            <!-- 把当前主评论的id给到子组件的parentId属性 -->
            <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>

Loading.vue

<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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/414900.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

零基础如何备考2023年系统集成项目管理工程师中级

系统集成项目管理工程师也是属于软考中级稍微好考一点的&#xff0c;管理类的知识多背多记多刷题。 中级集成是好考的&#xff0c;题目也不难&#xff0c;主要弄清楚47个过程的输入输出&#xff0c;还有工具的使用&#xff0c;几乎很多题都是按照这逻辑来的。 建议可以去网上…

ai写作软件免费-ai写作助手

ai写作 AI写作是一种利用人工智能技术和自然语言处理算法&#xff0c;从大量的数据中挖掘潜在信息、规律及信息流的写作方式。通过AI写作&#xff0c;可快速生成符合规范的文章、报告、文档、邮件等内容&#xff0c;为企业和个人提高效率、减少时间成本、实现更低的文字生产总成…

MySQL查询分组Group By原理分析

目录1. 使用group by的简单例子2. group by 原理分析2.1 explain 分析2.2 group by 的简单执行流程3. where 和 having的区别3.1 group by where 的执行流程3.2 group by having 的执行3.3 同时有where、group by 、having的执行顺序3.4 where having 区别总结4. 使用 group…

PR视频导出文件大

C 选择递刀工具 CtrlK 将视频分隔 pr导出的视频文件太大&#xff0c;通过这2个方法可以大大减小视频的大小&#xff0c;并且画质还能保持清晰&#xff01; 方法一&#xff1a;修改PR导出视频设置 在导出格式中选择【H.264】&#xff0c;在下面预设的位置选择【匹配源-高比特率…

永久免费内网穿透不限制速度

市面上的免费内网穿透大都有格式各样的限制&#xff0c;什么限制流量啊&#xff0c;每个月要签到打卡啊&#xff0c;还有更改域名地址等&#xff0c;只有神卓互联内网穿透是永久免费没有限制的&#xff0c;白嫖也可以。 这篇文章分享了3个方案&#xff0c;按照性能和综合指标排…

金三银四没把握住,凉了...

大家好&#xff0c;前两天跟朋友感慨&#xff0c;今年的铜三铁四、裁员、疫情导致好多人都没拿到offer!现在互联网大厂终于迎来了应届生集中求职季。 对于想跳槽的软件测试人来说&#xff0c;绝对是个找工作的好时机。这时候&#xff0c;很多高薪技术岗、管理岗的缺口和市场需…

提高stackoverflow方法速度方法

最近有些代码问题需要访问Stack Overflow - Where Developers Learn, Share, & Build Careers 但是刷新太慢&#xff0c;心都碎了 Stack Overflow - Where Developers Learn, Share, & Build CareersStack Overflow is the largest, most trusted online community …

在C上++!(上)

目录 一、传说中的C&#xff1a; 二、C发展史 三、应用领域 四、C怎么学&#xff1f; 五、命名空间namespace 1、命名空间的作用 2、如何定义命名空间 3、嵌套定义命名空间及其访问 ​编辑 4、命名空间的合并 六、简单输入输出 七、缺省参数 1、全缺省参数 2、半/部…

【从零开始学Skynet】基础篇(五):简易聊天室

在游戏中各玩家之间都可以进行聊天之类的交互&#xff0c;在这一篇中&#xff0c;我们就来实现一个简易的聊天室功能&#xff0c;这在上一篇代码的基础上很容易就能实现。1、功能需求 客户端发送一条消息&#xff0c;经由服务端转发&#xff0c;所有在线客户端都能收到&#xf…

【Jetpack】ActivityResult介绍及原理分析

​​ 前言 本文先介绍ActivityResult的基本使用&#xff0c;最后会通过源码来探讨背后的原理。 在Android中&#xff0c;我们如果想在Activity之间双向传递数据&#xff0c;需要使用startActivityForResult启动&#xff0c;然后在onActivityResult中处理返回&#xff0c;另外…

Vulnhub项目:Breakout

靶机地址&#xff1a;Empire: Breakout ~ VulnHub 渗透过程&#xff1a; 查询kali ip&#xff1a;192.168.56.104&#xff0c;靶机ip&#xff1a;192.168.56.131 探测靶机开放端口&#xff0c;利用nmap 靶机开放了80、139、445、10000、20000端口&#xff0c;先对80端口进行访…

【DevOps】GitOps多环境管理 - 别用多分支!

前言 在上一篇文章中【DevOps】GitOps多环境管理(上) - 别用多分支&#xff01;&#xff0c;我们介绍了在探索GitOps实践过程中会遇到的一些痛点&#xff0c;其中之一就是难以做到跨环境的版本发布&#xff0c;或者说怎么处理多个集群的部署。 在上一篇文章中&#xff0c;我们…

记一次内存泄漏排查

记一次内存泄漏排查 文章目录记一次内存泄漏排查背景问题排查问题处理背景 最近某项目的服务突然告警&#xff0c;cpu超85%&#xff0c;随后就是服务宕机。交付重启服务后恢复正常但是随后不久又开始告警&#xff0c;特别是白天&#xff0c;严重影响客户业务进行。 问题排查 …

【分享贴】如何衡量和提高项目成功?

“如何衡量项目成功&#xff1f;” 无论是对于项目经理还是组织来说都希望项目能够成功&#xff0c;但是怎样才算是项目成功了呢&#xff1f; 世界项目管理大师哈罗德科兹纳认为&#xff1a;“传统项目或运营项目成功的衡量标准是时间、成本和范围&#xff1b;创新项目成功的衡…

函数调用、

1、函数调用 重载了函数调用运算符&#xff08;&#xff09;的类 实例化的对象 就叫做函数对象 函数对象 &#xff08;&#xff09;触发 重载函数调用运算符 执行 》类似函数调用 &#xff08;仿函数&#xff09; #include <iostream> using namespace std; class Pr…

【hello Linux】Linux第一个小程序 - 进度条

目录 先来区分两个标识符&#xff1a;回车和换行 1. 倒计时 2. 进度条 Linux&#x1f337; 下面来编写Linux系统下的第一个小程序 - 进度条 先来区分两个标识符&#xff1a;回车和换行 \r 和 \n \r 回车 &#xff1a;代表回到本行的开头&#xff1b; \n 换行 &#xff1a;代表…

【Linux】vscode的使用 | 进程间通信(简单概括)

文章目录1.vscode的下载2. vscode的使用1. 连接远端2. 在vscode创建文件并运行程序切换到命令行3. 安装常见插件3. 进程间通信1. 简单举例2.管道原理为什么把读写都打开&#xff0c;只打开读或者写不可以吗&#xff1f;3. 通过父子进程理解管道1. 创建匿名管道系统调用为什么可…

不用996,不用007,赚的还比我多?我直接好家伙

今天打开手机就看见信息99&#xff0c;哟吼&#xff0c;还挺热闹——感情都在上班摸鱼呢。 好奇心让我点了第一条未读信息&#xff0c;好家伙&#xff0c;直接让我手机闪退出APP了&#xff01; 嗨&#xff0c;我这暴脾气&#xff0c;直接手动滑到了第一条&#xff01;但是我没…

CentOS7-部署Tomcat并运行Jpress

1. 简述静态网页和动态网页的区别。 2. 简述 Webl.0 和 Web2.0 的区别。 3. 安装tomcat8&#xff0c;配置服务启动脚本&#xff0c;部署jpress应用。1、简述静态网页和动态网页的区别 静态网页&#xff1a; 请求响应信息&#xff0c;发给客户端进行处理&#xff0c;由浏览器进…

009:Mapbox GL点击click某位置,显示坐标信息

第009个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中点击某位置,显示坐标信息 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共81行)相关API参考:专栏目标示例效果 配置方式 1)查看基础设置:htt…