uni-app 聊天界面1

自己研究的聊天界面,在安卓真机上进行调试,软键盘和功能面板切换时有概率会闪一下,但整体上比较顺畅。

为方便测试,直接写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  };
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值