# 一、顶部导航栏开发

  1. 新建即时通讯聊天页 pages/chat/chat.nvuepages.json
   {
        "path" : "pages/chat/chat",
        "style" : 
        {
            "navigationBarTitleText" : "聊天详情",
            "navigationStyle": "custom"
        }
    }
  1. 进入聊天即时通讯聊天页 pages/xiaoxi/xiaoxi.nvue
    ...
    methods: {
        ...
        openChat(e) {
            console.log('进入聊天详情页', e);
            uni.navigateTo({
                url: '/pages/chat/chat',
            });
        },
        ...
    }
    ...
  1. 在页面 pages/chat/chat.nvue
<template>
	<view>
		<!-- 导航栏 -->
		<chat-navbar title="聊天" :fixed="true"
		:showPlus="false" :showUser="false"
		:showBack="true" navbarClass="bg-light">
		    <chat-navbar-icon-button slot="right"
			@click="openMore" >
		    	<text class="iconfont font-md">&#xe626;</text>
		    </chat-navbar-icon-button>
		</chat-navbar>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				
			}
		},
		methods: {
			openMore(){
				console.log('点击打开更多');
			}
		}
	}
</script>

<style>
    /* #ifdef H5 */
	@import '/common/css/common.nvue.vue.css';
	/* #endif */
</style>

  1. 优化顶部导航栏组件 /components/chat-navbar/chat-navbar.vue
<template>
	<view>
		<!-- 导航栏 -->
		<view :class="[fixed ? 'fixed-top' : '',navbarClass]">
			<!-- 状态栏 -->
			...
			<!-- 导航 -->
			<!-- #ifdef APP || H5 -->
			<view class="flex justify-between align-center"
			style="height: 90rpx;">
				<!-- 左边 -->
				...
				<!-- 右边 -->
				<view class="flex align-center">
					...
					<slot name="right"></slot>
				</view>
			</view>
			<!-- #endif -->
			<!-- #ifdef MP -->
			...
			<!-- #endif -->
			
		</view>
		<!-- 占位符:占用 状态栏 + 导航栏的高度 -->
		...
		
		<!-- 导航栏点击加号的弹出菜单 -->
		...
		
	</view>
</template>

<script>
	...
	export default {
		...
		props:{
			...
			// 导航栏动态class
			navbarClass:{
				type:String,
				default:'bg-light'
			},
		},
		...
	}
</script>

<style>
	...
</style>

# 二、 底部聊天输入区域和聊天内容区域开发

在页面 pages/chat/chat.nvue

<template>
	<view>
		<!-- 导航栏 -->
		<chat-navbar title="聊天" :fixed="true"
		:showPlus="false" :showUser="false"
		:showBack="true" navbarClass="bg-light">
		    <chat-navbar-icon-button slot="right"
			@click="openMore" >
		    	<text class="iconfont font-lg">&#xe626;</text>
		    </chat-navbar-icon-button>
		</chat-navbar>
		
		<!-- 聊天内容区域 -->
		<scroll-view scroll-y class="bg-primary position-fixed left-0 right-0"
		:style="chatContentStyle">
			<view class="px-3" v-for="i in 5" :key="i">
				<text>第一季课程我们称之为 知识过渡课,前面第一学期、第二学期我们主要面对PC端进行的学习和网站开发,虽然我们开发的网站后台也做的是响应式,可以兼容我们的手机端,但是随着现在移动互联网的到来,客户更多希望他的网站可以在手机微信上进行浏览、可以在小程序上面搜索(小程序包括微信小程序、支付宝小程序、抖音小程序等),甚至有的客户希望他的项目可以做app,可以进行安装。
				</text>
			</view>	
		</scroll-view>
		
		<!-- 底部聊天输入区域 -->
		<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">&#xe643;</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;"></textarea>
				</view>
			</view>
			<!-- 右边 -->
			<view class="flex align-center">
				<chat-navbar-icon-button>
					<text class="iconfont font-lg">&#xe642;</text>
				</chat-navbar-icon-button>
				<chat-navbar-icon-button>
					<text class="iconfont font-lg">&#xe637;</text>
				</chat-navbar-icon-button>
			</view>
		</view>
		
	</view>
</template>

<script>
	export default {
		data() {
			return {
				statusBarHeight:0,//状态栏高度动态计算
				fixedHeight:0, //占位:状态栏+导航栏
				bottomSafeAreaHeight:0, // 底部安全距离
			}
		},
		mounted() {
			let info = uni.getSystemInfoSync();
			this.statusBarHeight = info.statusBarHeight;
			this.fixedHeight = this.statusBarHeight + uni.upx2px(90);
			this.bottomSafeAreaHeight = info.safeAreaInsets.bottom;
		},
		computed:{
			chatContentStyle(){
				let pbottom = this.bottomSafeAreaHeight == 0 ?
				uni.upx2px(12) : this.bottomSafeAreaHeight;
				let bottom = pbottom + uni.upx2px(90 + 12);
				return `top:${this.fixedHeight}px;bottom:${bottom}px;`;
			},
			chatBottomStyle(){
				let pbottom = this.bottomSafeAreaHeight == 0 ?
				uni.upx2px(12) : this.bottomSafeAreaHeight;
				return `padding-bottom: ${pbottom}px;`;
			}
		},
		methods: {
			openMore(){
				console.log('点击了三个点图标');
			}
		}
	}
</script>

<style>
    /* #ifdef H5 */
	@import '/common/css/common.nvue.vue.css';
	/* #endif */
</style>

# 三、 聊天内容区域开发:聊天气泡和封装组件

在页面 pages/chat/chat.nvue

# 1. 聊天气泡样式

    <!-- 聊天内容区域 -->
	<scroll-view scroll-y 
	class="bg-light position-fixed left-0 right-0 px-3"
	:style="chatContentStyle">
		<!-- 对话部分 -->
		<!-- 左边 -->
		<view class="flex align-start justify-start mb-3 position-relative">
			<!-- 头像 -->
			<u--image
			src="https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png"                    
			mode="widthFix" width="80rpx" height="80rpx" radius="10rpx"></u--image>
			<!-- 气泡 -->
			<!-- 三角标 -->
			<text class="iconfont font-md text-white"
			style="left: 25rpx;top: 20rpx;z-index: 100;">&#xe609;</text>
			<!-- 内容 -->
			<view class="bg-white p-2 rounded ml-1"
			style="max-width: 500rpx;">
				<text class="font" style="text-align: justify;">
					老师你好,我想咨询一下本季课程,如果我不学习上一个季度,可以直接学习本季度吗?
				</text>
			</view>
			
		</view>	
		<!-- 右边 -->
		<view class="flex align-start justify-end mb-3 position-relative">
			<!-- 气泡 -->
			<!-- 内容 -->
			<view class="p-2 rounded mr-1"
			style="max-width: 500rpx;background-color: #95EC69;">
				<text class="font" style="text-align: justify;">
					同学你好,如果不学习上一个季度课程,如果你有vue的基础和js的基础知识,也可以学习本季度课程
				</text>
			</view>
			<!-- 三角标 -->
			<text class="iconfont font-md text-white"
			style="right: 25rpx;top: 20rpx;z-index: 100;color:#95EC69;">&#xe640;</text>
			<!-- 头像 -->
			<u--image
			src="https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-08.png"                    
			mode="widthFix" width="80rpx" height="80rpx" radius="10rpx"></u--image>
			
		</view>
	</scroll-view>

# 2. 提取不同样式封装成class类

    ...
    <!-- 聊天内容区域 -->
	<scroll-view scroll-y 
	class="bg-light position-fixed left-0 right-0 px-3"
	:style="chatContentStyle">
		<!-- 对话部分 -->
		<!-- 左边 -->
		<view class="flex align-start justify-start mb-3 position-relative">
			<!-- 头像 -->
			<u--image
			src="https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png"                    
			mode="widthFix" width="80rpx" height="80rpx" radius="10rpx"></u--image>
			<!-- 气泡 -->
			<!-- 三角标 -->
			<text class="iconfont font-md chat-left-icon">&#xe609;</text>
			<!-- 内容 -->
			<view class="p-2 rounded ml-1 chat-left-content-bg"
			style="max-width: 500rpx;">
				<text class="font" style="text-align: justify;">
					老师你好,我想咨询一下本季课程,如果我不学习上一个季度,可以直接学习本季度吗?
				</text>
			</view>
			
		</view>	
		<!-- 右边 -->
		<view class="flex align-start justify-end mb-3 position-relative">
			<!-- 气泡 -->
			<!-- 内容 -->
			<view class="p-2 rounded mr-1 chat-right-content-bg"
			style="max-width: 500rpx;">
				<text class="font" style="text-align: justify;">
					同学你好,如果不学习上一个季度课程,如果你有vue的基础和js的基础知识,也可以学习本季度课程
				</text>
			</view>
			<!-- 三角标 -->
			<text class="iconfont font-md chat-right-icon">&#xe640;</text>
			<!-- 头像 -->
			<u--image
			src="https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-08.png"                    
			mode="widthFix" width="80rpx" height="80rpx" radius="10rpx"></u--image>
			
		</view>
	</scroll-view>
    ...

	<style>
    ...

	.chat-left-icon{
		left: 25rpx;top: 20rpx;z-index: 100;color: #ffffff;
	}
	.chat-right-icon{
		right: 25rpx;top: 20rpx;z-index: 100;color:#95EC69;
	}
	.chat-left-content-bg{
		background-color: white;
	}
	.chat-right-content-bg{
		background-color: #95EC69;
	}
</style>

# 3. 封装聊天气泡

# 1. 先循环绑定数据

   ...
   <!-- 聊天内容区域 -->
	<scroll-view scroll-y class="bg-light position-fixed left-0 right-0 px-3"
	:style="chatContentStyle">
		<!-- 对话部分 -->
		<view v-for="(item,index) in chatDataList" :key="index">
			<view class="flex align-start mb-3 position-relative"
			:class="[item.user_id != 2 ? 'justify-start':'justify-end']">
				<!-- 好友 -->
				<!-- 头像 -->
				<u--image v-if="item.user_id != 2"
				:src="item.avatar"  mode="widthFix"
				width="80rpx" height="80rpx" radius="10rpx"></u--image>
				<!-- 三角形 -->
				<text v-if="item.user_id != 2"
				class="iconfont font-md chat-left-icon">&#xe609;</text>
				<!-- 内容 -->
				<view class="p-2 rounded"
				style="max-width: 500rpx;"
				:class="[item.user_id != 2 ? 'chat-left-content-bg ml-1' : 
				'chat-right-content-bg mr-1']">
					<text class="font" style="text-align: justify;">
						{{item.data}}
					</text>
				</view>
				
				<!-- 我 -->
				<text v-if="item.user_id == 2"
				class="iconfont font-md chat-right-icon">&#xe640;</text>
				<u--image v-if="item.user_id == 2"
				:src="item.avatar" 
				mode="widthFix"
				width="80rpx" height="80rpx" radius="10rpx"></u--image>
			</view>
		</view>
		
		<!-- 右边 -->
		<!-- <view class="flex justify-end align-start mb-3 position-relative">
			<view class="chat-right-content-bg p-2 rounded mr-1"
			style="max-width: 500rpx;background-color: #95ec69;">
				<text class="font" style="text-align: justify;">
					同学你好,如果不学习上一个季度课程,如果你有vue的基础和js的基础知识,也可以学习本季度课程
				</text>
			</view>
			<text class="iconfont font-md chat-right-icon">&#xe640;</text>
			<u--image 
			src="https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-08.png" 
			mode="widthFix"
			width="80rpx" height="80rpx" radius="10rpx"></u--image>
			
		</view> -->
	</scroll-view>
	...
	<script>
	export default {
		data() {
			return {
				...
				chatDataList:[
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
						nickname: '彦祖',
						chat_time: '16:11',
						data: '老师你好,我想咨询一下本季课程,如果我不学习上一个季度,可以直接学习本季度吗?',
						type: 'text', // image,video
						user_id:1,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
						nickname: '小二哥',
						chat_time: '16:11',
						data: '同学你好,如果不学习上一个季度课程,如果你有vue的基础和js的基础知识,也可以学习本季度课程',
						type: 'text', // image,video
						user_id:2,
					},
				],
			}
		},
		...
	}
</script>

# 2. 封装成组件

# 1. 创建组件 /components/chat-item/chat-item.vue
<template>
	<view class="flex align-start mb-3 position-relative"
	:class="[!isMe ? 'justify-start':'justify-end']">
		<!-- 好友 -->
		<!-- 头像 -->
		<u--image v-if="!isMe"
		:src="item.avatar"  mode="widthFix"
		width="80rpx" height="80rpx" radius="10rpx"></u--image>
		<!-- 三角形 -->
		<text v-if="!isMe"
		class="iconfont font-md chat-left-icon">&#xe609;</text>
		<!-- 内容 -->
		<view class="p-2 rounded"
		style="max-width: 500rpx;"
		:class="[!isMe ? 'chat-left-content-bg ml-1' : 
		'chat-right-content-bg mr-1']">
			<text class="font" style="text-align: justify;">
				{{item.data}}
			</text>
		</view>
		
		<!-- 我 -->
		<text v-if="isMe"
		class="iconfont font-md chat-right-icon">&#xe640;</text>
		<u--image v-if="isMe"
		:src="item.avatar" 
		mode="widthFix"
		width="80rpx" height="80rpx" radius="10rpx"></u--image>
	</view>
</template>

<script>
	export default{
		name:"chat-item",
	    props:{
			item:Object,
			index:Number
		},
		computed:{
			// 我的判断, 假设我的id=2,后期由实际数据在更换
			isMe(){
				let user_id = 2;
				return this.item.user_id === user_id;
			}
		}
	}
</script>

<style>
	/* #ifdef H5 */
	@import '/common/css/common.nvue.vue.css';
	/* #endif */
	.chat-left-icon{
		left: 25rpx;top: 20rpx;z-index: 100;color: #ffffff;
	}
	.chat-right-icon{
		right: 25rpx;top: 20rpx;z-index: 100;color:#95EC69;
	}
	.chat-left-content-bg{
		background-color: #ffffff;
	}
	.chat-right-content-bg{
		background-color: #95ec69;
	}
</style>
# 2. 在聊天页使用组件 pages/chat/chat.nvue
    <!-- 聊天内容区域 -->
	<scroll-view scroll-y class="bg-light position-fixed left-0 right-0 px-3"
	:style="chatContentStyle">
		<!-- 对话部分 -->
		<view v-for="(item,index) in chatDataList" :key="index">
			<chat-item :item="item" :index="index"></chat-item>
		</view>
	</scroll-view>

# 4. 聊天时间处理

聊天时间比较短,比如5分钟内,不会频繁显示聊天时间
另外针对时间使用时间戳处理,在网上随便搜索在线时间戳,如:https://www.beijing-time.org/shijianchuo/ (opens new window)

# 1. 组件 /components/chat-item/chat-item.vue

<template>
	<view>
		<!-- 时间 -->
		<view v-if="chatShowTime"
		class="flex align-center justify-center pt-2 pb-2">
			<text class="font-sm text-light-muted">{{chatShowTime}}</text>
		</view>
		<!-- 聊天内容 -->
		<view class="flex align-start mb-3 position-relative"
		:class="[!isMe ? 'justify-start' : 'justify-end']">
			<!-- 好友 -->
			...
			
			<!-- 我 -->
			...
		</view>
    </view>
</template>

<script>
	import parseTimeJs from '@/common/mixins/parseTime.js';
	export default{
		name:"chat-item",
		mixins:[parseTimeJs],
		props:{
			...
			//上一条消息时间
			prevTime:[Number,String]
		},
		computed:{
			...
			// 处理聊天时间
			chatShowTime(){
				return parseTimeJs.getChatTime(this.item.chat_time,this.prevTime);
			}
		}
	}
</script>

<style scoped>
	...
</style>

# 2. 处理时间的js工具 /common/mixins/parseTime.js

export default{
	//当前时间
	CurentTime(){ 
		var now = new Date();
		var year = now.getFullYear();       
		var month = now.getMonth() + 1;     
		var day = now.getDate();            
		var hh = now.getHours();            
		var mm = now.getMinutes();          
		var ss = now.getSeconds();
		var clock = year + "-";
		if(month < 10) clock += "0";
		clock += month + "-";
		if(day < 10) clock += "0";
		clock += day + " ";
		if(hh < 10) clock += "0";
		clock += hh + ":";
		if (mm < 10) clock += '0'; 
		clock += mm + ":"; 
		if (ss < 10) clock += '0';
		clock += ss;
		return(clock); 
	},
	// 计算当前日期星座
	getHoroscope(date) {
	  let c = ['摩羯','水瓶','双鱼','白羊','金牛','双子','巨蟹','狮子','处女','天秤','天蝎','射手','摩羯']
	  date=new Date(date);
	  let month = date.getMonth() + 1;
	  let day = date.getDate();
	  let startMonth = month - (day - 14 < '865778999988'.charAt(month));
	  return c[startMonth]+'座';
	},
	// 计算指定时间与当前的时间差
	sumAge(data){
		let dateBegin = new Date(data.replace(/-/g, "/"));
		let dateEnd = new Date();//获取当前时间
		let dateDiff = dateEnd.getTime() - dateBegin.getTime();//时间差的毫秒数
		let dayDiff = Math.floor(dateDiff / (24 * 3600 * 1000));//计算出相差天数
		let leave1=dateDiff%(24*3600*1000)    //计算天数后剩余的毫秒数
		let hours=Math.floor(leave1/(3600*1000))//计算出小时数
		//计算相差分钟数
		let leave2=leave1%(3600*1000)    //计算小时数后剩余的毫秒数
		let minutes=Math.floor(leave2/(60*1000))//计算相差分钟数
		//计算相差秒数
		let leave3=leave2%(60*1000)      //计算分钟数后剩余的毫秒数
		let seconds=Math.round(leave3/1000)
		return dayDiff+"天 "+hours+"小时 ";
	},
	// 获取聊天时间(相差300s内的信息不会显示时间)
	getChatTime(v1,v2){
		v1=v1.toString().length<13 ? v1*1000 : v1;
		v2=v2.toString().length<13 ? v2*1000 : v2;
		if(((parseInt(v1)-parseInt(v2))/1000) > 300){
			return this.gettime(v1);
		}
	},
	// 人性化时间格式
	gettime(shorttime){
		shorttime=shorttime.toString().length<13 ? shorttime*1000 : shorttime;
		let now = (new Date()).getTime();
		let cha = (now-parseInt(shorttime))/1000;
		
		if (cha < 43200) {
			// 当天
			return this.dateFormat(new Date(shorttime),"{A} {t}:{ii}");
		} else if(cha < 518400){
			// 隔天 显示日期+时间
			return this.dateFormat(new Date(shorttime),"{Mon}月{DD}日 {A} {t}:{ii}");
		} else {
			// 隔年 显示完整日期+时间
			return this.dateFormat(new Date(shorttime),"{Y}-{MM}-{DD} {A} {t}:{ii}");
		}
	},
	
	parseNumber(num) {
		return num < 10 ? "0" + num : num;
	},
	 
	dateFormat(date, formatStr) {
		let dateObj = {},
			rStr = /\{([^}]+)\}/,
			mons = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
		 
		dateObj["Y"] = date.getFullYear();
		dateObj["M"] = date.getMonth() + 1;
		dateObj["MM"] = this.parseNumber(dateObj["M"]);
		dateObj["Mon"] = mons[dateObj['M'] - 1];
		dateObj["D"] = date.getDate();
		dateObj["DD"] = this.parseNumber(dateObj["D"]);
		dateObj["h"] = date.getHours();
		dateObj["hh"] = this.parseNumber(dateObj["h"]);
		dateObj["t"] = dateObj["h"] > 12 ? dateObj["h"] - 12 : dateObj["h"];
		dateObj["tt"] = this.parseNumber(dateObj["t"]);
		dateObj["A"] = dateObj["h"] > 12 ? '下午' : '上午';
		dateObj["i"] = date.getMinutes();
		dateObj["ii"] = this.parseNumber(dateObj["i"]);
		dateObj["s"] = date.getSeconds();
		dateObj["ss"] = this.parseNumber(dateObj["s"]);
	 
		while(rStr.test(formatStr)) {
			formatStr = formatStr.replace(rStr, dateObj[RegExp.$1]);
		}
		return formatStr;
	},
	// 获取年龄
	getAgeByBirthday(data){
		let birthday=new Date(data.replace(/-/g, "\/")); 
		let d=new Date(); 
		return d.getFullYear()-birthday.getFullYear()-((d.getMonth()<birthday.getMonth()|| d.getMonth()==birthday.getMonth() && d.getDate()<birthday.getDate())?1:0);
	},
	
}

# 3. 聊天页调用 /pages/chat/chat.nvue

<template>
	<view>
		<!-- 导航栏 -->
		...
		
		<!-- 聊天内容区域 -->
		<scroll-view scroll-y 
		class="bg-light position-fixed left-0 right-0 px-3"
		:style="chatContentStyle">
			<!-- 对话部分 -->
			<view v-for="(item,index) in chatDataList" :key="index">
				<chat-item :item="item" :index="index"
				:prevTime="index>0 ? chatDataList[index-1].chat_time : 0">
				</chat-item>
			</view>
		</scroll-view>
		
		<!-- 底部聊天输入区域 -->
		...
		
	</view>
</template>

<script>
	export default {
		data() {
			return {
				...
				chatDataList:[
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
						nickname: '彦祖',
						chat_time: 1750145785,
						data: '老师你好,我想咨询一下本季课程,如果我不学习上一个季度,可以直接学习本季度吗?',
						user_id: 1,
						type:'text', //image,video
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
						nickname: '小二哥',
						chat_time: 1750145800,
						data: '同学你好,如果不学习上一个季度课程,如果你有vue的基础和js的基础知识,也可以学习本季度课程',
						user_id: 2,
						type:'text', //image,video
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
						nickname: '彦祖',
						chat_time: 1750145807,
						data: '好的,我了解了,谢谢老师',
						user_id: 1,
						type:'text', //image,video
					},
				],
			}
		},
		
	}
</script>

<style>
   ...
</style>

# 5. 聊天撤回处理

# 1. 聊天组件 /components/chat-item/chat-item.vue

<template>
	<view>
		<!-- 时间 -->
		...
		<!-- 聊天内容 -->
		<view  
		class="flex align-start mb-3 position-relative"
		:class="[!isMe ? 'justify-start' : 'justify-end']">
			<!-- 好友 -->
			...
			<!-- 内容 -->
			<view class="p-2 rounded"
			style="max-width: 500rpx;"
			:class="[!isMe ? 'chat-left-content-bg ml-1' 
			: 'chat-right-content-bg mr-1',`chatItem${index}`]"
			:ref="'chatItem' + index"
			@longpress="onLongpress($event,index,item)">
				<text class="font" style="text-align: justify;">
					{{item.data}}
				</text>
			</view>
			
			<!-- 我 -->
			...
		</view>
	
	
	    <!-- 弹出菜单 -->
	    <chat-tooltip ref="chatTooltip" :mask="true" 
	    :maskTransparent="true" :isBottom="false" 
	    :tooltipWidth="tooltipWidth"
	    :tooltipHeight="60"
	    transformOrigin="center center"
	    tooltipClass="bg-dark text-white border-0">
	    	<view class="flex flex-row flex-1">
	    		<view class="flex-1 align-center justify-center" 
	    		hover-class="bg-hover-dark"
	    			v-for="(item,index) in menuList" :key="index" 
	    			@click="clickType(item.type)">
	    			<text class="text-white">{{item.name}}</text>
	    		</view>
	    	</view>
	    	<!-- 向下箭头 -->
	    	<text class="position-fixed iconfont text-dark"
	    	style="font-size: 40rpx;"
	    	:style="jiantouStyle">&#xe649;</text>
	    </chat-tooltip>
	    	
    </view>
</template>

<script>
	...
	export default{
		...
		data(){
			return {
				tooltipLeft:0, // 弹出菜单组件left x
				tooltipTop:0,  // 弹出菜单组件top  y
				//组件超过这个宽度,菜单居中显示,小于它随着点击点显示
				rectmaxWidth:0,
				//长按获取相应数据
				longpressObj:null,
				menuList: [
					{
						name: "复制",
						type: 'copy'
					},
					{
						name: "撤回",
						type: 'reset'
					},
				],
			}
		},
		computed:{
			...
			tooltipWidth() {
				return this.menuList.length * 120;
			},
			jiantouStyle(){
				let left = (uni.upx2px(750-40)) / 2;
				let top = this.tooltipTop + uni.upx2px(40 + 5);
				let jiantouCss = ``;
				if(this.longpressObj && this.longpressObj.rect.width < this.rectmaxWidth){
					top = this.longpressObj.y - 10;
					left = this.longpressObj.x + 5;
					jiantouCss = `transform: rotate(180deg);`;
				}
				return `left:${left}px;top:${top}px;${jiantouCss}`;
			}
		},
		methods:{
			//长按消息
			onLongpress(e,index,item){
				console.log('组件里面的事件对象',e);
				
				let x = 0,
					y = 0;
				// #ifdef H5 || MP
				x = e.changedTouches[0].clientX;
				y = e.changedTouches[0].clientY;
				// #endif
				// #ifdef APP
				x = e.changedTouches[0].screenX;
				y = e.changedTouches[0].screenY;
				// #endif
				/*
				this.$emit('Longpress',{
					x,
					y,
					index,
					item
				});
				*/
			   
			    // #ifdef H5 || MP
			    const query = uni.createSelectorQuery().in(this);
			    query.select(`.chatItem${index}`).boundingClientRect(res => {
			        if (res) {
			            console.log('组件距离各个方向距离:', res);
			            // 可以在这里处理菜单弹出的位置逻辑
						/*
			            this.$emit('Longpress',{
			    			x,
			    			y,
			    			index,
			    			item,
							rect:res
			    		});
						*/
					    this.Longpressfn({
							x,
							y,
							index,
							item,
							rect:res
						});
			        }
			    }).exec();
			    // #endif

			    // #ifdef APP
			    const refName = 'chatItem' + index;
			    const ref = this.$refs[refName];
				if (!ref) {
					console.error('未找到元素引用: ' + refName);
					return;
				}
			    // 使用 Weex 的 dom 模块获取位置
			    const dom = weex.requireModule('dom');
			    dom.getComponentRect(ref, result => {
					if (result && result.result) {
						const rect = result.size;
						console.log('组件距离各个方向距离:', rect);  
						// 传递位置信息给父组件
						  /*
						  this.$emit('Longpress', {
							x: x, 
							y: y,
							index,
							item,
							rect:rect
						  });
						  */
						   this.Longpressfn({
						   	x,
						   	y,
						   	index,
						   	item,
						   	rect:rect
						   });
						} else {
						  console.error('获取位置失败', result);
						}
				});
			    // #endif
			    
			},
			Longpressfn(e) {
				console.log('长按', e);
				this.longpressObj = e;
				console.log('长按的是longpressObj', this.longpressObj);
				//超过这个宽度,菜单居中显示,小于它随着点击点显示
				this.rectmaxWidth = uni.upx2px((750 - 60 - 80 - (35 + 10 - 5)) / 2);
				console.log('最大组件宽度',this.rectmaxWidth);
				if(e.rect.width >= this.rectmaxWidth){
					this.tooltipLeft = (uni.upx2px(750 - this.tooltipWidth)) / 2;
					this.tooltipTop = e.y - uni.upx2px(60 + 40 - 15);
				}else{
					this.tooltipLeft = e.x;
					this.tooltipTop = e.y;
				}
				this.$refs.chatTooltip.show(this.tooltipLeft, this.tooltipTop);
			},
			clickType(e) {
				console.log('点击菜单',e);
				switch (e){
					case 'copy':
						break;
					case 'reset':
						break;
				}
				this.$refs.chatTooltip.hide();
			},
			
		}
	}
</script>

<style scoped>
	...
</style>

# 2. 聊天撤回处理

# ① 优化弹窗
  1. /components/chat-item/chat-item.vue 弹窗透明
 <!-- 弹出菜单 -->
 :maskTransparent="true"
  1. /components/chat-tooltip/chat-tooltip.vue 自行调整
props: {
	...
	// 底部tabbar高度(非原生tabbar)
	tabbarHeight:{
		type: Number,
		default:90+12+12,//tabbar高90rpx,上下内边距12rpx
	}
},
mounted() {
	...
	// 应该还要去掉底部tabbar高度,因为不是原生tabbar(同时要调整箭头位置)
	//this.maxTop = info.windowHeight - uni.upx2px(this.tooltipHeight) - uni.upx2px(this.tabbarHeight);
	this.maxTop = info.windowHeight - uni.upx2px(this.tooltipHeight);
},
# ② 不能撤回好友消息

/components/chat-item/chat-item.vue

    <!-- 弹出菜单 -->
	<chat-tooltip ref="chatTooltip" :mask="true" 
	:maskTransparent="true" :isBottom="false" 
	:tooltipWidth="tooltipWidth"
	:tooltipHeight="60"
	tooltipClass="bg-dark border-0 text-white">
		<view class="flex flex-row flex-1">
			<view class="flex-1 align-center justify-center" 
			hover-class="bg-hover-dark"
				v-for="(item,index) in getmenuList" :key="index" 
				@click="clickType(item.type)">
				<text class="text-white">{{item.name}}</text>
			</view>
		</view>
		<!-- 箭头 -->
		<text class="position-fixed iconfont text-dark"
		style="font-size: 40rpx;"
		:style="jiantouStyle">&#xe649;</text>
	</chat-tooltip>
	...
	computed:{
		tooltipHeight() {
			return this.getmenuList.length * this.menuEveHeight;
		},
		tooltipWidth(){
			return this.getmenuList.length * 120;
		},
		// 弹窗菜单处理
		getmenuList(){
			return this.menuList.filter(v=>{
				if(v.name === '撤回' && !this.isMe){
					return false;
				}
				return true;
			});
		}
	}

# ③ 撤回消息处理

首先在消息列表,给每条消息一个是否撤回字段做标记,字段如:isremove:false
在组件: /components/chat-item/chat-item.vue

    <!-- 时间 -->
	...
	<!-- 撤回消息 -->
	<view v-if="item.isremove"
	class="flex align-center justify-center pt-2 pb-2">
		<text class="font-sm text-light-muted">你撤回了一条信息</text>
	</view>
	<!-- 聊天内容 -->
	<view v-else ...></view>
	...
	methods:{
		clickType(e) {
			...
			switch (e){
				...
				case 'removeChatItem':// 撤回消息
					this.item.isremove = true;
					break;
			}
			...
		},
	}

# 四、聊天内容区域开发:底部发送内容区域

内容过多,在新页面打开,具体查看:聊天内容区域开发:底部发送内容区域

  1. 聊天页输入区域:加号扩展菜单功能(发图片)
  2. 聊天页输入区域:加号扩展菜单功能:语音功能(语音播放)
  3. 聊天页输入区域:加号扩展菜单功能:语音功能(发语音)
  4. 聊天页输入区域:加号扩展菜单功能:播放视频及发视频
更新时间: 2025年7月11日星期五上午10点20分