# 一、解决输入内容时键盘弹起,页面整体上移
# 1. 禁用键盘弹起时,自动上推页面
详见textarea组件的adjust-position属性: https://uniapp.dcloud.net.cn/component/textarea.html (opens new window)
在页面: /pages/chat/chat.nvue
<textarea ... :adjust-position="false"></textarea>
# 2. 重新处理底部输入框的布局
# 1. 用到监听键盘高度变化的事件
监听键盘高度变化: https://uniapp.dcloud.net.cn/api/key.html#onkeyboardheightchange (opens new window)
在页面: /pages/chat/chat.nvue
<script>
export default {
data() {
return {
...
KeyboardHeight:0, // 键盘高度
}
},
mounted() {
...
//监听键盘高度变化
uni.onKeyboardHeightChange(res=>{
console.log('键盘高度变化',res);
this.KeyboardHeight = res.height;
});
},
computed:{
chatContentStyle(){
let pbottom = this.bottomSafeAreaHeight == 0 ?
uni.upx2px(12) : this.bottomSafeAreaHeight;
let bottom = pbottom + uni.upx2px(90 + 12) + this.KeyboardHeight;
return `top:${this.fixedHeight}px;bottom:${bottom}px;`;
},
chatBottomStyle(){
let pbottom = this.bottomSafeAreaHeight == 0 ?
uni.upx2px(12) : this.bottomSafeAreaHeight;
return `padding-bottom: ${pbottom}px;bottom:${this.KeyboardHeight}px;`;
}
},
...
}
</script>
# 2. 隐藏滚动条并默认滚动到聊天底部
<!-- 聊天内容区域 -->
<scroll-view ... :show-scrollbar="false"></scroll-view>
weex(app-plus-nvue)页面滚动参考:https://weexapp.com/zh/docs/modules/dom.html#scrolltoelement (opens new window)- 在小程序MP端,使用
scroll-view组件的scroll-into-view属性,具体查看官网:https://uniapp.dcloud.net.cn/component/scroll-view.html#scroll-view (opens new window)
<template>
<view>
<!-- 导航栏 -->
...
<!-- 聊天内容区域 -->
<scroll-view ...
:scroll-into-view="scrollIntoViewId" @scroll="onScroll">
<!-- 对话部分 -->
<view ...
:id="'chat-item-'+index">
<chat-item ... :show-scrollbar="false"
ref="chatItem"></chat-item>
</view>
</scroll-view>
<!-- 底部聊天输入区域 -->
...
</view>
</template>
<script>
export default {
data() {
return {
...
KeyboardHeight:0, // 键盘高度
scrollIntoViewId: '', // 滚动到指定元素的id
...
}
},
mounted() {
...
//监听键盘高度变化
uni.onKeyboardHeightChange(res=>{
console.log('键盘高度变化',res);
this.KeyboardHeight = res.height;
if(this.KeyboardHeight){
this.chatContentToBottom();
}
});
// 页面加载完成后滚动到底部
this.$nextTick(() => {
this.chatContentToBottom();
});
},
computed:{
chatContentStyle(){
...
let bottom = pbottom + uni.upx2px(90 + 12) + this.KeyboardHeight;
// #ifdef MP
if(this.KeyboardHeight){
bottom = uni.upx2px(90 + 12 + 12) + this.KeyboardHeight;
}
// #endif
return ...;
},
chatBottomStyle(){
...
// #ifdef MP
if(this.KeyboardHeight){
pbottom = uni.upx2px(12);
}
// #endif
return `padding-bottom: ${pbottom}px;bottom:${this.KeyboardHeight}px;`;
}
},
methods: {
...,
//聊天内容滚动到底部
chatContentToBottom(){
// #ifdef APP
let chatItems = this.$refs.chatItem;
let lastIndex = chatItems.length - 1 == 0 ? 0 : chatItems.length - 1;
let last = chatItems[lastIndex];
const dom = weex.requireModule('dom');
dom.scrollToElement(last, {});
// #endif
// #ifdef MP
if (this.chatDataList.length === 0) return;
const lastIndex = this.chatDataList.length - 1;
this.scrollIntoViewId = `chat-item-${lastIndex}`;
setTimeout(() => {
this.scrollIntoViewId = '';
this.$nextTick(() => {
this.scrollIntoViewId = `chat-item-${lastIndex}`;
});
}, 100);
// #endif
},
onScroll() {
// 可添加滚动位置检测逻辑
// console.log('页面发生了滚动');
}
},
watch: {
// 监听聊天数据变化,自动滚动到底部
chatDataList: {
handler() {
this.$nextTick(() => {
this.chatContentToBottom();
});
},
deep: true
}
}
}
</script>
<style>
...
</style>
# 二、聊天页底部发送信息功能(发送普通文字)功能
在页面: /pages/chat/chat.nvue
<template>
<view>
<!-- 导航栏 -->
...
<!-- 聊天内容区域 -->
...
<!-- 底部聊天输入区域 -->
<view ...>
<!-- 左边 -->
<view ...>
<!-- 图标 -->
...
<!-- 输入框 -->
<view ...>
<textarea ...
v-model="messageValue"></textarea>
</view>
</view>
<!-- 右边 -->
<view ...>
<!-- 表情 -->
<chat-navbar-icon-button v-if="!messageValue">
...
</chat-navbar-icon-button>
<!-- 加号 -->
<chat-navbar-icon-button v-if="!messageValue">
...
</chat-navbar-icon-button>
<!-- 发送按钮 -->
<view v-if="messageValue"
class="rounded bg-success px-2 py-1 mr-4"
hover-class="bg-hover-success"
@click="sendMessage('text')">
<text class="text-white font">发送</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
...
messageValue:'', //输入的消息内容
...
}
},
mounted() {
...
},
computed:{
...
},
methods: {
// 发消息
sendMessage(msgType){
console.log('发送信息',msgType);
let msg = {
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
user_id: 2,
chat_time: (new Date()).getTime(),
data: '',
type:msgType, //image,video
isremove:false,
};
switch (msgType){
case 'text':
msg.data = this.messageValue;
break;
}
this.chatDataList.push(msg);
// 清空及滚动到底部
if(msgType == 'text') this.messageValue = '';
this.chatContentToBottom();
},
...
},
...
}
</script>
<style>
...
</style>
# 三、底部输入区域加号扩展菜单功能
# 1. 先确定好扩展菜单的高度和位置
- 在组件:
/components/chat-tooltip/chat-tooltip.vue中
methods: {
hide() {
...
// 通知父组件或者页面弹出组件隐藏了
this.$emit('hideTooltip');
}
}
- 在页面:
/pages/chat/chat.nvue中
<template>
<view>
<!-- 导航栏 -->
...
<!-- 聊天内容区域 -->
...
<!-- 底部聊天输入区域 -->
<view ...>
<!-- 左边 -->
...
<!-- 右边 -->
<view ...>
<!-- 表情 -->
...
<!-- 加号 -->
<chat-navbar-icon-button ...
@click="openPlus">
...
</chat-navbar-icon-button>
<!-- 发送按钮 -->
...
</view>
</view>
<!-- 加号扩展菜单 -->
<chat-tooltip ref="tooltipPlus" :mask="true"
:maskTransparent="true" :isBottom="true"
:tooltipWidth="750"
:tooltipHeight="tooltipHeight"
transformOrigin="center bottom"
tooltipClass="bg-light border-0 rounded-0"
@hideTooltip="hideTooltip">
<view class="bg-warning border-top border-light-secondary">
<swiper :indicator-dots="true" :duration="1000"
:style="tooltipPlusMenuStyle">
<!-- 第一页 -->
<swiper-item>
<view style="background-color: red;"
:style="swiperItemStyle">
<text>1</text>
</view>
</swiper-item>
</swiper>
</view>
</chat-tooltip>
</view>
</template>
<script>
export default {
data() {
return {
tooltipHeight:600, // 加号弹出菜单高度rpx
...
}
},
mounted() {
...
},
computed:{
...,
// 加号菜单样式
tooltipPlusMenuStyle(){
let pbottom = 0;
let height = uni.upx2px(this.tooltipHeight - 1) - pbottom;
// #ifdef APP || MP
pbottom = this.bottomSafeAreaHeight;
// #endif
return `padding-bottom:${pbottom}px;height:${height}px;`;
},
//加号菜单每页滑动样式
swiperItemStyle(){
let pbottom = 0;
let height = uni.upx2px(this.tooltipHeight - 1) - pbottom;
return `padding-bottom:${pbottom}px;height:${height}px;`;
},
},
methods: {
//点击加号
openPlus(){
console.log('点击了加号');
this.$refs.tooltipPlus.show();
// #ifdef APP || MP || H5
this.KeyboardHeight = uni.upx2px(this.tooltipHeight);
this.chatContentToBottom();
// #endif
},
// 弹出框隐藏了
hideTooltip(){
console.log('弹出框隐藏了');
// #ifdef APP || MP || H5
this.KeyboardHeight = 0;
this.chatContentToBottom();
// #endif
},
...
},
...
}
</script>
<style>
...
</style>
# 2. 加号扩展菜单内容布局
在页面: /pages/chat/chat.nvue中
<template>
<view>
<!-- 导航栏 -->
...
<!-- 聊天内容区域 -->
...
<!-- 底部聊天输入区域 -->
...
<!-- 弹出菜单 -->
<chat-tooltip ...>
<view ...>
<!-- 动态设置指示点 -->
<!-- 动态设置指示点 -->
<swiper :indicator-dots="pageCount > 1" :duration="1000"
:style="tooltipPlusMenuStyle">
<!-- 动态生成分页 -->
<swiper-item v-for="(page, index) in groupedPlusMenus" :key="index">
<view class="flex flex-row justify-start flex-wrap"
:style="swiperItemStyle">
<!-- 每页显示8个 -->
<view class="col-3 flex flex-column
align-center justify-center"
v-for="(item, itemIndex) in page" :key="itemIndex"
style="height: 260rpx;align-items: center;
justify-content:center;"
@click="swiperItemClick(item, itemIndex)">
<!-- 图标 -->
<view class="bg-white rounded-lg
flex flex-row align-center justify-center mb-2"
style="width: 120rpx;height: 120rpx;">
<!-- 包含反斜杠\u的是自定义图标 -->
<text v-if="item.iconType == 'custom'"
class="iconfont"
style="font-size: 26px;color:#222222">{{item.icon}}</text>
<!-- 否则是uView图标 -->
<u-icon v-if="item.iconType == 'uview'"
:name="item.icon" size="26" color="#222222"></u-icon>
</view>
<!-- 文字 -->
<text class="font-sm text-light-muted">{{item.name}}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
</chat-tooltip>
</view>
</template>
<script>
export default {
data() {
return {
...,
plusMenus:[ //加号扩展菜单
{ name:"照片", icon:"photo",iconType:"uview", eventType:"photo"},
{ name:"位置", icon:"map", iconType:"uview",eventType:"map"},
{ name:"拍摄", icon:"\ue62c",iconType:"custom", eventType:"camera"},
{ name:"我的名片", icon:"\ue69d",iconType:"custom", eventType:"mingpian"},
{ name:"视频", icon:"\ue66d",iconType:"custom", eventType:"video"},
// { name:"拍摄", icon:"\ue62c",iconType:"custom", eventType:""},
// { name:"我的名片", icon:"\ue69d",iconType:"custom", eventType:""},
// { name:"视频", icon:"\ue66d",iconType:"custom", eventType:""},
// { name:"拍摄", icon:"\ue62c",iconType:"custom", eventType:""},
// { name:"我的名片", icon:"\ue69d",iconType:"custom", eventType:""},
// { name:"视频", icon:"\ue66d",iconType:"custom", eventType:""},
],
...,
}
},
...,
computed:{
...,
// 新增分页计算属性
groupedPlusMenus() {
const perPage = 8; // 每页8个
const result = [];
// 将数组按每页8个分组
for (let i = 0; i < this.plusMenus.length; i += perPage) {
result.push(this.plusMenus.slice(i, i + perPage));
}
return result;
},
// 计算总页数
pageCount() {
return Math.ceil(this.plusMenus.length / 8);
}
},
methods: {
//点击加号菜单的某一项
swiperItemClick(item, itemIndex){
console.log('点击加号菜单的某一项',item.eventType);
switch(item.eventType){
case 'photo':
break;
case 'map':
break;
case 'camera':
break;
case 'mingpian':
break;
case 'video':
break;
}
},
...,
},
...
}
</script>
<style>
...
</style>
# 四、加号和键盘切换问题(主要针对app端)
针对app端:出现加号和输入内容时候键盘的bug,主要原因是我们在之前开发的时候没有考虑到多种消息发送情况的各种判定,因此我们要分析在发送消息的时候,有哪些情况,很显然,我们发送消息的时候有四种情况:1. 发送文本消息 2. 点击加号发送扩展菜单的内容 3. 发送表情 4. 发送语音消息 5. 其它情况。
在页面: /pages/chat/chat.nvue 中
<template>
<view>
<!-- 导航栏 -->
...
<!-- 聊天内容区域 -->
...
<!-- 针对app端点击聊天区域隐藏加号扩展菜单 -->
<!-- #ifdef APP -->
<view v-if="sendMessageMode == 'plus'" @click="scrollViewClick"
class="position-fixed left-0 right-0 bg-success"
:style="chatContentStyle"></view>
<!-- #endif -->
<!-- 底部聊天输入区域 -->
<view ...>
<!-- 左边 -->
<view ...>
<!-- 图标 -->
...
<!-- 输入框 -->
<view ...>
<textarea ...
@focus="textareaFocus"></textarea>
</view>
</view>
<!-- 右边 -->
...
</view>
<!-- 弹出菜单 -->
...
</view>
</template>
<script>
export default {
data() {
return {
...,
sendMessageMode:"text", //发送消息的情况:文本|加号|表情|语音
// #ifdef MP || H5
chatTooltipMask:true,
// #endif
// #ifdef APP
chatTooltipMask:false,
// #endif
...
}
},
mounted() {
...
// 监听键盘高度变化
uni.onKeyboardHeightChange(res=>{
console.log('键盘高度变化',res);
// #ifdef MP || H5
this.KeyboardHeight = res.height;
// #endif
// #ifdef APP
console.log('此时的输入模式',this.sendMessageMode);
if(this.sendMessageMode != 'plus'){
this.KeyboardHeight = res.height;
}
// #endif
if(this.KeyboardHeight){
this.chatContentToBottom();
}
});
...
},
computed:{
...
},
methods: {
...,
// 点击聊天区域
scrollViewClick(){
// #ifdef APP
console.log('点击聊天区域');
this.KeyboardHeight = 0;
uni.hideKeyboard();
this.$refs.tooltipPlus.hide();
this.sendMessageMode = "text";
// #endif
},
// 文本输入聚焦
textareaFocus(){
// #ifdef APP
this.sendMessageMode = "text";
console.log('触发文本输入聚焦事件模式是',this.sendMessageMode);
// #endif
},
//点击了加号
openPlus(){
console.log('点击了加号');
// #ifdef APP
// 改变发送模式,收起键盘
this.sendMessageMode = "plus";
uni.hideKeyboard();
// #endif
...
},
...
},
watch:{
...,
sendMessageMode(newVal,oldVal){
// #ifdef APP
console.log('监听发送模式',newVal);
if(newVal != 'plus'){
this.$refs.tooltipPlus.hide();
}
// #endif
}
},
}
</script>
<style>
...
</style>
# 五、表情包开发及发送表情包
# 1. 表情包开发
说明:
- gif动画表情包,大家可以在网上自行下载你喜欢的,如 https://www.fabiaoqing.com/ (opens new window),然后放在本地文件static/emoji文件夹下或者放在阿里云oss云存储上,然后就可以引入使用了
- emoji 文字表情包,可以查看这个网站选择你喜欢的 https://www.emojiall.com/zh-hans/all-emojis (opens new window)
在页面:/pages/chat/chat.nvue中
<template>
<view>
<!-- 导航栏 -->
...
<!-- 聊天内容区域 -->
...
<!-- 针对我们的app端点击聊天区域授权加号扩展菜单 -->
<!-- #ifdef APP -->
<view v-if="sendMessageMode == 'plus' || sendMessageMode == 'icon'"
class="position-fixed left-0 right-0"
:style="chatContentStyle"
@click="scrollViewClick"></view>
<!-- #endif -->
<!-- 底部聊天输入区域 -->
<view ...>
<!-- 左边 -->
...
<!-- 右边 -->
<view class="flex align-center">
<!-- 表情 -->
<chat-navbar-icon-button v-if="!messageValue"
@click="openIcon">
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<!-- 加号 -->
...
<!-- 发送按钮 -->
...
</view>
</view>
<!-- 弹出菜单 -->
<chat-tooltip ...>
<view ...>
<swiper ...>
<!-- 动态分页 -->
<swiper-item ...>
<view ...>
<!-- 每页显示8个 -->
<view ...>
<!-- 图标 -->
<view ...>
<!-- 图片类型图标 -->
<image v-if="item.iconType === 'image'" :src="item.icon"
style="width:100%;height:100%;" mode="aspectFit" />
<!-- emoji类型图标 -->
<text v-if="item.iconType === 'emoji'"
style="font-size: 40px;color: #222222;">
{{item.icon}}
</text>
<!-- 自定义图标 -->
...
<!-- uviewUi框架的图标 -->
...
</view>
<!-- 名称 -->
...
</view>
</view>
</swiper-item>
</swiper>
</view>
</chat-tooltip>
</view>
</template>
<script>
export default {
data() {
return {
...
iconMenus:[
// { name: "微笑", icon: "/static/tabbar/index.png",
// iconType: "image", eventType: "smile" },
{ name: "嘿嘿", icon: "😀",
iconType: "emoji", eventType: "smile" },
{ name: "嗯,哼", icon: "https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/iconMenus/en.gif",
iconType: "image", eventType: "enheng" },
],
...,
}
},
mounted() {
...
// 监听键盘高度变化
uni.onKeyboardHeightChange(res=>{
...
// #ifdef H5 || MP
...
// #endif
// #ifdef APP
...
if(this.sendMessageMode != 'plus' && this.sendMessageMode != 'icon'){
this.KeyboardHeight = res.height;
}
// #endif
...
});
...
},
computed:{
...,
// 加号菜单分页
groupedPlusMenus(){
const perPage = 8; // 每页8个
const result = [];
// 将数组plusMenus每页8个分组
for(let i=0;i<this.tooltipPlusMenusOrIconMenus.length; i += perPage){
result.push(this.tooltipPlusMenusOrIconMenus.slice(i, i + perPage))
}
return result;
},
//计算总页数
pageCount(){
return Math.ceil(this.tooltipPlusMenusOrIconMenus.length / 8);
},
// 扩展菜单或表情包数据源
tooltipPlusMenusOrIconMenus(){
if(this.sendMessageMode == 'plus' || this.sendMessageMode == 'icon'){
return this[`${this.sendMessageMode}Menus`]
}
return [];
},
},
methods: {
//点击加号扩展菜单的某一项
swiperItemClick(item,itemIndex){
if (this.sendMessageMode === 'icon') {
// 表情包点击处理
// this.messageValue += `[${item.name}]`; // 在输入框中添加表情标记
console.log('点击了表情包');
}else {
// 加号菜单点击处理
console.log('点击加号扩展菜单的某一项',item.eventType);
...
}
},
//点击了笑脸
openIcon(){
console.log('点击了笑脸');
// #ifdef APP
this.sendMessageMode = "icon";
uni.hideKeyboard();
// #endif
// #ifdef H5 || MP
this.sendMessageMode = 'icon';
// #endif
this.$refs.tooltipPlus.show();
// #ifdef APP || MP || H5
this.KeyboardHeight = uni.upx2px(this.tooltipHeight);
this.chatContentToBottom();
// #endif
},
...,
},
watch:{
...,
sendMessageMode(newVal,oldVal){
// #ifdef APP
console.log('监听发送模式',newVal);
if(newVal != 'plus' && newVal != 'icon'){
this.$refs.tooltipPlus.hide();
}
// #endif
},
},
}
</script>
<style>
...
</style>
# 2. 发送表情包功能实现
# ① 在页面 pages/chat/chat.nvue 中
<template>
<view>
<!-- 导航栏 -->
...
<!-- 聊天内容区域 -->
...
<!-- 底部聊天输入区域 -->
...
<!-- 弹出菜单 -->
...
</view>
</template>
<script>
export default {
data() {
return {
...,
iconMenus:[
{ name:"微笑", icon:"/static/tabbar/index.png",
iconType:"image", eventType:"smile" },
{ name:"嘿嘿", icon:"😀",
iconType:"emoji", eventType:"heihei" },
{ name: "嗯,哼", icon: "https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/iconMenus/en.gif", iconType: "image", eventType: "enheng" },
],
...,
}
},
mounted() {
...
},
computed:{
...
},
methods: {
//点击加号扩展菜单的某一项
swiperItemClick(item,itemIndex){
if (this.sendMessageMode === 'icon') {
// 表情包点击处理
// this.messageValue += `[${item.name}]`; // 在输入框中添加表情标记
console.log('点击了表情包');
this.sendMessage('iconMenus', item);
}else {
console.log('点击加号扩展菜单的某一项',item.eventType);
switch (item.eventType){
case 'phote':
break;
case 'map':
break;
case 'camera':
break;
case 'mingpian':
break;
case 'video':
break;
}
}
},
...,
//发送消息
sendMessage(msgType,option={}){
...
console.log('额外参数',option);
...
switch (msgType){
case 'text':
...
break;
case 'iconMenus':
msg.data = option.icon;
msg.dataType = option.iconType;
break;
}
...
},
...
},
watch:{
...
},
}
</script>
<style>
...
</style>
# ② 调整组件 /components/chat-item/chat-item.vue 显示表情包
<template>
<view>
<!-- 时间 -->
...
<!-- 撤回消息 -->
...
<!-- 聊天内容 -->
<view v-else ...>
<!-- 好友 -->
<!-- 头像 -->
<u--image ...></u--image>
<!-- 气泡 -->
<!-- 三角形 -->
<text v-if="!isMe && needQipaoClass"
class="iconfont font-md chat-left-icon"></text>
<!-- 内容 -->
<view class="p-2 rounded"
style="max-width: 500rpx;"
:class="[!isMe ? 'ml-1':'mr-1',`chatItem${index}`,
!isMe && needQipaoClass ? 'chat-left-content-bg pt-2' : 'pt-0',
isMe && needQipaoClass ? 'chat-right-content-bg pt-2' : 'pt-0',]"
:ref="'chatItem' + index"
@longpress="onLongpress($event,index,item)">
<!-- 情况1 -->
<view v-if="item.dataType && item.dataType == 'image'">
<u--image :src="item.data" mode="widthFix"
width="200rpx" height="200rpx" radius="10rpx"></u--image>
</view>
<!-- 文字 -->
<text v-else
class="font" style="text-align: justify;">
{{item.data}}
</text>
</view>
<!-- 我 -->
<text v-if="isMe && needQipaoClass"
class="iconfont font-md chat-right-icon"></text>
<u--image ...></u--image>
</view>
<!-- 弹出菜单 -->
...
</view>
</template>
<script>
...
export default{
...,
computed:{
...,
// 需要气泡样式
needQipaoClass(){
return this.item.type === 'text' ||
this.item.type === 'audio' ||
(this.item.type === 'iconMenus' && this.item.dataType
&& this.item.dataType == 'emoji');
},
},
}
</script>
# 六、调整在H5端让页面和微信更好的融合
有同学提出,说在H5端的页面,90%以上的时候,是通过微信传播然后通过分享页面链接打开的,希望调整一下页面的样式,让页面和微信更加融合。
# 1. 先解决一个bug
在苹果手机上在微信打开H5端页面,
复制撤回弹出菜单没有完全漏出来 解决:
- 来到页面
/pages/chat/chat.nvue, 把<scroll-view>的class中的px-3去掉; - 来到组件
/components/chat-item/chat-item.vue在根view上 加上class="px-3"
# 2. 消息页(根据自己的爱好调整)
- 在
/common/mixins/tool.js中写几个方法,先要知道当前运行的环境是不是H5端,并且是在微信打开的, 如果是在手机浏览器:QQ浏览器、UC浏览器等打开,则不做什么改变,界面和小程序、app一样。
export default {
methods: {
/**
* 判断当前H5环境是否在微信内置浏览器打开
* @returns {Boolean} true:微信环境 false:非微信环境
*/
isWeixinBrowser() {
// 非H5环境直接返回false
if (process.env.VUE_APP_PLATFORM !== 'h5') return false
// 获取浏览器userAgent并转换为小写
const ua = navigator.userAgent.toLowerCase()
// 检测微信特有标识
return ua.includes('micromessenger')
},
/**
* 扩展方法:获取详细的H5环境信息
* @returns {Object} 环境信息对象
*/
getBrowserEnv() {
if (process.env.VUE_APP_PLATFORM !== 'h5') {
return {
inWeixin: false,
inQQ: false,
inWeibo: false
}
}
const ua = navigator.userAgent.toLowerCase()
return {
inWeixin: ua.includes('micromessenger'), // 微信浏览器
inQQ: ua.includes('qq/'), // QQ内置浏览器
inWeibo: ua.includes('weibo'), // 微博内置浏览器
inAlipay: ua.includes('alipay'), // 支付宝浏览器
inUC: ua.includes('ucbrowser'), // UC浏览器
isMobile: /mobile|android|iphone|ipad/.test(ua) // 是否移动设备
}
},
...
}
}
- 在
/common/css/common.nvue.vue.css公共样式添加一个置顶样式的背景色
这个背景色和微信的颜色一致,便于融合置顶部分
.bg-zhiding{
background-color: #EDEDED;
}
- 在页面 :
/pages/xiaoxi/xiaoxi.nvue
<!-- 导航栏 -->
<chat-navbar title="消息(100)" :fixed="true"
:h5WeiXinNeedNavbar="false"></chat-navbar>
- 在组件
/components/chat-navbar/chat-navbar.vue
<template>
<view>
<!-- 导航栏 -->
<view :class="[fixed ? 'fixed-top' : '',weixinClass,navbarClass]"
v-if="needNavbar">
...
</view>
<!-- 占位符:占用 状态栏 + 导航栏的高度 -->
<view :style="fixedHeightStyle" v-if="fixed && needNavbar"></view>
<!-- 导航栏点击加号的弹出菜单 -->
...
</view>
</template>
<script>
import toolJs from '@/common/mixins/tool.js';
export default {
...,
mixins:[toolJs],
props:{
...,
// h5端在微信打开是否需要导航栏
h5WeiXinNeedNavbar:{
type:Boolean,
default:false
}
},
data() {
return {
weixinClass:'', // 微信上导航栏颜色
needNavbar:true, // 是否需要导航栏
...
}
},
mounted() {
...
if (this.isWeixinBrowser()) {
console.log('运行在微信内置浏览器')
this.needNavbar = this.h5WeiXinNeedNavbar ? true : false;
this.weixinClass = this.h5WeiXinNeedNavbar ? 'bg-zhiding': '';
}
// 获取详细环境信息
const env = this.getBrowserEnv()
console.log('环境信息:', env)
},
...
}
</script>
- 在组件
/components/chat-chatlist/chat-chatlist.vue
<template>
<view ... :class="bgClass">
...
</view>
</template>
<script>
import toolJs from '@/common/mixins/tool.js';
export default{
...
mixins:[toolJs],
...,
computed:{
bgClass(){
/*
if(this.item.isZhiding){
if(this.isWeixinBrowser()){
return 'bg-zhiding'
}else{
return 'bg-light';
}
}else{
return 'bg-white';
}
*/
return this.item.isZhiding ? this.isWeixinBrowser() ?
'bg-zhiding' : 'bg-light' : 'bg-white';
}
}
}
</script>
# 3. 聊天页融合到微信(根据自己喜好设置)
在页面 pages/chat/chat.nvue
<template>
<view>
<!-- 导航栏 -->
<chat-navbar ... :h5WeiXinNeedNavbar="false">
...
</chat-navbar>
<!-- 聊天内容区域 -->
...
<!-- 底部聊天输入区域 -->
...
<!-- 弹出菜单 -->
...
</view>
</template>
<script>
import toolJs from '@/common/mixins/tool.js';
export default {
mixins:[toolJs],
data() {
return {
h5WeiXinNeedNavbar:false, // h5端微信上是否需要导航栏
...
}
},
...
computed:{
chatContentStyle(){
...
let bottom = pbottom + uni.upx2px(90 + 12) + this.KeyboardHeight;
// 如果是H5端微信打开页面
if(this.isWeixinBrowser() && !this.h5WeiXinNeedNavbar){
this.fixedHeight = this.statusBarHeight + 0;
uni.setNavigationBarTitle({
title:'阿祖'
});
}
// #ifdef APP || MP
...
// #endif
return ...;
},
...
},
...
}
</script>
# 七、【选修】调整表情包排布(把emoji表情放在第一页,emoji和文字一起发送)
在页面 pages/chat/chat.nvue 完整代码
# 1. 调整前的完整代码
<template>
<view>
<!-- 导航栏 -->
<chat-navbar title="聊天" :fixed="true"
:showPlus="false" :showUser="false"
:showBack="true" navbarClass="bg-light"
:h5WeiXinNeedNavbar="h5WeiXinNeedNavbar">
<chat-navbar-icon-button slot="right"
@click="openMore" >
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
</chat-navbar>
<!-- 聊天内容区域 -->
<scroll-view scroll-y class="bg-light position-fixed left-0 right-0"
:style="chatContentStyle" :show-scrollbar="false" @scroll="onScroll"
:scroll-into-view="scrollIntoViewId" >
<!-- 对话部分 -->
<view v-for="(item,index) in chatDataList" :key="index"
:id="'chat-item-'+index">
<chat-item :item="item" :index="index"
:prevTime="index>0 ? chatDataList[index-1].chat_time : 0"
ref="chatItem"></chat-item>
</view>
</scroll-view>
<!-- 针对我们的app端点击聊天区域授权加号扩展菜单 -->
<!-- #ifdef APP -->
<view v-if="sendMessageMode == 'plus' || sendMessageMode == 'icon'"
class="position-fixed left-0 right-0"
:style="chatContentStyle"
@click="scrollViewClick"></view>
<!-- #endif -->
<!-- 底部聊天输入区域 -->
<view class="position-fixed bottom-0 border-top
flex flex-row align-center justify-between"
style="background-color: #f7f7f7;width: 750rpx;
min-height: 90rpx;max-height: 320rpx;
padding-top: 12rpx;"
:style="chatBottomStyle">
<!-- 左边 -->
<view class="flex align-center">
<!-- 图标 -->
<chat-navbar-icon-button>
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<!-- 输入框 -->
<view class="flex align-center font-sm
bg-white px-2 py-1 border rounded">
<textarea fixed auto-height :maxlength="-1"
style="width: 440rpx;min-height: 60rpx;
max-height: 274rpx;overflow-y: scroll;
text-align: justify;"
:adjust-position="false"
v-model="messageValue"
@focus="textareaFocus"></textarea>
</view>
</view>
<!-- 右边 -->
<view class="flex align-center">
<!-- 表情 -->
<chat-navbar-icon-button v-if="!messageValue"
@click="openIcon">
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<!-- 加号 -->
<chat-navbar-icon-button v-if="!messageValue"
@click="openPlus">
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<!-- 发送按钮 -->
<view v-if="messageValue"
class="rounded bg-success px-2 py-1 mr-4"
hover-class="bg-hover-success"
@click="sendMessage('text')">
<text class="font text-white">发送</text>
</view>
</view>
</view>
<!-- 弹出菜单 -->
<chat-tooltip ref="tooltipPlus" :mask="chatTooltipMask"
:maskTransparent="true" :isBottom="true"
:tooltipWidth="750"
:tooltipHeight="tooltipHeight"
transformOrigin = "center bottom"
tooltipClass ="bg-light border-0 rounded-0"
@hideTooltip="hideTooltip">
<view class="border-top border-light-secondary">
<swiper :indicator-dots="pageCount > 1" :duration="1000"
:style="tooltipPlusMenuStyle">
<!-- 动态分页 -->
<swiper-item v-for="(page,index) in groupedPlusMenus" :key="index">
<view class="flex flex-row justify-start flex-wrap"
:style="swiperItemStyle">
<!-- 每页显示8个 -->
<view class="col-3 flex flex-column justify-center align-center"
style="height: 260rpx;align-items: center;
justify-content: center;"
v-for="(item,itemIndex) in page" :key="itemIndex"
@click="swiperItemClick(item,itemIndex)">
<!-- 图标 -->
<view class="bg-white rounded-lg
flex flex-row align-center justify-center mb-2"
style="width:120rpx;height: 120rpx;">
<!-- 图片 -->
<!-- <image v-if="item.iconType == 'image'"
:src="item.icon" style="width: 100%;height:100%"
mode="aspectFit"></image> -->
<u--image
:src="item.icon" v-if="item.iconType == 'image'"
mode="aspectFit"
width="120rpx" height="120rpx"
radius="0rpx"></u--image>
<!-- emoji类型 -->
<text v-if="item.iconType == 'emoji'"
style="font-size: 40px;color: #222222;">{{item.icon}}</text>
<!-- 自定义图标 -->
<text v-if="item.iconType == 'custom'"
class="iconfont"
style="font-size: 26px;color: #222222;">{{item.icon}}</text>
<!-- uviewUi框架的图标 -->
<u-icon v-if="item.iconType == 'uview'"
:name="item.icon" color="#222222" size="26"></u-icon>
</view>
<!-- 名称 -->
<text class="font-sm text-light-muted">{{item.name}}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
</chat-tooltip>
</view>
</template>
<script>
import toolJs from '@/common/mixins/tool.js';
export default {
mixins:[toolJs],
data() {
return {
h5WeiXinNeedNavbar:false, // h5端微信上是否需要导航栏
statusBarHeight:0,//状态栏高度动态计算
fixedHeight:0, //占位:状态栏+导航栏
bottomSafeAreaHeight:0, // 底部安全距离
KeyboardHeight:0, //键盘高度
scrollIntoViewId:'', // 滚动到指定的元素id
messageValue:'', // 发送的内容信息
tooltipHeight:600, // 加号弹出菜单的高度rpx
sendMessageMode:"text",//发送消息的情况:文字|语音|加号|表情
// #ifdef MP || H5
chatTooltipMask:true,
// #endif
// #ifdef APP
chatTooltipMask:false,
// #endif
iconMenus:[
{ name:"微笑", icon:"/static/tabbar/index.png",
iconType:"image", eventType:"smile" },
{ name:"嘿嘿", icon:"😀",
iconType:"emoji", eventType:"heihei" },
{ name: "嗯,哼", icon: "https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/iconMenus/en.gif", iconType: "image", eventType: "enheng" },
],
plusMenus:[ // 加号扩展菜单栏目
{ name:"照片", icon:"photo", iconType:"uview", eventType:"photo" },
{ name:"位置", icon:"map", iconType:"uview", eventType:"map" },
{ name:"拍摄", icon:"\ue62c", iconType:"custom", eventType:"camera" },
{ name:"我的名片", icon:"\ue69d", iconType:"custom", eventType:"mingpian" },
{ name:"视频", icon:"\ue66d", iconType:"custom", eventType:"video" },
// { name:"拍摄", icon:"\ue62c", iconType:"custom", eventType:"camera" },
// { name:"我的名片", icon:"\ue69d", iconType:"custom", eventType:"mingpian" },
// { name:"视频", icon:"\ue66d", iconType:"custom", eventType:"video" },
// { name:"拍摄", icon:"\ue62c", iconType:"custom", eventType:"camera" },
// { name:"我的名片", icon:"\ue69d", iconType:"custom", eventType:"mingpian" },
// { name:"视频", icon:"\ue66d", iconType:"custom", eventType:"video" },
],
chatDataList:[
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148439,
data: '老师你好,我想咨询一下本季课程,如果我不学习上一个季度,可以直接学习本季度吗?',
user_id: 1,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
chat_time: 1750148449,
data: '同学你好,如果不学习上一个季度课程,如果你有vue的基础和js的基础知识,也可以学习本季度课程',
user_id: 2,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148759,
data: '好的,我了解了,谢谢老师',
user_id: 1,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
chat_time: 1750148859,
data: '不用谢',
user_id: 2,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148879,
data: 'ok',
user_id: 1,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148879,
data: '哈哈哈',
user_id: 1,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148879,
data: '嗯啦',
user_id: 1,
type:'text', //image,video
isremove:false,
},
],
}
},
mounted() {
let info = uni.getSystemInfoSync();
this.statusBarHeight = info.statusBarHeight;
this.fixedHeight = this.statusBarHeight + uni.upx2px(90);
this.bottomSafeAreaHeight = info.safeAreaInsets.bottom;
// 监听键盘高度变化
uni.onKeyboardHeightChange(res=>{
console.log('键盘高度变化',res);
// #ifdef H5 || MP
this.KeyboardHeight = res.height;
// #endif
// #ifdef APP
console.log('此时的输入模式', this.sendMessageMode);
if(this.sendMessageMode != 'plus' && this.sendMessageMode != 'icon'){
this.KeyboardHeight = res.height;
}
// #endif
if(this.KeyboardHeight){
this.chatContentToBottom();
}
});
// 页面加载完了之后就应该滚动到底部
this.$nextTick(()=>{
this.chatContentToBottom();
});
},
computed:{
chatContentStyle(){
let pbottom = this.bottomSafeAreaHeight == 0 ?
uni.upx2px(12) : this.bottomSafeAreaHeight;
let bottom = pbottom + uni.upx2px(90 + 12) + this.KeyboardHeight;
//如果是h5端用微信打开并且不需要导航栏的时候
if(this.isWeixinBrowser() && !this.h5WeiXinNeedNavbar){
this.fixedHeight = this.statusBarHeight + 0;
uni.setNavigationBarTitle({
title:'阿祖'
});
}
// #ifdef APP || MP
if(this.KeyboardHeight){
bottom = uni.upx2px(90 + 12 + 12) + this.KeyboardHeight;
}
// #endif
return `top:${this.fixedHeight}px;bottom:${bottom}px;`;
},
chatBottomStyle(){
let pbottom = this.bottomSafeAreaHeight == 0 ?
uni.upx2px(12) : this.bottomSafeAreaHeight;
// #ifdef APP || MP
if(this.KeyboardHeight){
pbottom = uni.upx2px(12);
}
// #endif
return `padding-bottom: ${pbottom}px;bottom:${this.KeyboardHeight}px;`;
},
//加号菜单每页的滑动样式
tooltipPlusMenuStyle(){
let pbottom = 0;
let height = uni.upx2px(this.tooltipHeight - 1) - pbottom;
// #ifdef APP || MP
pbottom = this.bottomSafeAreaHeight;
// #endif
return `padding-bottom:${pbottom}px;height:${height}px;`;
},
//加号菜单每页的布局样式
swiperItemStyle(){
let pbottom = 0;
let height = uni.upx2px(this.tooltipHeight - 1) - pbottom;
return `padding-bottom:${pbottom}px;height:${height}px;`;
},
// 加号菜单分页
groupedPlusMenus(){
const perPage = 8; // 每页8个
const result = [];
// 将数组plusMenus或者iconMenus每页8个分组
for(let i=0;i<this.tooltipPlusMenusOrIconMenus.length; i += perPage){
result.push(this.tooltipPlusMenusOrIconMenus.slice(i, i + perPage))
}
return result;
},
//计算总页数
pageCount(){
return Math.ceil(this.tooltipPlusMenusOrIconMenus.length / 8);
},
// 扩展菜单或者表情包数据源
tooltipPlusMenusOrIconMenus(){
if(this.sendMessageMode == 'plus' || this.sendMessageMode == 'icon'){
return this[`${this.sendMessageMode}Menus`]
}
return [];
}
},
methods: {
//点击加号扩展菜单的某一项
swiperItemClick(item,itemIndex){
if(this.sendMessageMode == 'icon'){
console.log('点击了表情包里面的某个表情');
// this.messageValue += `[${item.name}]`;
this.sendMessage('iconMenus',item)
}else{
console.log('点击加号扩展菜单的某一项',item.eventType);
switch (item.eventType){
case 'phote':
break;
case 'map':
break;
case 'camera':
break;
case 'mingpian':
break;
case 'video':
break;
}
}
},
//点击聊天区域
scrollViewClick(){
// #ifdef APP
console.log('点击聊天区域');
this.KeyboardHeight = 0;
uni.hideKeyboard();
this.$refs.tooltipPlus.hide();
this.sendMessageMode = "text";
// #endif
},
// 文本输入框聚焦
textareaFocus(){
// #ifdef APP
this.sendMessageMode = "text";
// #endif
},
//点击笑脸图标
openIcon(){
console.log('点击了笑脸');
// #ifdef APP
this.sendMessageMode = "icon";
uni.hideKeyboard();
// #endif
// #ifdef H5 || MP
this.sendMessageMode = "icon";
// #endif
this.$refs.tooltipPlus.show();
// #ifdef APP || MP || H5
this.KeyboardHeight = uni.upx2px(this.tooltipHeight);
this.chatContentToBottom();
// #endif
},
//点击了加号
openPlus(){
console.log('点击了加号');
// #ifdef APP
this.sendMessageMode = "plus";
uni.hideKeyboard();
// #endif
// #ifdef H5 || MP
this.sendMessageMode = "plus";
// #endif
this.$refs.tooltipPlus.show();
// #ifdef APP || MP || H5
this.KeyboardHeight = uni.upx2px(this.tooltipHeight);
this.chatContentToBottom();
// #endif
},
// 弹出框隐藏了
hideTooltip(){
console.log('弹出框隐藏了');
// #ifdef APP || MP || H5
this.KeyboardHeight = 0;
this.chatContentToBottom();
// #endif
},
//发送消息
sendMessage(msgType, option = {}){
console.log('发送消息',msgType);
let msg = {
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
user_id: 2,
chat_time: (new Date()).getTime(),
data: '',
type:msgType, //image,video
isremove:false,
};
switch (msgType){
case 'text':
msg.data = this.messageValue;
break;
case 'iconMenus':
console.log('iconMenus的数据',option);
msg.data = option.icon;
msg.dataType = option.iconType;
break;
}
this.chatDataList.push(msg);
// 清空发送的内容然后还要滚动到底部
if(msgType == 'text') this.messageValue = '';
this.chatContentToBottom();
},
openMore(){
console.log('点击了三个点图标');
},
//聊天内容滚到到底部
chatContentToBottom(){
// #ifdef APP
let chatItems = this.$refs.chatItem;
let lastIndex = chatItems.length - 1 == 0 ? 0 : chatItems.length - 1;
let last = chatItems[lastIndex];
const dom = weex.requireModule('dom');
dom.scrollToElement(last, {});
// #endif
// #ifdef MP || H5
if(this.chatDataList.length == 0) return;
const lastIndex = this.chatDataList.length - 1;
this.scrollIntoViewId = `chat-item-${lastIndex}`;
setTimeout(()=>{
this.scrollIntoViewId = '';
this.$nextTick(()=>{
this.scrollIntoViewId = `chat-item-${lastIndex}`;
});
},100)
// #endif
},
onScroll(){
console.log('页面发生了滚动');
}
},
watch:{
// 监听聊天记录数据变化,自动滚动到底部
chatDataList:{
handler(){
this.$nextTick(()=>{
this.chatContentToBottom();
});
},
deep:true
},
sendMessageMode(newVal,oldVal){
// #ifdef APP
console.log('监听发送模式',newVal);
if(newVal != 'plus' && newVal != 'icon'){
this.$refs.tooltipPlus.hide();
}
// #endif
},
},
}
</script>
<style>
/* #ifdef H5 */
@import '/common/css/common.nvue.vue.css';
/* #endif */
</style>
# 2. 调整后的完整代码
<template>
<view>
<!-- 导航栏 -->
<chat-navbar title="聊天" :fixed="true"
:showPlus="false" :showUser="false"
:showBack="true" navbarClass="bg-light"
:h5WeiXinNeedNavbar="h5WeiXinNeedNavbar">
<chat-navbar-icon-button slot="right"
@click="openMore" >
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
</chat-navbar>
<!-- 聊天内容区域 -->
<scroll-view scroll-y class="bg-light position-fixed left-0 right-0"
:style="chatContentStyle" :show-scrollbar="false" @scroll="onScroll"
:scroll-into-view="scrollIntoViewId" >
<!-- 对话部分 -->
<view v-for="(item,index) in chatDataList" :key="index"
:id="'chat-item-'+index">
<chat-item :item="item" :index="index"
:prevTime="index>0 ? chatDataList[index-1].chat_time : 0"
ref="chatItem"></chat-item>
</view>
</scroll-view>
<!-- 针对我们的app端点击聊天区域授权加号扩展菜单 -->
<!-- #ifdef APP -->
<view v-if="sendMessageMode == 'plus' || sendMessageMode == 'icon'"
class="position-fixed left-0 right-0"
:style="chatContentStyle"
@click="scrollViewClick"></view>
<!-- #endif -->
<!-- 底部聊天输入区域 --><!-- 修改:添加ref获取textarea实例 -->
<view class="position-fixed bottom-0 border-top
flex flex-row align-center justify-between"
style="background-color: #f7f7f7;width: 750rpx;
min-height: 90rpx;max-height: 320rpx;
padding-top: 12rpx;"
:style="chatBottomStyle">
<view class="flex align-center">
<chat-navbar-icon-button>
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<view class="flex align-center font-sm
bg-white px-2 py-1 border rounded">
<textarea ref="textarea" fixed auto-height :maxlength="-1"
style="width: 440rpx;min-height: 60rpx;
max-height: 274rpx;overflow-y: scroll;
text-align: justify;"
:adjust-position="false"
v-model="messageValue"
@focus="textareaFocus"
@input="onTextareaInput"
@blur="onTextareaBlur" ><!-- 新增:监听失焦事件 --><!-- 新增:监听输入事件 -->
</textarea>
</view>
</view>
<view class="flex align-center">
<chat-navbar-icon-button v-if="!messageValue"
@click="openIcon">
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<chat-navbar-icon-button v-if="!messageValue"
@click="openPlus">
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<view v-if="messageValue"
class="rounded bg-success px-2 py-1 mr-4"
hover-class="bg-hover-success"
@click="sendMessage('text')">
<text class="font text-white">发送</text>
</view>
</view>
</view>
<!-- 弹出菜单 --><!-- 主要修改区域 -->
<chat-tooltip ref="tooltipPlus" :mask="chatTooltipMask"
:maskTransparent="true" :isBottom="true"
:tooltipWidth="750"
:tooltipHeight="tooltipHeight"
transformOrigin = "center bottom"
tooltipClass ="bg-light border-0 rounded-0"
@hideTooltip="hideTooltip">
<view class="border-top border-light-secondary">
<!-- 表情菜单 -->
<swiper v-if="sendMessageMode === 'icon'"
:indicator-dots="groupedIconMenus.length > 1"
:duration="1000"
:style="tooltipPlusMenuStyle"
@change="handleSwiperChange"><!-- 添加分页切换事件 -->
<!-- 第一页:emoji表情(滚动显示) -->
<swiper-item v-if="emojiPageItems.length > 0">
<scroll-view scroll-y style="height: 100%;">
<view class="flex flex-row flex-wrap justify-start">
<view v-for="(item,itemIndex) in emojiPageItems"
:key="itemIndex"
class="flex flex-column justify-center align-center"
style="width: 12.5%; height: 120rpx; padding: 10rpx;"
@click="insertEmoji(item)">
<text style="font-size: 50rpx;">{{item.icon}}</text>
</view>
</view>
</scroll-view>
</swiper-item>
<!-- 其他类型表情分页显示 -->
<swiper-item v-for="(page,pageIndex) in otherPages"
:key="pageIndex">
<view class="flex flex-row flex-wrap justify-start">
<view v-for="(item,itemIndex) in page"
:key="pageIndex+itemIndex"
class="col-3 flex flex-column justify-center align-center"
style="height: 260rpx;"
@click="item ? swiperItemClick(item,itemIndex) : null"
><!-- 小程序添加空值检查 -->
<view class="bg-white rounded-lg flex flex-row
align-center justify-center mb-2"
style="width:120rpx;height: 120rpx;">
<u--image v-if="item.iconType == 'image'"
:src="item.icon" mode="aspectFit"
width="120rpx" height="120rpx" radius="0rpx"></u--image>
<text v-if="item.iconType == 'emoji'"
style="font-size: 40px;color: #222222;">{{item.icon}}</text>
<text v-if="item.iconType == 'custom'"
class="iconfont"
style="font-size: 26px;color: #222222;">{{item.icon}}</text>
<u-icon v-if="item.iconType == 'uview'"
:name="item.icon" color="#222222" size="26"></u-icon>
</view>
<text class="font-sm text-light-muted">{{item.name}}</text>
</view>
</view>
</swiper-item>
</swiper>
<!-- 加号菜单(保持不变) -->
<swiper v-else-if="sendMessageMode === 'plus'"
:indicator-dots="pageCount > 1" :duration="1000"
:style="tooltipPlusMenuStyle">
<swiper-item v-for="(page,index) in groupedPlusMenus" :key="index">
<view class="flex flex-row justify-start flex-wrap"
:style="swiperItemStyle">
<view class="col-3 flex flex-column justify-center align-center"
style="height: 260rpx;"
v-for="(item,itemIndex) in page" :key="itemIndex"
@click="swiperItemClick(item,itemIndex)">
<view class="bg-white rounded-lg
flex flex-row align-center justify-center mb-2"
style="width:120rpx;height: 120rpx;">
<u--image v-if="item.iconType == 'image'"
:src="item.icon" mode="aspectFit"
width="120rpx" height="120rpx" radius="0rpx"></u--image>
<text v-if="item.iconType == 'emoji'"
style="font-size: 40px;color: #222222;">{{item.icon}}</text>
<text v-if="item.iconType == 'custom'"
class="iconfont"
style="font-size: 26px;color: #222222;">{{item.icon}}</text>
<u-icon v-if="item.iconType == 'uview'"
:name="item.icon" color="#222222" size="26"></u-icon>
</view>
<text class="font-sm text-light-muted">{{item.name}}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
</chat-tooltip>
</view>
</template>
<script>
import toolJs from '@/common/mixins/tool.js';
export default {
mixins:[toolJs],
data() {
return {
cursorPos: 0, // 新增:记录textarea光标位置
h5WeiXinNeedNavbar:false, // h5端微信上是否需要导航栏
statusBarHeight:0,//状态栏高度动态计算
fixedHeight:0, //占位:状态栏+导航栏
bottomSafeAreaHeight:0, // 底部安全距离
KeyboardHeight:0, //键盘高度
scrollIntoViewId:'', // 滚动到指定的元素id
messageValue:'', // 发送的内容信息
tooltipHeight:600, // 加号弹出菜单的高度rpx
sendMessageMode:"text",//发送消息的情况:文字|语音|加号|表情
// #ifdef MP || H5
chatTooltipMask:true,
// #endif
// #ifdef APP
chatTooltipMask:false,
// #endif
iconMenus:[
{ name:"微笑", icon:"/static/tabbar/index.png",
iconType:"image", eventType:"smile" },
{ name:"嘿嘿", icon:"😀",
iconType:"emoji", eventType:"heihei" },
{ name: "嗯,哼", icon: "https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/iconMenus/en.gif", iconType: "image", eventType: "enheng" },
{ name:"嘻嘻", icon:"😁",
iconType:"emoji", eventType:"xixi" },
{ name:"笑哭了", icon:"😂",
iconType:"emoji", eventType:"xiaokule" },
{ name:"哈哈", icon:"😃",
iconType:"emoji", eventType:"haha" },
{ name:"大笑", icon:"😄",
iconType:"emoji", eventType:"daxiao" },
{ name:"苦笑", icon:"😅",
iconType:"emoji", eventType:"kuxiao" },
{ name:"斜眼笑", icon:"😆",
iconType:"emoji", eventType:"xieyanxiao" },
{ name:"微笑天使", icon:"😇",
iconType:"emoji", eventType:"weixiaotianshi" },
{ name:"眨眼", icon:"😉",
iconType:"emoji", eventType:"zhayan" },
{ name:"羞涩微笑", icon:"😊",
iconType:"emoji", eventType:"xiuseweixiao" },
{ name:"呵呵", icon:"🙂",
iconType:"emoji", eventType:"hehe" },
{ name:"倒脸", icon:"🙃",
iconType:"emoji", eventType:"daolian" },
{ name:"笑得满地打滚", icon:"🤣",
iconType:"emoji", eventType:"xiaodemandidagun" },
],
plusMenus:[ // 加号扩展菜单栏目
{ name:"照片", icon:"photo", iconType:"uview", eventType:"photo" },
{ name:"位置", icon:"map", iconType:"uview", eventType:"map" },
{ name:"拍摄", icon:"\ue62c", iconType:"custom", eventType:"camera" },
{ name:"我的名片", icon:"\ue69d", iconType:"custom", eventType:"mingpian" },
{ name:"视频", icon:"\ue66d", iconType:"custom", eventType:"video" },
// { name:"拍摄", icon:"\ue62c", iconType:"custom", eventType:"camera" },
// { name:"我的名片", icon:"\ue69d", iconType:"custom", eventType:"mingpian" },
// { name:"视频", icon:"\ue66d", iconType:"custom", eventType:"video" },
// { name:"拍摄", icon:"\ue62c", iconType:"custom", eventType:"camera" },
// { name:"我的名片", icon:"\ue69d", iconType:"custom", eventType:"mingpian" },
// { name:"视频", icon:"\ue66d", iconType:"custom", eventType:"video" },
],
chatDataList:[
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148439,
data: '老师你好,我想咨询一下本季课程,如果我不学习上一个季度,可以直接学习本季度吗?',
user_id: 1,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
chat_time: 1750148449,
data: '同学你好,如果不学习上一个季度课程,如果你有vue的基础和js的基础知识,也可以学习本季度课程',
user_id: 2,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148759,
data: '好的,我了解了,谢谢老师',
user_id: 1,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
chat_time: 1750148859,
data: '不用谢',
user_id: 2,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148879,
data: 'ok',
user_id: 1,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148879,
data: '哈哈哈',
user_id: 1,
type:'text', //image,video
isremove:false,
},
{
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
nickname: '彦祖',
chat_time: 1750148879,
data: '嗯啦',
user_id: 1,
type:'text', //image,video
isremove:false,
},
],
}
},
mounted() {
let info = uni.getSystemInfoSync();
this.statusBarHeight = info.statusBarHeight;
this.fixedHeight = this.statusBarHeight + uni.upx2px(90);
this.bottomSafeAreaHeight = info.safeAreaInsets.bottom;
// 监听键盘高度变化
uni.onKeyboardHeightChange(res=>{
console.log('键盘高度变化',res);
// #ifdef H5 || MP
this.KeyboardHeight = res.height;
// #endif
// #ifdef APP
console.log('此时的输入模式', this.sendMessageMode);
if(this.sendMessageMode != 'plus' && this.sendMessageMode != 'icon'){
this.KeyboardHeight = res.height;
}
// #endif
if(this.KeyboardHeight){
this.chatContentToBottom();
}
});
// 页面加载完了之后就应该滚动到底部
this.$nextTick(()=>{
this.chatContentToBottom();
});
},
computed:{
chatContentStyle(){
let pbottom = this.bottomSafeAreaHeight == 0 ?
uni.upx2px(12) : this.bottomSafeAreaHeight;
let bottom = pbottom + uni.upx2px(90 + 12) + this.KeyboardHeight;
//如果是h5端用微信打开并且不需要导航栏的时候
if(this.isWeixinBrowser() && !this.h5WeiXinNeedNavbar){
this.fixedHeight = this.statusBarHeight + 0;
uni.setNavigationBarTitle({
title:'阿祖'
});
}
// #ifdef APP || MP
if(this.KeyboardHeight){
bottom = uni.upx2px(90 + 12 + 12) + this.KeyboardHeight;
}
// #endif
return `top:${this.fixedHeight}px;bottom:${bottom}px;`;
},
chatBottomStyle(){
let pbottom = this.bottomSafeAreaHeight == 0 ?
uni.upx2px(12) : this.bottomSafeAreaHeight;
// #ifdef APP || MP
if(this.KeyboardHeight){
pbottom = uni.upx2px(12);
}
// #endif
return `padding-bottom: ${pbottom}px;bottom:${this.KeyboardHeight}px;`;
},
//加号菜单每页的滑动样式
tooltipPlusMenuStyle(){
let pbottom = 0;
let height = uni.upx2px(this.tooltipHeight - 1) - pbottom;
// #ifdef APP || MP
pbottom = this.bottomSafeAreaHeight;
// #endif
return `padding-bottom:${pbottom}px;height:${height}px;`;
},
//加号菜单每页的布局样式
swiperItemStyle(){
let pbottom = 0;
let height = uni.upx2px(this.tooltipHeight - 1) - pbottom;
return `padding-bottom:${pbottom}px;height:${height}px;`;
},
// 加号菜单分页
// groupedPlusMenus(){
// const perPage = 8; // 每页8个
// const result = [];
// // 将数组plusMenus或者iconMenus每页8个分组
// for(let i=0;i<this.tooltipPlusMenusOrIconMenus.length; i += perPage){
// result.push(this.tooltipPlusMenusOrIconMenus.slice(i, i + perPage))
// }
// return result;
// },
//计算总页数
// pageCount(){
// return Math.ceil(this.tooltipPlusMenusOrIconMenus.length / 8);
// },
// 扩展菜单或者表情包数据源
tooltipPlusMenusOrIconMenus(){
if(this.sendMessageMode == 'plus' || this.sendMessageMode == 'icon'){
return this[`${this.sendMessageMode}Menus`]
}
return [];
},
// 修改:加号菜单分页计算
groupedPlusMenus() {
const perPage = 8; // 每页8个(2行)
const result = [];
for (let i = 0; i < this.plusMenus.length; i += perPage) {
result.push(this.plusMenus.slice(i, i + perPage));
}
return result;
},
// 修改:计算总页数(分别处理两种菜单)
pageCount() {
if (this.sendMessageMode === 'plus') {
return Math.ceil(this.plusMenus.length / 8);
} else if (this.sendMessageMode === 'icon') {
return this.groupedIconMenus.length;
}
return 0;
},
// 新增:表情菜单计算属性
emojiList() {
return this.iconMenus.filter(item => item.iconType === 'emoji');
},
otherList() {
return this.iconMenus.filter(item => item.iconType !== 'emoji');
},
emojiPageItems() {
return this.emojiList; // 所有emoji表情放在第一页
},
otherPages() {
const perPage = 8; // 每页8个(2行)
const pages = [];
for (let i = 0; i < this.otherList.length; i += perPage) {
pages.push(this.otherList.slice(i, i + perPage));
}
return pages;
},
groupedIconMenus() {
// 总页数 = 1 (emoji页) + 其他类型页数
const pages = [];
if (this.emojiPageItems.length > 0) {
pages.push(this.emojiPageItems); // 第一页放emoji
}
return pages.concat(this.otherPages);
},
},
methods: {
// 新增:textarea输入事件记录光标位置
onTextareaInput(e) {
// #ifdef H5 || APP
this.cursorPos = e.detail.cursor;
// #endif
},
// 新增:textarea失焦事件记录光标位置
onTextareaBlur(e) {
this.cursorPos = e.detail.cursor || this.messageValue.length;
},
// 新增:插入表情到textarea
insertEmoji(item) {
if (!item || !item.icon) return;
const emoji = item.icon;
const text = this.messageValue || '';
// 插入到当前光标位置
const newText = text.substring(0, this.cursorPos) +
emoji +
text.substring(this.cursorPos);
this.messageValue = newText;
// 更新光标位置(在插入的表情后面)
const newCursorPos = this.cursorPos + emoji.length;
this.cursorPos = newCursorPos;
// 设置光标位置(H5/APP支持)
this.$nextTick(() => {
if (!this.$refs.textarea) return;
let textarea;
// 处理 H5 平台的特殊情况
// #ifdef H5
textarea = this.$refs.textarea.$el; // 获取原生 DOM 元素
// #endif
// #ifndef H5
textarea = this.$refs.textarea;
// #endif
if (textarea) {
// 尝试设置光标位置
if (typeof textarea.setSelectionRange === 'function') {
try {
textarea.setSelectionRange(newCursorPos, newCursorPos);
} catch (e) {
console.warn('设置光标位置失败', e);
}
}
// 确保输入框聚焦
if (typeof textarea.focus === 'function') {
try {
textarea.focus();
} catch (e) {
console.warn('聚焦输入框失败', e);
}
}
}
});
},
//点击加号扩展菜单的某一项
// swiperItemClick(item,itemIndex){
// if(this.sendMessageMode == 'icon'){
// console.log('点击了表情包里面的某个表情');
// // this.messageValue += `[${item.name}]`;
// this.sendMessage('iconMenus',item)
// }else{
// console.log('点击加号扩展菜单的某一项',item.eventType);
// switch (item.eventType){
// case 'phote':
// break;
// case 'map':
// break;
// case 'camera':
// break;
// case 'mingpian':
// break;
// case 'video':
// break;
// }
// }
// },
// 修改:点击菜单项处理
swiperItemClick(item, itemIndex) {
console.log('点击菜单项处理',item);
if (!item) return; // 防止undefined错误
if (this.sendMessageMode === 'icon') {
if (item.iconType === 'emoji') {
this.insertEmoji(item); // emoji插入输入框
} else {
this.sendMessage('iconMenus', item); // 其他类型直接发送
}
} else {
console.log('点击加号扩展菜单的某一项',item.eventType);
switch (item.eventType){
case 'phote':
break;
case 'map':
break;
case 'camera':
break;
case 'mingpian':
break;
case 'video':
break;
}
}
},
handleSwiperChange(e) {
console.log('分页切换', e.detail.current);
// 可以在这里处理分页切换逻辑
},
//点击聊天区域
scrollViewClick(){
// #ifdef APP
console.log('点击聊天区域');
this.KeyboardHeight = 0;
uni.hideKeyboard();
this.$refs.tooltipPlus.hide();
this.sendMessageMode = "text";
// #endif
},
// 文本输入框聚焦
textareaFocus(){
// #ifdef APP
this.sendMessageMode = "text";
// #endif
},
//点击笑脸图标
openIcon(){
console.log('点击了笑脸');
// #ifdef APP
this.sendMessageMode = "icon";
uni.hideKeyboard();
// #endif
// #ifdef H5 || MP
this.sendMessageMode = "icon";
// #endif
this.$refs.tooltipPlus.show();
// #ifdef APP || MP || H5
this.KeyboardHeight = uni.upx2px(this.tooltipHeight);
this.chatContentToBottom();
// #endif
},
//点击了加号
openPlus(){
console.log('点击了加号');
// #ifdef APP
this.sendMessageMode = "plus";
uni.hideKeyboard();
// #endif
// #ifdef H5 || MP
this.sendMessageMode = "plus";
// #endif
this.$refs.tooltipPlus.show();
// #ifdef APP || MP || H5
this.KeyboardHeight = uni.upx2px(this.tooltipHeight);
this.chatContentToBottom();
// #endif
},
// 弹出框隐藏了
hideTooltip(){
console.log('弹出框隐藏了');
// #ifdef APP || MP || H5
this.KeyboardHeight = 0;
this.chatContentToBottom();
// #endif
},
//发送消息
sendMessage(msgType, option = {}){
console.log('发送消息',msgType);
let msg = {
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
user_id: 2,
chat_time: (new Date()).getTime(),
data: '',
type:msgType, //image,video
isremove:false,
};
switch (msgType){
case 'text':
msg.data = this.messageValue;
break;
case 'iconMenus':
console.log('iconMenus的数据',option);
msg.data = option.icon;
msg.dataType = option.iconType;
break;
}
this.chatDataList.push(msg);
// 清空发送的内容然后还要滚动到底部
if(msgType == 'text') this.messageValue = '';
this.chatContentToBottom();
},
openMore(){
console.log('点击了三个点图标');
},
//聊天内容滚到到底部
chatContentToBottom(){
// #ifdef APP
let chatItems = this.$refs.chatItem;
let lastIndex = chatItems.length - 1 == 0 ? 0 : chatItems.length - 1;
let last = chatItems[lastIndex];
const dom = weex.requireModule('dom');
dom.scrollToElement(last, {});
// #endif
// #ifdef MP || H5
if(this.chatDataList.length == 0) return;
const lastIndex = this.chatDataList.length - 1;
this.scrollIntoViewId = `chat-item-${lastIndex}`;
setTimeout(()=>{
this.scrollIntoViewId = '';
this.$nextTick(()=>{
this.scrollIntoViewId = `chat-item-${lastIndex}`;
});
},100)
// #endif
},
onScroll(){
console.log('页面发生了滚动');
}
},
watch:{
// 监听聊天记录数据变化,自动滚动到底部
chatDataList:{
handler(){
this.$nextTick(()=>{
this.chatContentToBottom();
});
},
deep:true
},
sendMessageMode(newVal,oldVal){
// #ifdef APP
console.log('监听发送模式',newVal);
if(newVal != 'plus' && newVal != 'icon'){
this.$refs.tooltipPlus.hide();
}
// #endif
},
},
}
</script>
<style>
/* #ifdef H5 */
@import '/common/css/common.nvue.vue.css';
/* #endif */
</style>
# 3. 调整后的完整代码改动点说明
<template>
<view>
<!-- 导航栏 -->
...
<!-- 聊天内容区域 -->
...
<!-- 底部聊天输入区域 --><!-- 修改:添加ref获取textarea实例 -->
<view class="position-fixed bottom-0 border-top
flex flex-row align-center justify-between"
style="background-color: #f7f7f7;width: 750rpx;
min-height: 90rpx;max-height: 320rpx;
padding-top: 12rpx;"
:style="chatBottomStyle">
<view class="flex align-center">
<chat-navbar-icon-button>
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<view class="flex align-center font-sm
bg-white px-2 py-1 border rounded">
<textarea ref="textarea" fixed auto-height :maxlength="-1"
style="width: 440rpx;min-height: 60rpx;
max-height: 274rpx;overflow-y: scroll;
text-align: justify;"
:adjust-position="false"
v-model="messageValue"
@focus="textareaFocus"
@input="onTextareaInput"
@blur="onTextareaBlur" ><!-- 新增:监听失焦事件 --><!-- 新增:监听输入事件 -->
</textarea>
</view>
</view>
<view class="flex align-center">
<chat-navbar-icon-button v-if="!messageValue"
@click="openIcon">
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<chat-navbar-icon-button v-if="!messageValue"
@click="openPlus">
<text class="iconfont font-lg"></text>
</chat-navbar-icon-button>
<view v-if="messageValue"
class="rounded bg-success px-2 py-1 mr-4"
hover-class="bg-hover-success"
@click="sendMessage('text')">
<text class="font text-white">发送</text>
</view>
</view>
</view>
<!-- 弹出菜单 --><!-- 主要修改区域 -->
<chat-tooltip ref="tooltipPlus" :mask="chatTooltipMask"
:maskTransparent="true" :isBottom="true"
:tooltipWidth="750"
:tooltipHeight="tooltipHeight"
transformOrigin = "center bottom"
tooltipClass ="bg-light border-0 rounded-0"
@hideTooltip="hideTooltip">
<view class="border-top border-light-secondary">
<!-- 表情菜单 -->
<swiper v-if="sendMessageMode === 'icon'"
:indicator-dots="groupedIconMenus.length > 1"
:duration="1000"
:style="tooltipPlusMenuStyle"
@change="handleSwiperChange"><!-- 添加分页切换事件 -->
<!-- 第一页:emoji表情(滚动显示) -->
<swiper-item v-if="emojiPageItems.length > 0">
<scroll-view scroll-y style="height: 100%;">
<view class="flex flex-row flex-wrap justify-start">
<view v-for="(item,itemIndex) in emojiPageItems"
:key="itemIndex"
class="flex flex-column justify-center align-center"
style="width: 12.5%; height: 120rpx; padding: 10rpx;"
@click="insertEmoji(item)">
<text style="font-size: 50rpx;">{{item.icon}}</text>
</view>
</view>
</scroll-view>
</swiper-item>
<!-- 其他类型表情分页显示 -->
<swiper-item v-for="(page,pageIndex) in otherPages"
:key="pageIndex">
<view class="flex flex-row flex-wrap justify-start">
<view v-for="(item,itemIndex) in page"
:key="pageIndex+itemIndex"
class="col-3 flex flex-column justify-center align-center"
style="height: 260rpx;"
@click="item ? swiperItemClick(item,itemIndex) : null"
><!-- 小程序添加空值检查 -->
<view class="bg-white rounded-lg flex flex-row
align-center justify-center mb-2"
style="width:120rpx;height: 120rpx;">
<u--image v-if="item.iconType == 'image'"
:src="item.icon" mode="aspectFit"
width="120rpx" height="120rpx" radius="0rpx"></u--image>
<text v-if="item.iconType == 'emoji'"
style="font-size: 40px;color: #222222;">{{item.icon}}</text>
<text v-if="item.iconType == 'custom'"
class="iconfont"
style="font-size: 26px;color: #222222;">{{item.icon}}</text>
<u-icon v-if="item.iconType == 'uview'"
:name="item.icon" color="#222222" size="26"></u-icon>
</view>
<text class="font-sm text-light-muted">{{item.name}}</text>
</view>
</view>
</swiper-item>
</swiper>
<!-- 加号菜单(保持不变) -->
<swiper v-else-if="sendMessageMode === 'plus'"
:indicator-dots="pageCount > 1" :duration="1000"
:style="tooltipPlusMenuStyle">
<swiper-item v-for="(page,index) in groupedPlusMenus" :key="index">
<view class="flex flex-row justify-start flex-wrap"
:style="swiperItemStyle">
<view class="col-3 flex flex-column justify-center align-center"
style="height: 260rpx;"
v-for="(item,itemIndex) in page" :key="itemIndex"
@click="swiperItemClick(item,itemIndex)">
<view class="bg-white rounded-lg
flex flex-row align-center justify-center mb-2"
style="width:120rpx;height: 120rpx;">
<u--image v-if="item.iconType == 'image'"
:src="item.icon" mode="aspectFit"
width="120rpx" height="120rpx" radius="0rpx"></u--image>
<text v-if="item.iconType == 'emoji'"
style="font-size: 40px;color: #222222;">{{item.icon}}</text>
<text v-if="item.iconType == 'custom'"
class="iconfont"
style="font-size: 26px;color: #222222;">{{item.icon}}</text>
<u-icon v-if="item.iconType == 'uview'"
:name="item.icon" color="#222222" size="26"></u-icon>
</view>
<text class="font-sm text-light-muted">{{item.name}}</text>
</view>
</view>
</swiper-item>
</swiper>
</view>
</chat-tooltip>
</view>
</template>
<script>
...
export default {
...,
data() {
return {
cursorPos: 0, // 新增:记录textarea光标位置
...,
iconMenus:[
{ name:"微笑", icon:"/static/tabbar/index.png",
iconType:"image", eventType:"smile" },
{ name:"嘿嘿", icon:"😀",
iconType:"emoji", eventType:"heihei" },
{ name: "嗯,哼", icon: "https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/iconMenus/en.gif", iconType: "image", eventType: "enheng" },
{ name:"嘻嘻", icon:"😁",
iconType:"emoji", eventType:"xixi" },
{ name:"笑哭了", icon:"😂",
iconType:"emoji", eventType:"xiaokule" },
{ name:"哈哈", icon:"😃",
iconType:"emoji", eventType:"haha" },
{ name:"大笑", icon:"😄",
iconType:"emoji", eventType:"daxiao" },
{ name:"苦笑", icon:"😅",
iconType:"emoji", eventType:"kuxiao" },
{ name:"斜眼笑", icon:"😆",
iconType:"emoji", eventType:"xieyanxiao" },
{ name:"微笑天使", icon:"😇",
iconType:"emoji", eventType:"weixiaotianshi" },
{ name:"眨眼", icon:"😉",
iconType:"emoji", eventType:"zhayan" },
{ name:"羞涩微笑", icon:"😊",
iconType:"emoji", eventType:"xiuseweixiao" },
{ name:"呵呵", icon:"🙂",
iconType:"emoji", eventType:"hehe" },
{ name:"倒脸", icon:"🙃",
iconType:"emoji", eventType:"daolian" },
{ name:"笑得满地打滚", icon:"🤣",
iconType:"emoji", eventType:"xiaodemandidagun" },
],
...
}
},
mounted() {
...
},
computed:{
...
// 加号菜单分页
// groupedPlusMenus(){
// const perPage = 8; // 每页8个
// const result = [];
// // 将数组plusMenus或者iconMenus每页8个分组
// for(let i=0;i<this.tooltipPlusMenusOrIconMenus.length; i += perPage){
// result.push(this.tooltipPlusMenusOrIconMenus.slice(i, i + perPage))
// }
// return result;
// },
//计算总页数
// pageCount(){
// return Math.ceil(this.tooltipPlusMenusOrIconMenus.length / 8);
// },
// 修改:加号菜单分页计算
groupedPlusMenus() {
const perPage = 8; // 每页8个(2行)
const result = [];
for (let i = 0; i < this.plusMenus.length; i += perPage) {
result.push(this.plusMenus.slice(i, i + perPage));
}
return result;
},
// 修改:计算总页数(分别处理两种菜单)
pageCount() {
if (this.sendMessageMode === 'plus') {
return Math.ceil(this.plusMenus.length / 8);
} else if (this.sendMessageMode === 'icon') {
return this.groupedIconMenus.length;
}
return 0;
},
// 新增:表情菜单计算属性
emojiList() {
return this.iconMenus.filter(item => item.iconType === 'emoji');
},
otherList() {
return this.iconMenus.filter(item => item.iconType !== 'emoji');
},
emojiPageItems() {
return this.emojiList; // 所有emoji表情放在第一页
},
otherPages() {
const perPage = 8; // 每页8个(2行)
const pages = [];
for (let i = 0; i < this.otherList.length; i += perPage) {
pages.push(this.otherList.slice(i, i + perPage));
}
return pages;
},
groupedIconMenus() {
// 总页数 = 1 (emoji页) + 其他类型页数
const pages = [];
if (this.emojiPageItems.length > 0) {
pages.push(this.emojiPageItems); // 第一页放emoji
}
return pages.concat(this.otherPages);
},
},
methods: {
// 新增:textarea输入事件记录光标位置
onTextareaInput(e) {
// #ifdef H5 || APP
this.cursorPos = e.detail.cursor;
// #endif
},
// 新增:textarea失焦事件记录光标位置
onTextareaBlur(e) {
this.cursorPos = e.detail.cursor || this.messageValue.length;
},
// 新增:插入表情到textarea
insertEmoji(item) {
if (!item || !item.icon) return;
const emoji = item.icon;
const text = this.messageValue || '';
// 插入到当前光标位置
const newText = text.substring(0, this.cursorPos) +
emoji +
text.substring(this.cursorPos);
this.messageValue = newText;
// 更新光标位置(在插入的表情后面)
const newCursorPos = this.cursorPos + emoji.length;
this.cursorPos = newCursorPos;
// 设置光标位置(H5/APP支持)
this.$nextTick(() => {
if (!this.$refs.textarea) return;
let textarea;
// 处理 H5 平台的特殊情况
// #ifdef H5
textarea = this.$refs.textarea.$el; // 获取原生 DOM 元素
// #endif
// #ifndef H5
textarea = this.$refs.textarea;
// #endif
if (textarea) {
// 尝试设置光标位置
if (typeof textarea.setSelectionRange === 'function') {
try {
textarea.setSelectionRange(newCursorPos, newCursorPos);
} catch (e) {
console.warn('设置光标位置失败', e);
}
}
// 确保输入框聚焦
if (typeof textarea.focus === 'function') {
try {
textarea.focus();
} catch (e) {
console.warn('聚焦输入框失败', e);
}
}
}
});
},
// 修改:点击菜单项处理
swiperItemClick(item, itemIndex) {
console.log('点击菜单项处理',item);
if (!item) return; // 防止undefined错误
if (this.sendMessageMode === 'icon') {
if (item.iconType === 'emoji') {
this.insertEmoji(item); // emoji插入输入框
} else {
this.sendMessage('iconMenus', item); // 其他类型直接发送
}
} else {
console.log('点击加号扩展菜单的某一项',item.eventType);
switch (item.eventType){
case 'phote':
break;
case 'map':
break;
case 'camera':
break;
case 'mingpian':
break;
case 'video':
break;
}
}
},
handleSwiperChange(e) {
console.log('分页切换', e.detail.current);
// 可以在这里处理分页切换逻辑
},
...
},
...
}
</script>
# 八、加号扩展菜单功能
内容过多,在新页面打开,具体查看: