文章目录
- 说明
- 仿微信带尾巴聊天气泡组件
- 效果展示
- 组件整体代码
- 气泡主体
- 气泡尾巴
- 使用
- 私聊页面滑动到顶部获取历史数据
- 页面整体代码
说明
之前已经在【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>