自己研究的聊天界面,在安卓真机上进行调试,软键盘和功能面板切换时有概率会闪一下,但整体上比较顺畅。
为方便测试,直接写view标签高度(152-输入框高度 96-Bar高度 212.56-功能面板高度),正常来说应该用下面这种方式进行计算
const query = uni.createSelectorQuery();
query.select('.panel-container').boundingClientRect();
query.select('.input-container').boundingClientRect();
query.exec((res) => {
const heights = {
panelHeight: res[0]?.height || 0,
inputHeight: res[1]?.height || 0
};}
关于软键盘将页面往上顶的内容不进行赘述,要在pages.json配置 "softinputMode": "adjustPan"
uniapp 软键盘将页面往上顶文章浏览阅读755次,点赞28次,收藏5次。顶部标题_uniapp 动态配置 softinputmode
https://blog.csdn.net/yunyun1110_/article/details/146471619?spm=1001.2014.3001.5501
chat.vue聊天窗口界面
<template>
<view class="container">
<!-- 顶部导航栏 -->
<Bar :title="name" isMore />
<!-- 消息列表 -->
<scroll-view scroll-y="true" :scroll-top="scrollTop" class="message-container" :style="{ height: chatHeight }" :scroll-into-view="scrollIntoView">
<!-- 实时获取键盘的高度和谈起的功能框的高度,动态设置paddingBottom -->
<text class="time">{{ currentTime }}</text>
<view v-for="(msg, index) in messages" :key="index" :id="'msg-' + index"
:class="['message-wrapper', msg.isSent ? 'sent' : 'received']">
<image v-show="!msg.isSent" class="avatar" src="../../static/user/touxiang.svg"></image>
<view class="service">
<text v-show="!msg.isSent" class="serviceName">糖果</text>
<view class="message">
<text>{{ msg.content }}</text>
</view>
</view>
<image v-show="msg.isSent" class="avatar" src="../../static/order/weitu.png"></image>
</view>
</scroll-view><!-- 底部输入框和按钮 -->
<view class="bottom-container" cursor-spacing="0"
:style="{ transform: `translateY(${btmTranslateY})`, bottom: keyboardHeight }">
<view class="input-container" ref="inputContainer">
<input id="messageInput" maxlength="500" v-model="inputMessage" :adjust-position="false" placeholder="在此输入您的问题" />
<!-- 切换图标 -->
<image :src="
changeBtn
? '../../static/chat/more.svg'
: '../../static/chat/close.svg'
" v-show="!inputMessage" @click="changeButton"></image>
<!-- 发送按钮 -->
<button @touchend.prevent="sendMessage" class="options-button" v-show="inputMessage">
发送
</button>
</view><!-- 功能面板 -->
<view class="panel-container" v-show="isPanel">
<Panel></Panel>
</view>
</view>
</view>
</template><script setup>
import { ref,
onMounted,
onUnmounted,
nextTick,
watch
} from 'vue';
import Bar from '@/components/Bar/Bar.vue';
import Panel from '@/components/Chat/Panel.vue';
import {
useSystemInfoStore
} from '@/store/systemInfo.js';
const systemInfo = useSystemInfoStore();
const {
barHeightRpx,
screenWidth,
screenHeightRpx
} = systemInfo.dataList;const name = ref('name');
const inputMessage = ref(''); //用户输入框的信息
const messages = ref([]); //存储对话信息
const scrollIntoView = ref('');
const scrollTop = ref(0); //控制滚动
const changeBtn = ref(true); //切换展示/关闭功能面板的图标
const btmTranslateY = ref('212.56rpx'); //input+功能面板的高度--控制切换功能面板的动画const chatHeight = ref(noActive); //对话面板的高度
const currentTime = ref('');
const keyboardHeightRpx = ref(0);
const isPanel = ref(true); //控制功能面板是否展示// 152-输入框高度 96-Bar高度 212.56-功能面板高度
let keyboardActive = ref(); //键盘激活时的高度
// 整个对话区域的高度[展示功能面板时]=手机屏幕高度-状态栏高度-Bar组件高度-输入框高度-功能面板高度
let panelActive = ref(
screenHeightRpx - barHeightRpx - 96 - 152 - 212.56 + 'rpx'
);
// 整个对话区域的默认高度=手机屏幕高度-状态栏高度-Bar组件高度-输入框高度
let noActive = ref(screenHeightRpx - barHeightRpx - 96 - 152 + 'rpx');onMounted(() => {
getCurrentTime();
uni.onKeyboardHeightChange(handleKeyboardShow);
});let keyboardHeight = ref();
const handleKeyboardShow = (event) => {
// const keyboardHeightPx = event.height;
const rpxConversionRate = 750 / screenWidth;
// 将px为单位转换为 以rpx为单位
keyboardHeightRpx.value = event.height * rpxConversionRate;
// 整个对话区域的高度[唤起键盘时]=手机屏幕高度-状态栏高度-键盘高度-Bar组件高度-输入框高度 (单位:rpx)
keyboardActive.value = screenHeightRpx - barHeightRpx - keyboardHeightRpx.value - 96 - 152 + 'rpx';if (event.height == 0) {
chatHeight.value=noActive.value
keyboardHeight.value = 0 + 'px';
} else {
chatHeight.value=keyboardActive.value;
console.log('keyboardActive',chatHeight.value)
keyboardHeight.value = event.height + 'px';
}
};// 如果唤起键盘,隐藏功能面板,需要控制css样式translateY,确保input框能够展示
watch(keyboardHeight,(newV)=>{
if(newV !== '0px'){
btmTranslateY.value = '0rpx'
isPanel.value = false;
if(!changeBtn.value ) changeBtn.value = true
}else{
btmTranslateY.value = panelHeightRpx.value+'rpx'
isPanel.value = true;
}
})const changeButton = () => {
uni.hideKeyboard(); //收起键盘
changeBtn.value = !changeBtn.value; //切换展示/关闭功能面板的图标
setTimeout(() => {
//需要加倒计时,切换的时候会自然一点
chatHeight.value = changeBtn.value ? noActive.value : panelActive.value;
// 控制bottom-container的滚动
btmTranslateY.value = changeBtn.value ? '212.56rpx' : '0rpx';
}, 100);
};//显示当前时间
const getCurrentTime = () => {
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
currentTime.value = `${hours}:${minutes}`;
};//点击“发送”触发函数
const sendMessage = () => {
if (inputMessage.value.trim() === '') return; //输入框内没有内容则停止执行
receiveMessage();
messages.value.push({
content: inputMessage.value,
isSent: true,
}); //显示信息
inputMessage.value = ''; //清空输入框
nextTick(() => { scrollToBottom(); }); //滚动到最下方
};//模拟自动回复
const receiveMessage = () => {
setTimeout(() => {
messages.value.push({
content: 'This is an auto-reply message.',
isSent: false,
});
nextTick(() => { scrollToBottom(); });
}, 300);
};//滚动到页面最下方的函数
const scrollToBottom = () => {
const query = uni.createSelectorQuery();
query.select('.message-container').boundingClientRect();
query.select('.message-container').scrollOffset();
query.exec((res) => {
const scrollOffset = res[1];
const buttonHeight = scrollOffset.scrollHeight;
scrollTop.value = buttonHeight;
});
};// 组件卸载时取消监听
onUnmounted(() => {
uni.offKeyboardHide(handleKeyboardHide);
});
</script><style>
.container {
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
background-color: #f6f6f6;
}.message-container {
height: 1440rpx;
width: 100%;
overflow-y: auto;
background-color: #f0e2b7;
/* padding-bottom: 30rpx; */
}.time {
display: inline-block;
width: 100%;
text-align: center;
font-size: 24rpx;
line-height: 34rpx;
margin-top: 66rpx;
color: #969696;
}.message-wrapper {
display: flex;
margin: 55rpx 18rpx;
/* background-color: antiquewhite; */
}.sent {
justify-content: flex-end;
/* 发送的消息靠右 */
}.received {
justify-content: flex-start;
/* 接收的消息靠左 */
}.service {
display: flex;
flex-direction: column;
}.serviceName {
margin-bottom: 5rpx;
font-size: 24rpx;
align-self: flex-start;
color: #696969;
}.message {
padding: 18rpx 28rpx;
max-width: 510rpx;
box-sizing: border-box;
/* 设置消息气泡的最大宽度 */
border-radius: 24px;
word-wrap: break-word;
/* 长单词换行 */
word-break: break-all;
/* 强制单词换行 */
background-color: #ffffff;
}.message text {
font-size: 31rpx;
color: #323232;
}.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 12rpx;
}.bottom-container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
transition: transform 0.2s ease;
}.input-container {
display: flex;
align-items: center;
gap: 14.49rpx;
height: 152rpx;
padding: 7rpx 30rpx 27rpx 30rpx;
position: relative;
background-color: #f6f6;
border-top: solid 2rpx #f2f2f7;
}.panel-container {
height: 212.56rpx;
overflow: hidden;
}#messageInput {
/* flex: 1; */
height: 68.8rpx;
width: 623rpx;
padding: 13rpx 24rpx 15rpx 24rpx;
box-sizing: border-box;
border-radius: 33rpx;
background-color: #ffffff;
}.input-container image {
width: 52rpx;
height: 52rpx;
}.options-button {
width: 100rpx;
height: 52rpx;
padding: 0;
margin-left: 14rpx;
border: 4rpx solid #000000;
border-radius: 55rpx;
margin-top: 9rpx;
box-sizing: border-box;
text-align: center;
line-height: 45rpx;
font-size: 27rpx;
}
</style>
Bar组件
<template>
<view>
<view class="topA">
<view :style=" { height: barHeight + 'rpx' }"></view>
<view class="box">
<image class="back" @click="goBack" src="/static/fonts/back.svg" />
<text class="title">{{title}}</text>
<image src="../../static/send/more.svg" class="more" mode="" ></image>
</view>
</view>
<view class="filter" :style="{ marginTop: barHeight + 'rpx' } "></view></view>
</template>
<script setup>
import { ref } from 'vue';
import {
useSystemInfoStore
} from "@/store/systemInfo.js";const systemInfo = useSystemInfoStore();
const barHeight = systemInfo.dataList.barHeightRpx;
const goBack=()=>{ uni.navigateBack() }
const props = defineProps({
title: { type: String }
});
</script><style scoped>
.filter {
height: 96rpx;
background-color: #f6f6f6;
}.topA {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 96rpx;
z-index: 500;
border-bottom: solid 1.81rpx #f2f2f2;
background-color: #f6f6f6;
}
.box {
display: flex;
justify-content: center;
position: relative;
margin: 33.81rpx 0 12.68rpx 0;
}
.back {
position: absolute;
left: 11rpx;
width: 60rpx;
height: 60rpx;
}.title {
color: #323232;
font-size: 36.23rpx;
font-weight: 500;}
/* ”更多“图片 */
.more {
position: absolute;
right: 30rpx;
top: 27rpx;
width: 30rpx;
height: 7rpx;
}
</style>
Panel组件
<template>
<view class="options-panel">
<view class="option-item" @click="selectOption('Order')" v-for="(item, index) in bottomBar" :key="index">
<view class="option-icon">
<image :src="item.pic" class=""></image>
</view>
<text class="option-text">{{ item.text }}</text>
</view>
</view>
</template><script setup>
import {
ref
} from 'vue';
const bottomBar = ref([{
text: '发送订单',
pic: '../../static/chat/icon1.svg',
},
{
text: '相册',
pic: '../../static/chat/icon2.svg',
},
{
text: '拍照',
pic: '../../static/chat/icon3.svg',
},
{
text: '表情',
pic: '../../static/chat/icon4.svg',
}]);
</script><style scoped>
.options-panel {
height: 212.56rpx;
display: flex;
justify-content: space-between;
padding: 0 61.59rpx 68.23rpx;}
.option-item {
display: flex;
flex-direction: column;
align-items: center;
}.option-icon {
display: flex;
justify-content: center;
align-items: center;
width: 103.86rpx;
height: 103.86rpx;
padding: 26.76rpx;
padding: 32.60rpx 7.24rpx;
box-sizing: border-box;
background-color: #ffffff;
border-radius: 24rpx;
/* margin-bottom: 13rpx; */
}.option-icon image {
width: 45.89rpx;
height: 45.89rpx;
}.option-text {
margin-top: 5px;
font-size: 27rpx;
color: #676767;
}
</style>
systemInfo.js
import { defineStore} from 'pinia';
import { reactive } from 'vue';
export const useSystemInfoStore = defineStore('systemInfo', () => {
let dataList = reactive({
barHeightRpx:0,
screenWidth: 0,
screenHeightRpx:0
});
const getSystemInfo = () => {
uni.getSystemInfo({
success: function(res) {
dataList.screenWidth = res.screenWidth || 0;
const rpxConversionRate = 750 / res.screenWidth;
dataList.screenHeightRpx = res.screenHeight * rpxConversionRate;
dataList.barHeightRpx = res.statusBarHeight * rpxConversionRate;
},
fail: function(err) { console.error("Failed to get system info:", err); }
});
}
return { dataList };
});