基于uniapp实现即时通讯聊天界面(仿微信)

最近在开发一款即时通讯聊天APP,主要是实现聊天的功能,在实现过程中遇到很多坑,网上也查找资料不是很全,经过一番摸索最终还是实现了,特此记录一下,以备后续自己查看方便,也为同样问题的小伙伴提共参考和借鉴。

先展示下最终的效果图:

其实聊天app的界面设计,难点主要是通过监控键盘的高度变化来调整整个聊天内容和输入框的位置,还有切换表情时的变化。

  下面是完整的聊天界面代码,可以直接复制看效果,我这套聊天系统已经上线,前后端分离开发 ,具体效果可以查看我的

演示站点(http://chat.xidicom.cn/)

也可以自己下载源码本地运行查看效果(下载地址也在演示站点)

直接上代码

<template>
	<view>
		<view class="chat-list">
			<block v-for="msg in record">
				<view class="chat-item chat-right" v-if="msg.from_id === userInfo.id">
					<u-avatar :src="host+userInfo.avatar"></u-avatar>
					<view class="chat-content content-right">{{ msg.content }}</view>
				</view>
				<view class="chat-item chat-left" v-else>
					<u-avatar :src="host+chatUser.avatar"></u-avatar>
					<view class="chat-content content-left">{{ msg.content }}</view>
				</view>
			</block>
		</view>
		<view :style="{height: paddingBottom+'px'}" id="chat-bottom"></view>
		<view class="input-bar" :style="{ bottom: keyboardHeight + 'px' }">
			<view class="input-box">
				<input ref="inputRef" class="input" :focus="isFocus" v-model="message" confirm-hold
					cursor-color="#00aa00" confirm-type="send" :adjust-position="false" @focus="inputFocus"
					@blur="inputBlur" @confirm="sendMessage"></input>
				<image class="emoji-btn" :src="`/static/${emojiType}.png`" mode="heightFix"
					@touchstart.prevent="changeEmoji">
				</image>
				<button class="send-btn" size="mini" v-if="message" @touchstart.prevent="sendMessage">发送</button>
				<u-icon name="plus-circle" size="30" v-else @click="more"></u-icon>
			</view>
			<scroll-view class="emoji-scroll" scroll-y :style="{height: emojiHeight+'px'}">
				<view class="emoji-panel">
					<view class="emoji-item" v-for="(item, index) in emojisArr" @touchstart.prevent="message+=item">
						{{item}}
					</view>
				</view>
			</scroll-view>
		</view>

	</view>
</template>
<script>
	import {
		mapGetters
	} from 'vuex';
	import WebSocketService from '@/config/websocket.js';
	export default {
		data() {
			return {
				paddingBottom: 50,
				record: [],
				message: '', // 当前输入消息

				userInfo: uni.getStorageSync('userInfo'),
				chatUser: this.$store.state.currentChatUser,

				page: 0, // 当前页数
				size: 10, //加载条数
				isLoading: false, // 是否在加载中
				noMoreData: false,

				typing: '对方正在输入',
				interval: null,
				isFocus: false,
				keyboardHeight: 0, // 键盘高度
				emojiHeight: 0, //表情高度
				emojiType: 'emoji',
				emojisArr: ['😊', '😁', '😀', '😃', '😣', '😞', '😩', '😫', '😲', '😟', '😦', '😜', '😳', '😋', '😥', '😰',
					'🤠', '😎', '😇', '😉', '😭', '😈', '😕', '😏', '😘', '😤', '😡', '😅', '😬', '😺', '😻', '😽',
					'😼', '🙈', '🙉', '🙊', '🔥', '👍', '👎', '👌', '✌️', '🙏', '💪', '👻'
				],

			};
		},
		computed: {
			...mapGetters(['getCurrentMessage', 'getTyping']), // 使用 Vuex getter 获取聊天消息
		},
		watch: {
			getCurrentMessage(newMessage) {
				console.log(newMessage)
				this.record.push(newMessage);
				this.scrollBottom();
			},
			getTyping(newVal) {
				console.log(newVal)
				if (newVal.state == 1 && newVal.from_id == this.chatUser.id) {
					this.startTyping(); 
				} else { 
					clearInterval(this.interval)
					uni.setNavigationBarTitle({
						title: this.chatUser.nickname
					})
				}
			}
		},
		onPullDownRefresh() {
			if (this.noMoreData) {
				uni.showToast({
					title: '没有更多数据了',
					icon: 'none'
				})
				uni.stopPullDownRefresh()
			}
			this.loadMoreData()
		},
		onLoad() {
			uni.setNavigationBarTitle({
				title: this.chatUser.nickname
			})
			this.loadMoreData();
			this.showKeyboard();
		},
		onUnload() {
			console.log('离开页面')
			clearInterval(this.interval);
			this.$store.commit('setCurrentChatUser', null);
			this.$store.commit('setCurrentMessage', {});
			this.api.read({
				to: this.chatUser.id
			})
		},
		methods: {
			loadMoreData() {
				if (this.isLoading || this.noMoreData) return;
				this.isLoading = true; // 设置加载中标志
				this.api.record({
					id: this.chatUser.id,
					page: this.page,
					size: this.size
				}).then(res => {
					uni.stopPullDownRefresh()
					this.isLoading = false; // 加载完成
					this.record = [...res.reverse(), ...this.record]
					if (this.page == 0) {
						this.scrollBottom();
					}
					if (res.length < this.size) {
						this.noMoreData = true;
					} else {
						this.page++;
					}
				})
			},
			showKeyboard() {
				uni.onKeyboardHeightChange((res) => {
					if (res.height > 0) {
						this.emojiType = 'emoji';
						this.emojiHeight = 0;
						this.keyboardHeight = res.height;
						this.paddingBottom = res.height + 50
						this.scrollBottom();
					} else {
						this.keyboardHeight = 0;
						// this.paddingBottom = 50;
						this.scrollBottom();
					}
				});
			},

			sendMessage() {
				if (this.message.trim() === '') return;
				const message = {
					type: "chat",
					content: this.message,
					from_id: this.userInfo.id,
					to_id: this.chatUser.id,
				};
				WebSocketService.sendMessage(message);
				// this.$store.commit('setMessages', message);
				this.$store.commit('setCurrentMessage', message);
				// this.record.push(message);
				this.scrollBottom();
				message.createtime = Math.floor(Date.now() / 1000);
				this.$store.dispatch('updateSessions', {
					from_id: this.chatUser.id,
					content: this.message
				})
				this.message = '';
			},

			more() {
				uni.showToast({
					title: '该功能正在开发中',
					icon: 'none'
				})
			},
			inputFocus() {
				WebSocketService.sendMessage({
					type: 'typing',
					from_id: this.userInfo.id,
					to_id: this.chatUser.id,
					state: 1
				});
			},
			inputBlur() {
				this.emojiHeight = 0;
				this.emojiType = 'emoji';
				this.paddingBottom = 50;
				WebSocketService.sendMessage({
					type: 'typing',
					from_id: this.userInfo.id,
					to_id: this.chatUser.id,
					state: 0
				});
			},
			startTyping() {
				let count = 0;
				this.interval = setInterval(() => {
					if (count < 3) {
						this.typing = '对方正在输入' + '.'.repeat(count + 1);
						count++;
					} else {
						count = 0;
						this.typing = '对方正在输入';
					}
					uni.setNavigationBarTitle({
						title: this.typing
					})
				}, 500);
			},
			changeEmoji() {
				if (this.emojiType === 'emoji') {
					uni.hideKeyboard();
					this.$nextTick(() => {
						this.emojiType = 'keyboard';
						this.emojiHeight = 150;
						this.paddingBottom = 200;
						this.scrollBottom();
					})
				}
				if (this.emojiType === 'keyboard') {
					this.isFocus = true;
					this.$nextTick(() => {
						this.emojiType = 'emoji';
						this.emojiHeight = 0;
						this.scrollBottom();
					})
				}
			},

			scrollBottom() {
				this.$nextTick(() => {
					uni.pageScrollTo({
						selector: '#chat-bottom',
						duration: 100
					});
				})
			},

		}

	};
</script>
<style>
	.chat-list {
		transition: all 0.3 ease;
	}

	.chat-item {
		display: flex;
		margin: 20upx 0;
		padding: 20upx;
		border-radius: 10upx;
		position: relative;
	}

	.chat-right {
		flex-direction: row-reverse;
	}

	.chat-content {
		border-radius: 10upx;
		padding: 16upx;
		margin: 10upx 20upx;
		max-width: 500upx;
		position: relative;
	}

	.content-left {
		background-color: #f3f3f3;
	}

	.content-right {
		background-color: #95ec69;
	}

	.chat-content::after {
		content: '';
		position: absolute;
		top: 30upx;
		border-top: 10upx solid transparent;
		border-bottom: 10upx solid transparent;
		transform: translateY(-50%);
	}

	.content-left::after {
		left: -10upx;
		border-right: 10upx solid #f3f3f3;
	}

	.content-right::after {
		right: -10upx;
		border-left: 10upx solid #95ec69;
	}

	.input-bar {
		position: fixed;
		bottom: 0;
		width: 100%;
		z-index: 999;
		/* border-top: solid 1px #333; */
		background: #f8f8f8;
	}

	.input-box {
		display: flex;
		align-items: center;
		padding: 20upx;
		box-sizing: border-box;
		height: 50px;
	}

	.input {
		flex: 1;
		padding: 10upx 20upx;
		border-radius: 20px;
		background: #eeeeee;

	}

	.emoji-btn {
		height: 30px;
		margin: 0 20upx;
	}

	.send-btn {
		color: #ffffff;
		background: #00aa00;
		border-radius: 20px;
	}

	.emoji-scroll {
		transition: all 0.3s ease;
	}

	.emoji-panel {
		padding: 20upx;
		display: flex;
		flex-wrap: wrap;
	}

	.emoji-item {
		padding: 10upx;
		font-size: 50upx;
	}
</style>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值