说明
之前已经在【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;
将元素的宽度和高度设置为0border-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>
【效果】
私聊页面滑动到顶部获取历史数据
相较于上篇文章,除了替换了聊天气泡,聊天页面在加载历史数据的时候添加了“正在加载”字样,如下图所示
当获取历史消息时,将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>