【易售小程序项目】小程序私聊页面完善(带尾巴聊天气泡组件封装、滑至顶端获取历史聊天数据逻辑优化)【后端基于若依管理系统开发】

简介: 【易售小程序项目】小程序私聊页面完善(带尾巴聊天气泡组件封装、滑至顶端获取历史聊天数据逻辑优化)【后端基于若依管理系统开发】

说明

之前已经在【UniApp开发小程序】私聊功能uniapp界面实现 (买家、卖家 沟通商品信息)【后端基于若依管理系统开发】这篇文章中介绍了私聊页面的实现,这篇文章主要针对一些细节进行完善

仿微信带尾巴聊天气泡组件

效果展示

组件整体代码

<template>
  <view class="bubble" :class="tailDirection" :style="{'--tail-color':backgroundColor}">
    <text class="content" :style="{'background-color': backgroundColor,'color':fontColor}">{{text}}</text>
  </view>
</template>
<script>
  export default {
    props: {
      // 气泡的尾巴朝向 left:左 right:右
      tailDirection: {
        type: String,
        default: 'left'
      },
      // 气泡的背景颜色
      backgroundColor: {
        type: String,
        default: '#ffffff'
      },
      // 气泡的字体颜色
      fontColor: {
        type: String,
        default: '#000000'
      },
      // 气泡里面显示的文字
      text: {
        type: String,
        default: ''
      }
    },
    data: {
      contentId: 0,
      contentStyle: {}
    },
  }
</script>
<style lang="scss">
  .bubble {
    display: inline-flex;
    position: relative;
    align-items: center;
    .content {
      // 设置气泡的内间距,让气泡边缘距离文字有一定的距离
      padding: 10px 10px;
      // 设置气泡的边框半径,使边框有弧度
      border-radius: 8px;
      font-family: sans-serif;
      // 解决英文字符串、数字不换行的问题
      word-break: break-all;
      word-wrap: break-word;
    }
  }
  .left {
    margin-left: 5px;
  }
  .right {
    margin-right: 5px;
  }
  .left:before {
    position: absolute;
    content: "\00a0";
    width: 0px;
    height: 0px;
    border-width: 5px 10px 5px 0;
    border-style: solid;
    border-color: transparent var(--tail-color) transparent transparent;
    top: 10px;
    left: -10px;
  }
  .right:before {
    position: absolute;
    content: "\00a0";
    // display: inline-block;
    width: 0px;
    height: 0px;
    border-width: 5px 0px 5px 10px;
    border-style: solid;
    border-color: transparent transparent transparent var(--tail-color);
    top: 10px;
    right: -10px;
  }
</style>

气泡主体

气泡主体主要使用一个text标签来存储文字内容,并设置背景颜色、边框半径、内间距、单词和数字分解

气泡尾巴

【伪元素(气泡尾巴)的css介绍】

.left:before.right:before 两个伪元素主要用来给气泡添加尾巴,一个向左、一个向右

  • :before 使用该伪元素可以用来向被选元素的内容前插入一个虚拟元素,用于显示一些额外的内容或进行样式修饰,比如添加图标、箭头、编号……
  • position: absolute; 将伪元素的位置设置为绝对定位,以便于相对于其父元素位置进行位置设置
  • content: "\00a0"; 添加一个不间断空格,作为伪元素的填充内容
  • width: 0px; height: 0px; 将元素的宽度和高度设置为0
  • border-width: 5px 10px 5px 0; 设置边框宽度,按顺序分别为上边框、右边框、下边框和左边框,其中左边框为0,因此左边不需要边框
  • border-style: solid; 将边框样式设置为实线
  • border-color: transparent var(--tail-color) transparent transparent; 设置边框颜色
  • top: 10px; left: -10px; 设置伪元素相对于父元素的位置

【修改一】

先将view的宽高都设为0,然后给view设置较粗的边框,最终渲染的时候,边框与边框会相交出三角形。当每条边框都设置不同的颜色时,效果如下图所示

.left:before {
  position: absolute;
  content: "\00a0";
  width: 0px;
  height: 0px;
  border-width: 10px 10px 10px 10px;
  border-style: solid;
  border-color: black var(--tail-color) blue yellow; 
  top: 10px;
  left: -30px;
}

【修改二】

要想只保留最右边的三角形,只需要将其他3个三角形都设置为透明即可

.left:before {
  position: absolute;
  content: "\00a0";
  width: 0px;
  height: 0px;
  border-width: 10px 10px 10px 10px;
  border-style: solid;
  border-color: transparent var(--tail-color) transparent transparent;
  top: 10px;
  left: -30px;
}

【修改三】

因为该三角形只由上边框、右边框、左边框相交即可得到,因此可以将左边框的宽度设置为0。border-width: 10px 10px 10px 0;分别设置上、右、下、左边框

.left:before {
  position: absolute;
  content: "\00a0";
  width: 0px;
  height: 0px;
  border-width: 10px 10px 10px 0;
  border-style: solid;
  border-color: transparent var(--tail-color) transparent transparent;
  top: 10px;
  left: -30px;
}

【修改四】

下面需要修改一下伪元素相对于父元素的位置,因为右边框的宽度是10px,通过left: -10px;让伪元素向左边偏移10px,这样尾巴刚好贴紧气泡

.left:before {
  position: absolute;
  content: "\00a0";
  width: 0px;
  height: 0px;
  border-width: 10px 10px 10px 0;
  border-style: solid;
  border-color: transparent var(--tail-color) transparent transparent;
  top: 10px;
  left: -10px;
}

【最终版】

最好修改一下上下边框的宽度,让尾巴瘦一点

.left:before {
  position: absolute;
  content: "\00a0";
  width: 0px;
  height: 0px;
  border-width: 5px 10px 5px 0;
  border-style: solid;
  border-color: transparent var(--tail-color) transparent transparent;
  top: 10px;
  left: -10px;
}

【尾巴颜色控制】

需要注意的是,尾巴的颜色也需要可以由开发者去定义,因此使用 var(--tail-color) 来控制伪元素从变量中获取颜色,并在下面的代码中对颜色进行赋值

<view class="bubble" :class="tailDirection" :style="{'--tail-color':backgroundColor}">

使用

如下面的代码所示,开发者可以在使用组件的时候设置气泡的尾巴朝向、背景颜色、字体颜色和气泡文字

props: {
  // 气泡的尾巴朝向 left:左 right:右
  tailDirection: {
    type: String,
    default: 'left'
  },
  // 气泡的背景颜色
  backgroundColor: {
    type: String,
    default: '#ffffff'
  },
  // 气泡的字体颜色
  fontColor: {
    type: String,
    default: '#000000'
  },
  // 气泡里面显示的文字
  text: {
    type: String,
    default: ''
  }
},

【引入组件并使用的代码】

<template>
  <view class="page">
    <bubble tailDirection="right" color="blue" text="Hello, I'm chat bubble!"  backgroundColor="#00ffff" fontColor="#ff0000"/>
  </view>
</template>
<script>
import Bubble from '@/components/bubble/bubble.vue'
export default {
  components: {
    Bubble
  }
}
</script>
<style>
.page {
  padding: 20px;
}
</style>

【效果】

image.png

私聊页面滑动到顶部获取历史数据

相较于上篇文章,除了替换了聊天气泡,聊天页面在加载历史数据的时候添加了“正在加载”字样,如下图所示

当获取历史消息时,将loadHistory设置为true,显示“正在加载”,同时让用户在等待此次加载结束之后才能重新加载下一批历史消息

<!-- 显示加载相关字样 -->
<u-loadmore v-if="loadHistory==true" status="loading" />
/**
* 滑到最顶端,分页加一,拉取更早的数据
 */
getHistoryChat() {
  // console.log("获取历史消息")
  if (this.messageList.length < this.total && this.loadHistory == false) {
    // 当目前的消息条数小于消息总量的时候,才去查历史消息
    this.page.pageNum++;
    this.loadHistory = true;
    this.scrollToView = '';
    this.listChat().then(() => {
      setTimeout(() => {
        this.loadHistory = false;
      }, 1000)
    })
  }
},

页面整体代码

【私聊页面】

<template>
  <view style="height:100vh;">
    <!-- @scrolltoupper:上滑到顶部执行事件,此处用来加载历史消息 -->
    <!-- scroll-with-animation="true" 设置滚动条位置的时候使用动画过渡,让动作更加自然 -->
    <scroll-view :scroll-into-view="scrollToView" scroll-y="true" class="messageListScrollView"
      :style="{height:scrollViewHeight}" @scrolltoupper="getHistoryChat()"
      :scroll-with-animation="!isFirstListChat" ref="chatScrollView">
      <!-- 显示加载相关字样 -->
      <u-loadmore v-if="loadHistory==true" status="loading" />
      <view v-for="(message,index) in messageList" :key="message.id" :id="`message`+message.id"
        style="width: 750rpx;min-height: 60px;">
        <view style="height: 10px;"></view>
        <view v-if="message.type==0" class="messageItemLeft">
          <view style="width: 8px;"></view>
          <u--image :showLoading="true" :src="you.avatar" width="50px" height="50px" radius="3"></u--image>
          <view style="width: 7px;"></view>
          <view class="messageBubble">
            <bubble tailDirection="left" :text="message.content" backgroundColor="#ffffff" />
          </view>
        </view>
        <view v-if="message.type==1" class="messageItemRight">
          <view class="messageBubble">
            <bubble tailDirection="right" :text="message.content" backgroundColor="#95EC69" />
          </view>
          <view style="width: 7px;"></view>
          <u--image :showLoading="true" :src="me.avatar" width="50px" height="50px" radius="3"></u--image>
          <view style="width: 8px;"></view>
        </view>
      </view>
    </scroll-view>
    <view class="messageSend">
      <view class="messageInput">
        <u--textarea v-model="messageInput" placeholder="请输入消息内容" autoHeight>
        </u--textarea>
      </view>
      <view style="width:5px"></view>
      <view class="commmitButton" @click="send()">发 送</view>
    </view>
  </view>
</template>
<script>
  import {
    getUserProfileVo
  } from "@/api/user";
  import {
    listChat
  } from "@/api/market/chat.js";
  import Bubble from '@/components/bubble/bubble.vue'
  let socket;
  export default {
    components: {
      Bubble
    },
    data() {
      return {
        webSocketUrl: "",
        socket: null,
        messageInput: '',
        // 我自己的信息
        me: {},
        // 对方信息
        you: {},
        scrollViewHeight: undefined,
        messageList: [],
        // 底部滑动到哪里
        scrollToView: '',
        page: {
          pageNum: 1,
          pageSize: 20
        },
        isFirstListChat: true,
        // 是否正在加载更多历史数据
        loadHistory: false,
        // 消息总条数
        total: 0,
        // 数据加载状态
        loadmoreStatus: "loadmore",
      }
    },
    created() {
      this.me = uni.getStorageSync("curUser");
    },
    beforeDestroy() {
      console.log("执行销毁方法");
      this.endChat();
    },
    onLoad(e) {
      // 设置初始高度
      this.scrollViewHeight = `calc(100vh - 20px - 44px)`;
      this.you = JSON.parse(decodeURIComponent(e.you));
      uni.setNavigationBarTitle({
        title: this.you.nickname,
      })
      this.startChat();
      this.listChat();
      this.receiveMessage();
    },
    onReady() {
      // 监听键盘高度变化,以便设置输入框的高度
      uni.onKeyboardHeightChange(res => {
        let keyBoardHeight = res.height;
        console.log("keyBoardHeight:" + keyBoardHeight);
        this.scrollViewHeight = `calc(100vh - 20px - 44px - ${keyBoardHeight}px)`;
        this.scrollToView = '';
        setTimeout(() => {
          this.scrollToView = 'message' + this.messageList[this
            .messageList.length - 1].id;
        }, 150)
      })
    },
    methods: {
      /**
       * 发送消息
       */
      send() {
        if (this.messageInput != '') {
          let message = {
            from: this.me.userName,
            to: this.you.username,
            text: this.messageInput
          }
          // console.log("this.socket.send:" + this.$socket)
          // 将组装好的json发送给服务端,由服务端进行转发
          this.$socket.send({
            data: JSON.stringify(message)
          });
          this.total++;
          let newMessage = {
            // code: this.messageList.length,
            type: 1,
            content: this.messageInput
          };
          this.messageList.push(newMessage);
          this.messageInput = '';
          this.toBottom();
        }
      },
      /**
       * 开始聊天
       */
      startChat() {
        let message = {
          from: this.me.userName,
          to: this.you.username,
          text: "",
          status: "start"
        }
        // 告诉服务端要开始聊天了
        this.$socket.send({
          data: JSON.stringify(message)
        });
      },
      /**
       * 结束聊天
       */
      endChat() {
        let message = {
          from: this.me.userName,
          to: this.you.username,
          text: "",
          status: "end"
        }
        // 告诉服务端要结束聊天了
        this.$socket.send({
          data: JSON.stringify(message)
        });
      },
      /**
       * 接收消息
       */
      receiveMessage() {
        this.$socket.onMessage((response) => {
          // console.log("接收消息:" + response.data);
          let message = JSON.parse(response.data);
          let newMessage = {
            // code: this.messageList.length,
            type: 0,
            content: message.text
          };
          this.messageList.push(newMessage);
          this.total++;
          // 让scroll-view自动滚动到最新的数据那里
          // this.$nextTick(() => {
          //  // 滑动到聊天区域最底部
          //  this.scrollToView = 'message' + this.messageList[this
          //    .messageList.length - 1].id;
          // });
          this.toBottom();
        })
      },
      /**
       * 查询对方和自己最近的聊天数据
       */
      listChat() {
        return new Promise((resolve, reject) => {
          listChat(this.you.username, this.page).then(res => {
            for (var i = 0; i < res.rows.length; i++) {
              this.total = res.total;
              if (res.rows[i].fromWho == this.me.userName) {
                res.rows[i].type = 1;
              } else {
                res.rows[i].type = 0;
              }
              // 将消息放到数组的首位
              this.messageList.unshift(res.rows[i]);
            }
            if (this.isFirstListChat == true) {
              // this.$nextTick(function() {
              //  // 滑动到聊天区域最底部
              //  this.scrollToView = 'message' + this.messageList[this
              //    .messageList.length - 1].id;
              // })
              this.isFirstListChat = false;
              this.toBottom();
            }
            resolve();
          })
        })
      },
      /**
       * 滑到最顶端,分页加一,拉取更早的数据
       */
      getHistoryChat() {
        // console.log("获取历史消息")
        if (this.messageList.length < this.total && this.loadHistory == false) {
          // 当目前的消息条数小于消息总量的时候,才去查历史消息
          this.page.pageNum++;
          this.loadHistory = true;
          this.scrollToView = '';
          this.listChat().then(() => {
            setTimeout(() => {
              this.loadHistory = false;
            }, 1000)
          })
        }
      },
      /**
       * 滑动到聊天区域最底部
       */
      toBottom() {
        // 让scroll-view自动滚动到最新的数据那里
        this.scrollToView = '';
        setTimeout(() => {
          // 滑动到聊天区域最底部
          this.scrollToView = 'message' + this.messageList[this
            .messageList.length - 1].id;
        }, 150)
      }
    }
  }
</script>
<style lang="scss">
  .messageListScrollView {
    background: #F5F5F5;
    // overflow: auto;
    .messageBubble {
      max-width: calc(750rpx - 10px - 50px - 15px - 10px - 50px - 15px);
      padding: 0px 0px 10px 0px;
    }
    .messageItemLeft {
      display: flex;
      align-items: flex-start;
      justify-content: flex-start;
    }
    .messageItemRight {
      display: flex;
      align-items: flex-start;
      justify-content: flex-end;
    }
  }
  .messageSend {
    display: flex;
    background: #ffffff;
    padding-top: 5px;
    padding-bottom: 15px;
    .messageInput {
      border: 1px #EBEDF0 solid;
      border-radius: 5px;
      width: calc(750rpx - 65px);
      margin-left: 5px;
    }
    .commmitButton {
      height: 38px;
      border-radius: 5px;
      width: 50px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #ffffff;
      background: #3C9CFF;
    }
  }
</style>


目录
相关文章
|
人工智能 Java API
后端开发必看:零代码实现存量服务改造成MCP服务
本文介绍如何通过 **Nacos** 和 **Higress** 实现存量 Spring Boot 服务的零代码改造,使其支持 MCP 协议,供 AI Agent 调用。全程无需修改业务代码,仅通过配置完成服务注册、协议转换与工具映射,显著降低改造成本,提升服务的可集成性与智能化能力。
3297 1
|
Java 关系型数据库 MySQL
在Linux平台上进行JDK、Tomcat、MySQL的安装并部署后端项目
现在,你可以通过访问http://Your_IP:Tomcat_Port/Your_Project访问你的项目了。如果一切顺利,你将看到那绚烂的胜利之光照耀在你的项目之上!
615 41
|
存储 消息中间件 前端开发
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
校园圈子系统校园论坛小程序采用uni-app前端框架,支持多端运行,结合PHP后端(如ThinkPHP/Laravel),实现用户认证、社交关系管理、动态发布与实时聊天功能。前端通过组件化开发和uni.request与后端交互,后端提供RESTful API处理业务逻辑并存储数据于MySQL。同时引入Redis缓存热点数据,RabbitMQ处理异步任务,优化系统性能。核心功能包括JWT身份验证、好友系统、WebSocket实时聊天及活动管理,确保高效稳定的用户体验。
764 4
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
|
前端开发 Java 数据库连接
后端开发中的错误处理实践:原则与实战
在后端开发中,错误处理是保障系统稳定性的关键。本文介绍了错误分类、响应设计、统一处理机制及日志追踪等实践方法,帮助开发者提升系统的可维护性与排障效率,做到防患于未然。
|
前端开发 JavaScript 关系型数据库
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
971 5
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
|
存储 移动开发 小程序
校园圈子系统小程序(圈子论坛、私信聊天、资料共享、二手交易、兼职,跑腿)开源码开发/微信公众号、小程序、H5多端账号同步/搭建多城市的综合社交生活平台
基于开源技术栈构建的校园圈子系统小程序,整合社交与生活服务功能,涵盖兴趣圈子、私信聊天、资料共享、二手交易、兼职跑腿等六大核心模块。通过多端账号同步(微信公众号/小程序/H5),实现数据实时交互,满足学生群体的多元化需求。项目精准锚定校园市场,以“社交+服务”双轮驱动,提供一站式解决方案,支持快速部署与多校区运营,同时具备广告、佣金、会员等多元变现能力,是打造校园生态的理想工具。
1639 3
校园圈子系统小程序(圈子论坛、私信聊天、资料共享、二手交易、兼职,跑腿)开源码开发/微信公众号、小程序、H5多端账号同步/搭建多城市的综合社交生活平台
|
人工智能 小程序 NoSQL
【一步步开发AI运动小程序】二十一、如何将AI运动项目配置持久化到后端?
本文介绍基于云智「Ai运动识别引擎」的运动配置持久化方案,旨在优化小程序或Uni APP中AI运动识别能力。通过将运动检测参数(如`Key`、`Name`、`TickMode`、`rules`或`samples`)持久化到后端,可避免因频繁调整运动参数而重新发布应用,提升用户体验。持久化数据结构支持规则和姿态样本存储,适用于关系数据库、文件或文档数据库(如MongoDB)。此外,云智还提供运动自动适配工具及「AI乐运动」产品,助力快速实现AI体育、全民健身等场景。
|
存储 缓存 负载均衡
后端开发中的性能优化策略
本文将探讨几种常见的后端性能优化策略,包括代码层面的优化、数据库查询优化、缓存机制的应用以及负载均衡的实现。通过这些方法,开发者可以显著提升系统的响应速度和处理能力,从而提供更好的用户体验。
645 6
|
JSON 自然语言处理 前端开发
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
780 72
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
|
消息中间件 运维 安全
后端开发中的微服务架构实践与挑战####
在数字化转型的浪潮中,微服务架构凭借其高度的灵活性和可扩展性,成为众多企业重构后端系统的首选方案。本文将深入探讨微服务的核心概念、设计原则、关键技术选型及在实际项目实施过程中面临的挑战与解决方案,旨在为开发者提供一套实用的微服务架构落地指南。我们将从理论框架出发,逐步深入至技术细节,最终通过案例分析,揭示如何在复杂业务场景下有效应用微服务,提升系统的整体性能与稳定性。 ####
395 32

热门文章

最新文章