記事ディレクトリ
説明する
尾付きの模造 WeChat チャット バブル コンポーネント
結果を示す
コンポーネント全体のコード
<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>
バブルボディ
バブル本体は主にテキストタグを使用してテキストコンテンツを保存し、背景色、境界線の半径、内部の間隔、単語と数字の分解を設定します。
バブルテール
[CSS 疑似要素 (バブルテール) 入門]バブルに尻尾を追加するには
.left:before
、.right:before
主に 2 つの疑似要素 (左側と右側に 1 つ) を使用します。
: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;
親要素を基準とした擬似要素の位置を設定します
【修正1】
まずビューの幅と高さを0に設定し、次にビューの境界線を太く設定すると、最終的にレンダリングすると境界線が交差して三角形になります。各境界線を異なる色に設定すると、次のようになります。
.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;
}
【修正2】
一番右の三角形だけを残しておきたい場合は、他の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;
}
[変形例3]
三角形は上枠、右枠、左枠の交点でしか求められないため、左枠の幅を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;
}
【修正4】
次に、親要素を基準とした疑似要素の位置を変更する必要がありますが、右側のボーダーの幅が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>
同じプロジェクトに関する他の記事
このプロジェクトのその他の記事は、 【簡単ミニプログラムプロジェクト】プロジェクト紹介、ミニプログラムページ表示、連載記事集をご覧ください。