# 一、先了解一下uni-app的尺寸单位rpx

具体查看官方文档:https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#尺寸单位 (opens new window)

# 二、消息页顶部导航栏

# 1. 顶部导航栏基本样式

/pages/xiaoxi/xiaoxi.nvue

<template>
	<view class="page">
		<!-- 导航栏 -->
		<!-- #ifdef APP || H5 -->
		<view class="bg-light">
			<!-- 状态栏 -->
			<view :style="'height:' + statusBarHeight + 'px;' "></view>
			<!-- 导航 -->
			<view class="flex flex-row align-center justify-between" 
			style="height: 90rpx;">
			    <!-- 左边 -->
				<view class="flex align-center">
					<!-- 标题 -->
					<text class="font-md ml-3">消息</text>
				</view>
				<!-- 右边 -->
				<view class="flex align-center">
					<!-- 联系人 -->
					<view style="height: 90rpx;width: 90rpx;"
					class="flex align-center justify-center">
						<text class="iconfont font-lg">&#xe650;</text>
					</view>
					<!-- 加号 -->
					<view style="height: 90rpx;width: 90rpx;" 
					class="flex align-center justify-center">
						<text class="iconfont font-md">&#xe637;</text>
					</view>
				</view>
			</view>
		</view>
		<!-- #endif -->
		
	</view>
</template>

<script>
	export default {
		data() {
			return {
				statusBarHeight:0,//状态栏高度动态计算
			}
		},
		onLoad() {
			this.statusBarHeight = uni.getSystemInfoSync().statusBarHeight;
			// console.log('系统信息',uni.getSystemInfoSync());
		},
		methods: {
			
		}
	}
</script>

<style>
@import '/common/css/common.nvue.vue.css';
</style>

# 2. 初步学习封装组件:图标按钮组件的简单封装

我们在[第二学期第三季课程]中,已经教大家封装过提示组件了,接下来我们看一下在uni-app中如何封装组件。

# ① 创建组件

根目录新建文件夹 components,然后创建组件,比如:chat-navbar-icon-button/chat-navbar-icon-button.vue,文件夹名称和组件名称一样

好处:
可直接写组件,不用传统方式:

<script>
	import chatNavbarIconButton from '@/components/chat-navbar-icon-button/chat-navbar-icon-button.vue'
	export default {
		components:{
			chatNavbarIconButton
		},
        ...
    }
</script>

# ② 组件代码

/components/chat-navbar-icon-button/chat-navbar-icon-button.vue

<template>
	<view style="width: 90rpx;height: 90rpx;"
	class="flex align-center justify-center"
	hover-class="bg-hover-light">
		<slot></slot>
	</view>
</template>

<script>
	export default {
		name:'chat-navbar-icon-button',
	}
</script>

<style>
</style>

# ③ 组件使用

/pages/xiaoxi/xiaoxi.nvue

    <!-- 右边 -->
    <view class="flex align-center">
        <!-- 联系人 -->
        <chat-navbar-icon-button >
            <text class="iconfont font-lg">&#xe650;</text>
        </chat-navbar-icon-button>
        <!-- 加号 -->
        <chat-navbar-icon-button >
            <text class="iconfont font-md">&#xe655;</text>
        </chat-navbar-icon-button>
    </view>

# 3. 封装消息页顶部导航栏组件

# ① 创建组件

创建 /components/chat-navbar/chat-navbar.vue

<template>
	<view>
		<!-- 导航栏 -->
		<view class="bg-light" :class="[fixed?'fixed-top':'']">
			<!-- 状态栏 -->
			<view :style="'height:' + statusBarHeight + 'px;'"></view>
			<!-- 导航 -->
			<view class="flex justify-between align-center"
			style="height: 90rpx;">
				<!-- 左边 -->
				<view>
					<!-- 标题 -->
					<text class="ml-3" v-if="title">{{title}}</text>
				</view>
				<!-- 右边 -->
				<view class="flex align-center">
					<chat-navbar-icon-button>
						<text class="iconfont font-lg">&#xe650;</text>
					</chat-navbar-icon-button>
					<chat-navbar-icon-button>
						<text class="iconfont font-md">&#xe655;</text>
					</chat-navbar-icon-button>
				</view>
			</view>
		</view>
		<!-- 占位:占位状态栏 + 导航栏-->
		<view :style="fixedHeightStyle" v-if="fixed"></view>
	</view>
</template>

<script>
	export default {
		name:"chat-navbar",
		props:{
			// 标题
			title:{
				type:String,
				default:'',
			},
			// 是否固定在顶部
			fixed:{
				type:Boolean,
				default:true
			}
		},
		data() {
			return {
				statusBarHeight:0,//状态栏高度动态计算
				fixedHeight:0, // 占位状态栏 + 导航栏
			}
		},
		mounted() {
			this.statusBarHeight = uni.getSystemInfoSync().statusBarHeight;
			console.log('系统信息',uni.getSystemInfoSync());
			this.fixedHeight = this.statusBarHeight + uni.upx2px(90);
			console.log('占位高度',this.fixedHeight);
		},
		computed:{
			fixedHeightStyle(){
				return `height:${this.fixedHeight}px;`;
			}
		}
	}
</script>

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

# ② 组件使用

/pages/xiaoxi/xiaoxi.nvue

<template>
	<view class="page">
		<!-- 导航栏 -->
		<!-- #ifdef APP || H5 -->
		<chat-navbar title="消息" :fixed="true"></chat-navbar>
		<!-- #endif -->
		
		<!-- 聊天列表 -->
		<view style="height: 4000rpx;">
			<text>聊天列表1</text>
		</view>
		
	</view>
</template>

<script>
	export default {
		data() {
			return {
				
			}
		},
		onLoad() {
			
		},
		methods: {
			
		}
	}
</script>

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

# 三、消息列表开发

# 1. 消息列表效果


消息列表

# 2. 消息列表代码

在根目录 /pages/xiaoxi/xiaoxi.nvue

<template>
	<view >
		<!-- 导航栏 -->
		<!-- #ifdef APP || H5 -->
        <chat-navbar title="消息(100)" :fixed="true"></chat-navbar> 
		<!-- #endif -->
		
		<!-- 消息列表 -->
		<view v-for="(item,index) in chatList" :key="index">
			<view class="flex align-center bg-white">
				<!-- 头像 -->
				<view class="flex align-center justify-center"
				style="width: 150rpx;">
					<image :src="item.avatar" 
					mode="widthFix" class="rounded"
					style="width: 90rpx;height: 90rpx;"></image>
				</view>
				<!-- 右边聊天部分 -->
				<view class="flex flex-column border-bottom border-light-secondary flex-1 py-3 pr-3 ">
					<!-- 上面:昵称 时间 -->
					<view class="flex justify-between align-center mb-1">
						<text class="font-md">{{item.nickname}}</text>
						<text class="font-sm text-light-muted">{{item.chat_time}}</text>
					</view>
					<!-- 下面:内容 -->
					<view class="pr-5">
						<text class="font text-light-muted u-line-1">{{item.data}}</text>
					</view>
				</view>
			</view>
		</view>
		
	</view>
</template>

<script>
	export default {
		data() {
			return {
				chatList:[
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-01.png',
						nickname:'晓明哥',
						chat_time:'20:29',
						data:'5月30日我的武汉演唱会记得来给我当助邀嘉宾啊',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-02.png',
						nickname:'热巴',
						chat_time:'20:20',
						data:'我的电影通告设计完成了吗',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-03.png',
						nickname:'GIGI',
						chat_time:'19:27',
						data:'我参加的时光音乐会节目发挥得怎么样',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-04.png',
						nickname:'基仔',
						chat_time:'18:35',
						data:'最近做什么有没有新电影发布',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-05.png',
						nickname:'娜扎',
						chat_time:'17:47',
						data:'最近正在拍一部古装剧,化妆师有点拉,你给我设计一下',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
						nickname:'彦祖',
						chat_time:'16:11',
						data:'晚上过来吃饭,我今天在家里下厨,尝一下我的手艺',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
						nickname:'力宏哥',
						chat_time:'15:07',
						data:'明天一起出海晒晒太阳,这几天太忙了',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-08.png',
						nickname:'华仔',
						chat_time:'14:20',
						data:'今晚8点记得锁定我直播间啊',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-09.png',
						nickname:'黄奕姐',
						chat_time:'11:59',
						data:'干嘛呢,我的宣传海报还有几天可以给我啊',
					},
					{
						avatar:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-10.png',
						nickname:'阿帅',
						chat_time:'10:20',
						data:'下午去钓鱼,去不',
					},
				],
			}
		},
		onLoad() {
			
		},
		methods: {
			
		}
	}
</script>

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

说明:

  1. 如果有同学对消息列表中一行文字多出用省略号表示的.u-line-1写法感兴趣的,可以调试浏览器查看css的写法;

# 四、消息列表头像右上角消息数量提示

# 1. 效果图


消息列表

# 2. 代码

在根目录 /pages/xiaoxi/xiaoxi.nvue

<template>
	<view>
		<!-- 导航栏 -->
		<!-- #ifdef APP || H5 -->
		<chat-navbar title="消息(100)" :fixed="false"></chat-navbar>
		<!-- #endif -->

		<!-- 消息列表 -->
		<view v-for="(item,index) in chatList" :key="index">
			<view class="flex align-stretch">
				<!-- 头像 -->
				<view style="width: 150rpx;" class="flex align-center justify-center 
				position-relative ">
					<!-- 头像地址 -->
					<!-- <image :src="item.avatar" mode="widthFix" style="width: 90rpx;height: 90rpx;" 
					class="rounded">
					</image> -->
					<u--image :src="item.avatar" mode="widthFix"
					width="90rpx" height="90rpx" radius="8rpx"></u--image>
					<!-- 消息数量 角标-->
					<!-- <text class="bg-danger rounded-circle text-white font-sm position-absolute"
					style="padding-left: 14rpx;padding-right: 14rpx;
					padding-top: 2rpx;padding-bottom: 2rpx;
					top: 16rpx;right:10rpx;z-index: 100;">9</text> -->
					<u-badge :isDot="false" :value="item.datacount" 
					absolute :offset="['16rpx','10rpx']"
					max="999" shape="circle" numberType="limit"></u-badge>
				</view>
				<!-- 右边 -->
				<view class="flex flex-column border-bottom border-light-secondary flex-1 py-3 pr-3">
					<!-- 上面:昵称 + 时间 -->
					<view class="flex justify-between align-center mb-1">
						<text class="font-md">{{item.nickname}}</text>
						<text class="font-sm text-light-muted">{{item.chat_time}}</text>
					</view>
					<!-- 下面:聊天内容 -->
					<view class="pr-5">
						<text class="font text-light-muted u-line-1">{{item.data}}</text>
					</view>
				</view>
			</view>
		</view>

	</view>
</template>

<script>
	export default {
		data() {
			return {
				chatList: [{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-01.png',
						nickname: '晓明哥',
						chat_time: '20:29',
						data: '5月30日我的武汉演唱会记得来给我当助邀嘉宾啊',
						datacount:0,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-02.png',
						nickname: '热巴',
						chat_time: '20:20',
						data: '我的电影通告设计完成了吗',
						datacount:9,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-03.png',
						nickname: 'GIGI',
						chat_time: '19:27',
						data: '我参加的时光音乐会节目发挥得怎么样',
						datacount:19,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-04.png',
						nickname: '基仔',
						chat_time: '18:35',
						data: '最近做什么有没有新电影发布',
						datacount:599,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-05.png',
						nickname: '娜扎',
						chat_time: '17:47',
						data: '最近正在拍一部古装剧,化妆师有点拉,你给我设计一下',
						datacount:7991,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
						nickname: '彦祖',
						chat_time: '16:11',
						data: '晚上过来吃饭,我今天在家里下厨,尝一下我的手艺',
						datacount:79914,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
						nickname: '力宏哥',
						chat_time: '15:07',
						data: '明天一起出海晒晒太阳,这几天太忙了',
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-08.png',
						nickname: '华仔',
						chat_time: '14:20',
						data: '今晚8点记得锁定我直播间啊',
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-09.png',
						nickname: '黄奕姐',
						chat_time: '11:59',
						data: '干嘛呢,我的宣传海报还有几天可以给我啊',
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-10.png',
						nickname: '阿帅',
						chat_time: '10:20',
						data: '下午去钓鱼,去不',
					},
				],
			}
		},
		onLoad() {

		},
		methods: {

		}
	}
</script>

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

说明:

  1. app端发现当角标写在头像前面时候,就算设置了z-index属性,依然头像会盖住角标,说明了:nvue页面的渲染是从上而下进行渲染的,这是和vue页面比较大的区别,因此在开发nvue页面的时候注意顺序,如果希望某个组件在某个组件之上,你就要写在这个组件之后,也就是越后面层级越高。

# 五、对消息列表做一个组件封装并理解父子组件事件传递

# 1. 创建消息列表组件

/components/chat-chatlist/chat-chatlist.vue

<template>
	<view class="flex align-stretch" hover-class="bg-hover-light"
	@click="onClick(item,index)" @longpress="onLongpress">
		<!-- 头像 -->
		<view style="width: 150rpx;" class="flex align-center justify-center 
		position-relative ">
			<!-- 头像地址 -->
			<!-- <image :src="item.avatar" mode="widthFix" style="width: 90rpx;height: 90rpx;" 
			class="rounded">
			</image> -->
			<u--image :src="item.avatar" mode="widthFix"
			width="90rpx" height="90rpx" radius="8rpx"></u--image>
			<!-- 消息数量 角标-->
			<!-- <text class="bg-danger rounded-circle text-white font-sm position-absolute"
			style="padding-left: 14rpx;padding-right: 14rpx;
			padding-top: 2rpx;padding-bottom: 2rpx;
			top: 16rpx;right:10rpx;z-index: 100;">9</text> -->
			<u-badge :isDot="false" :value="item.datacount" 
			absolute :offset="['16rpx','10rpx']"
			max="999" shape="circle" numberType="limit"></u-badge>
		</view>
		<!-- 右边 -->
		<view class="flex flex-column border-bottom border-light-secondary flex-1 py-3 pr-3">
			<!-- 上面:昵称 + 时间 -->
			<view class="flex justify-between align-center mb-1">
				<text class="font-md">{{item.nickname}}</text>
				<text class="font-sm text-light-muted">{{item.chat_time}}</text>
			</view>
			<!-- 下面:聊天内容 -->
			<view class="pr-5">
				<text class="font text-light-muted u-line-1">{{item.data}}</text>
			</view>
		</view>
	</view>
</template>

<script>
	export default{
		name:"chat-chatlist",
		props:{
			item:Object,
			index:Number
		},
		methods:{
			onClick(item,index){
				console.log('点击了列表在组件');
				this.$emit('click',{
					item,
					index
				});
			},
			onLongpress(e){
				console.log(e);
			}
		}
	}
</script>

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

# 2. 消息页使用组件,理解父子组件事件传递

在根目录 /pages/xiaoxi/xiaoxi.nvue

<template>
	<view>
		<!-- 导航栏 -->
		<!-- #ifdef APP || H5 -->
		<chat-navbar title="消息(100)" :fixed="true"></chat-navbar>
		<!-- #endif -->

		<!-- 消息列表 -->
		<view v-for="(item,index) in chatList" :key="index">
			<chat-chatlist :item="item" :index="index" @click="openChat"></chat-chatlist>
		</view>

	</view>
</template>

<script>
	export default {
		data() {
			return {
				chatList: [{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-01.png',
						nickname: '晓明哥',
						chat_time: '20:29',
						data: '5月30日我的武汉演唱会记得来给我当助邀嘉宾啊',
						datacount:0,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-02.png',
						nickname: '热巴',
						chat_time: '20:20',
						data: '我的电影通告设计完成了吗',
						datacount:9,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-03.png',
						nickname: 'GIGI',
						chat_time: '19:27',
						data: '我参加的时光音乐会节目发挥得怎么样',
						datacount:19,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-04.png',
						nickname: '基仔',
						chat_time: '18:35',
						data: '最近做什么有没有新电影发布',
						datacount:599,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-05.png',
						nickname: '娜扎',
						chat_time: '17:47',
						data: '最近正在拍一部古装剧,化妆师有点拉,你给我设计一下',
						datacount:7991,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
						nickname: '彦祖',
						chat_time: '16:11',
						data: '晚上过来吃饭,我今天在家里下厨,尝一下我的手艺',
						datacount:79914,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
						nickname: '力宏哥',
						chat_time: '15:07',
						data: '明天一起出海晒晒太阳,这几天太忙了',
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-08.png',
						nickname: '华仔',
						chat_time: '14:20',
						data: '今晚8点记得锁定我直播间啊',
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-09.png',
						nickname: '黄奕姐',
						chat_time: '11:59',
						data: '干嘛呢,我的宣传海报还有几天可以给我啊',
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-10.png',
						nickname: '阿帅',
						chat_time: '10:20',
						data: '下午去钓鱼,去不',
					},
				],
			}
		},
		onLoad() {

		},
		methods: {
            openChat(e){
				console.log('点击了列表在页面',e);
			}
		}
	}
</script>

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

# 六、长按消息列表和加号等很多地方,弹出菜单并将其封装成组件

# 1. 基础样式

    ...
    <!-- 弹出菜单 -->
	<view style="z-index: 1000;overflow: hidden;">
		<!-- 透明蒙版,在弹出菜单之后,不能点页面其它地方 -->
		<view class="position-fixed top-0 left-0 right-0 bottom-0"
		style="background-color:rgba(0, 0, 0, 0.3);"></view>
		<!-- 菜单内容 -->
		<view class="position-fixed bg-white"
		style="left: 300rpx;top: 140rpx;">
			<view class="py-1 px-2">
				<view>撤回</view>
				<view>删除</view>
			</view>
		</view>
	</view>
	...

# 2. 写进组件 chat-tooltip

/components/chat-tooltip/chat-tooltip.vue

<template>
	<view style="z-index: 1000;overflow: hidden;" v-if="showstatus">
		<!-- 透明蒙版,在弹出菜单之后,不能点页面其它地方 -->
		<view class="position-fixed top-0 left-0 right-0 bottom-0"
		style="background-color:rgba(0, 0, 0, 0.3);" @click="hide"></view>
		<!-- 菜单内容 -->
		<view class="position-fixed bg-white"
		style="left: 300rpx;top: 140rpx;">
			<slot></slot>
		</view>
	</view>
</template>

<script>
	export default{
		name:"chat-tooltip",
		data(){
			return {
				showstatus:false,
			}
		},
		methods:{
			show(){
				this.showstatus = true
			},
			hide(){
				this.showstatus = false
			}
		}
	}
</script>

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

# 3. 长按测试组件是否执行

  1. 在组件 /components/chat-chatlist/chat-chatlist.vue 传递长按事件到消息页
    methods:{
		...
		onLongpress(e){
			console.log('组件里面的事件对象',e);
			this.$emit('Longpress')
		}
	}
  1. 在根目录 消息页 /pages/xiaoxi/xiaoxi.nvue
<template>
	<view>
		<!-- 导航栏 -->
		<!-- #ifdef APP || H5 -->
		<chat-navbar title="消息(100)" :fixed="true"></chat-navbar>
		<!-- #endif -->

		<!-- 消息列表 -->
		<view v-for="(item,index) in chatList" :key="index">
			<chat-chatlist :item="item" :index="index" @click="openChat"
			@Longpress="Longpressfn"></chat-chatlist>
		</view>
		
		<!-- 弹出菜单 -->
		<chat-tooltip ref="chatTooltip">
			<view class="py-1 px-2">
				<view>撤回</view>
				<view>删除</view>
			</view>
		</chat-tooltip>

	</view>
</template>

<script>
	export default {
		data() {
			return {
				chatList: [{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-01.png',
						nickname: '晓明哥',
						chat_time: '20:29',
						data: '5月30日我的武汉演唱会记得来给我当助邀嘉宾啊',
						datacount:0,
					},
					...
				],
			}
		},
		onLoad() {

		},
		methods: {
           openChat(e){
			   console.log('点击的是消息页',e);
		   },
		   Longpressfn(){
			   console.log('页面长按');
			   this.$refs.chatTooltip.show();
		   }
		}
	}
</script>

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

# 4. 弹出菜单的蒙版强化和设置弹出菜单位置

对于弹出菜单,它的蒙版和位置有以下场景:

  1. 需要蒙版;
  2. 不需要蒙版;
  3. 蒙版为透明(点击加号)或者不透明(点击设置);
  4. 菜单位置:底部或者根据点击坐标定;

# 1. 组件:/components/chat-tooltip/chat-tooltip.vue

<template>
	<view style="z-index: 1000;overflow: hidden;" v-if="showstatus">
		<!-- 透明蒙版 , 在弹出菜单之后,不能点击其它地方-->
		<view v-if="mask"
		class="position-fixed top-0 left-0 right-0 bottom-0"
		:style="maskTransparentStyle"
		@click="hide"></view>
		<!-- 菜单内容 -->
		<view class="position-fixed bg-white"
		:style="tooltipStyle"
		:class="isBottomClass">
			<view class="py-1 px-2">
				<view><text>撤回</text></view>
				<view><text>删除</text></view>
			</view>
		</view>
	</view>
</template>

<script>
	export default {
		name:"chat-tooltip",
		props:{
			// 是否有蒙版,默认有
			mask:{
				type:Boolean,
				default:true,
			},
			// 蒙版是否透明,默认透明
			maskTransparent:{
				type:Boolean,
				default:true,
			},
			// 弹出菜单位置:底部或者根据点击坐标定
			// 是否在底部
			isBottom:{
				type:Boolean,
				default:false,
			}
		},
		data(){
			return {
				showstatus:false,
				pageX:-1,
				pageY:-1,
			}
		},
		computed:{
			// 蒙版是否透明
			maskTransparentStyle(){
				const opacity = this.maskTransparent ? 0.0 : 0.3;
				return `background-color: rgba(0, 0, 0, ${opacity});`;
			},
			// 弹出菜单是否在底部
			isBottomClass(){
				return this.isBottom ? `left-0 right-0 bottom-0` : `rounded border`;
			},
			// 弹出菜单根据点击位置弹出
			tooltipStyle(){
				let left = this.pageX >=0 ? `left:${this.pageX}px;` : ``;
				let top = this.pageY >=0 ? `top:${this.pageY}px;` : ``;
				return `${left}${top}`;
			}
		},
		methods:{
			show(left,top){
				this.pageX = left;
				this.pageY = top;
				this.showstatus = true
			},
			hide(){
				this.showstatus = false
			}
		}
	}
</script>

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

# 2. 调试

在根目录 消息页 /pages/xiaoxi/xiaoxi.nvue

<template>
	<view>
		...
		
		<!-- 弹出菜单 -->
		<chat-tooltip ref="chatTooltip" 
		:mask="true" :maskTransparent="false"
		:isBottom="false"></chat-tooltip>

	</view>
</template>

<script>
	export default {
		...
		methods: {
            ...
		    Longpressfn(e){
			   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.$refs.chatTooltip.show(x,y);
		    }
		}
	}
</script>

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

# 5. 处理弹出菜单超出屏幕的问题

# 1. 组件:/components/chat-tooltip/chat-tooltip.vue

<template>
	<view style="z-index: 1000;overflow: hidden;" v-if="showstatus">
		<!-- 透明蒙版 , 在弹出菜单之后,不能点击其它地方-->
		<view v-if="mask"
		class="position-fixed top-0 left-0 right-0 bottom-0"
		:style="maskTransparentStyle"
		@click="hide"></view>
		<!-- 菜单内容 -->
		<view class="position-fixed bg-white"
		:style="tooltipStyle"
		:class="isBottomClass">
			<slot></slot>
		</view>
	</view>
</template>

<script>
	export default {
		name:"chat-tooltip",
		props:{
			...
			//弹出菜单宽度rpx
			tooltipWidth:{
				type:Number,
				default:0
			},
			//弹出菜单高度rpx
			tooltipHeight:{
				type:Number,
				default:0
			},
		},
		data(){
			return {
				...
				maxLeft:0,
				maxTop:0,
			}
		},
		mounted() {
			const info = uni.getSystemInfoSync();
			console.log('设备信息',info);
			this.maxLeft = info.windowWidth - uni.upx2px(this.tooltipWidth);
			this.maxTop = info.windowHeight - uni.upx2px(this.tooltipHeight);
			// console.log(this.maxLeft,this.maxTop); 
		},
		computed:{
			...
			// 弹出菜单的位置根据坐标定
			tooltipStyle(){
				let left = this.pageX >=0 ? `left: ${this.pageX}px;`: ``;
				let top = this.pageY >=0 ? `top: ${this.pageY}px;`: ``;
				return `${left}${top}
				width:${uni.upx2px(this.tooltipWidth)}px;
				height:${uni.upx2px(this.tooltipHeight)}px;`
			}
		},
		methods:{
			show(left,top){
				// console.log(this.maxLeft,this.maxTop);
				this.pageX = left > this.maxLeft ? this.maxLeft : left;
				this.pageY = top > this.maxTop ? this.maxTop : top;
				this.showstatus = true
			},
			...
		}
	}
</script>

...

# 2. 调试

在根目录 消息页 /pages/xiaoxi/xiaoxi.nvue

...
    <!-- 弹出菜单 -->
	<chat-tooltip ref="chatTooltip"
	:mask="true" :maskTransparent="false"
	:isBottom="false"
	:tooltipWidth="280" :tooltipHeight="400">
		<view>
			<view><text>撤回</text></view>
			<view><text>删除</text></view>
		</view>
	</chat-tooltip>
...

# 6. 菜单内容设置

在根目录 消息页 /pages/xiaoxi/xiaoxi.nvue

<template>
	<view>
		...

		<!-- 弹出菜单 -->
		<chat-tooltip ref="chatTooltip" :mask="true" :maskTransparent="false" :isBottom="false" :tooltipWidth="180"
			:tooltipHeight="tooltipHeight">
			<view class="flex flex-1 flex-column">
				<view class="flex-1 align-start justify-center pl-2" hover-class="bg-hover-light"
					v-for="(item,index) in menuList" :key="index" @click="clickType(item.type)">
					<text>{{item.name}}</text>
				</view>
			</view>
		</chat-tooltip>

	</view>
</template>

<script>
	export default {
		data() {
			return {
				menuEveHeight: 60, //每个菜单高度设定rpx
				menuList: [{
						name: '置顶',
						type: 'zhiding'
					},
					{
						name: '删除',
						type: 'deleteChat'
					},
				],
				...
			}
		},
		onLoad() {

		},
		computed: {
			//菜单高度
			tooltipHeight() {
				return this.menuList.length * this.menuEveHeight;
			}
		},
		methods: {
			clickType(e) {
               console.log('点击菜单',e);
			},
			...
		}
	}
</script>

<style>
	...
</style>

# 7. 菜单动画

# 1. app端nvue页面的动画

参考weex官方文档: https://weexapp.com/zh/docs/modules/animation.html (opens new window)
/components/chat-tooltip/chat-tooltip.vue

<template>
	<view style="z-index: 1000;overflow: hidden;" v-if="showstatus">
		<!-- 透明蒙版 , 在弹出菜单之后,不能点击其它地方-->
		<view v-if="mask"
		class="position-fixed top-0 left-0 right-0 bottom-0"
		:style="maskTransparentStyle"
		@click="hide"></view>
		<!-- 菜单内容 -->
		<view class="position-fixed bg-white chat-tooltip-animate"
		:style="tooltipStyle"
		:class="isBottomClass"
		ref="tooltipContent">
			<slot></slot>
		</view>
	</view>
</template>

<script>
	export default {
		name:"chat-tooltip",
		...
		methods:{
			show(left,top){
				console.log(this.maxLeft,this.maxTop);
				this.pageX = left > this.maxLeft ? this.maxLeft : left;
				this.pageY = top > this.maxTop ? this.maxTop : top;
				this.showstatus = true
				
				// #ifdef APP-PLUS-NVUE
				this.$nextTick(()=>{
					//app端nvue页面动画实现
					const animation = weex.requireModule('animation');
					animation.transition(this.$refs.tooltipContent, {
							styles: {
								opacity: 1,
								transform: 'scale(1, 1)',
								transformOrigin:'left top',
							},
							duration: 100, //ms
							timingFunction: 'ease',
							needLayout:false, //是否影响布局
							delay: 0 //ms
					    }, function () {
					        console.log('动画完成');
					    }
					);
				});
				// #endif
				
			},
			hide(){
				// #ifdef APP-PLUS-NVUE
				this.$nextTick(()=>{
					//app端nvue页面动画实现
					const animation = weex.requireModule('animation');
					animation.transition(this.$refs.tooltipContent, {
							styles: {
								opacity: 0,
								transform: 'scale(0, 0)',
								transformOrigin:'left top',
							},
							duration: 100, //ms
							timingFunction: 'ease',
							needLayout:false, //是否影响布局
							delay: 0 //ms
					    }, ()=> {
					        this.showstatus = false
							console.log('关闭菜单动画完成');
					    }
					);
				});
				// #endif
				// #ifndef APP-PLUS-NVUE
				this.showstatus = false
				// #endif
			}
		}
	}
</script>

<style scoped>
	/* #ifdef H5 */
	@import '/common/css/common.nvue.vue.css';
	/* #endif */
	.chat-tooltip-animate{
		/* #ifdef APP-PLUS-NVUE */
		transform: scale(0,0);
		opacity: 0;
		/* #endif */
	}
</style>

# 七、点击加号弹出菜单

# ① 组件 /components/chat-navbar-icon-button/chat-navbar-icon-button.vue

<template>
	<view style="width: 90rpx;height: 90rpx;"
	class="flex align-center justify-center"
	hover-class="bg-hover-light" @click="$emit('click')">
		<slot></slot>
	</view>
</template>

<script>
	export default {
		name:"chat-navbar-icon-button",
	}
</script>

<style>
</style>

# ② 组件 /components/chat-navbar/chat-navbar.vue

    <!-- 右边 -->
	<view class="flex align-center">
		<chat-navbar-icon-button @click="openUser">
			<text class="iconfont font-lg">&#xe650;</text>
		</chat-navbar-icon-button>
		<chat-navbar-icon-button @click="openPlus">
			<text class="iconfont font-md">&#xe655;</text>
		</chat-navbar-icon-button>
	</view>
	...
	<!-- 占位符:占用 状态栏 + 导航栏的高度 -->
	<view :style="fixedHeightStyle" v-if="fixed"></view>
	
	<!-- 导航栏点击加号的菜单 -->
	<chat-tooltip ref="chatTooltip" :mask="true"
	:maskTransparent="true" :isBottom="false" 
	:tooltipWidth="260"  :tooltipHeight="tooltipHeight"
	tooltipClass="bg-dark text-white"
	transformOrigin="right top">
		<view class="flex flex-column flex-1">
			<view class="flex-1 align-center justify-start pl-2 flex" 
			hover-class="bg-hover-dark"
				v-for="(item,index) in menuList" :key="index" @click="clickType(item.type)">
				<text class="iconfont font-md mr-3 text-white">{{item.icon}}</text>
				<text class="text-white">{{item.name}}</text>
			</view>
		</view>
		<!-- 一个小箭头 -->
		<view class="position-fixed right-0"
		style="right: 26rpx;" :style="'top:'+ (fixedHeight - 6) + 'px;'">
			<text class="iconfont font-lg text-dark">&#xe631;</text>
		</view>
	</chat-tooltip>

	<script>
	export default {
		...
		data() {
			return {
				...
				menuEveHeight: 100, //每个菜单默认高度是rpx
				menuList: [{
						name: "发起群聊",
						type: 'qunliao',
						icon:'\ue60a',
					},
					{
						name: "添加朋友",
						type: 'addfriend',
						icon:'\ue65d',
					},
					{
						name: "扫一扫",
						type: 'saoyisao',
						icon:'\ue661',
					},
				],
			}
		},
		methods: {
			openUser(){
				console.log('点击联系人图标');
			},
			openPlus(){
				console.log('点击加号图标');
				const info = uni.getSystemInfoSync();
				let left = uni.upx2px(750 - 260) - 5;
				let top = this.fixedHeight;
				this.$refs.chatTooltip.show(left,top);
			},
			clickType(e){
				console.log('点击菜单项目',e);
			}
		},
		computed:{
			...
			tooltipHeight() {
				return this.menuList.length * this.menuEveHeight;
			}
		}
	}
	</script>

# ③ 组件 /components/chat-tooltip/chat-tooltip.vue

<template>
	<view style="z-index: 1000;overflow: hidden;" v-if="showstatus">
		<!-- 透明蒙版 , 在弹出菜单之后,不能点击其它地方-->
		...
		<!-- 菜单内容 -->
		<view class="position-fixed chat-tooltip-animate" 
		:style="tooltipStyle" :class="[isBottomClass,tooltipClass]"
		ref="tooltipContent">
			<slot></slot>
		</view>
	</view>
</template>

<script>
	export default {
		name: "chat-tooltip",
		props: {
			...
			// 弹出菜单Class样式自定义
			tooltipClass:{
				type: String,
				default: 'bg-white'
			},
			// 菜单弹出方向
			transformOrigin:{
				type: String,
				default: 'left top'
			}
		},
		...
		methods: {
			show(left, top) {
				...

				// #ifdef APP-PLUS-NVUE
				// 针对app端的nvue页面的动画
				this.$nextTick(() => {
					...
					animation.transition(this.$refs.tooltipContent, {
						styles: {
							...
							transformOrigin:this.transformOrigin,
						},
						...
					}, ()=> {
						...
					});
				});
				// #endif
			},
			hide() {
				// #ifdef APP-PLUS-NVUE
				...
				animation.transition(this.$refs.tooltipContent, {
					styles: {
						...
						transformOrigin:this.transformOrigin,
					},
					...
				}, ()=> {
					...
				});
				// #endif
				...
			}
		}
	}
</script>

<style>
	...
</style>

# 八、完成置顶和删除聊天的静态页面效果

主要突破点就是长按的时候找到是哪一条聊天记录

# 1. 删除聊天

  1. 在组件 /components/chat-chatlist/chat-chatlist.vue
<template>
	<view class="flex align-stretch" hover-class="bg-hover-light"
	@click="onClick(item,index)" @longpress="onLongpress($event,index,item)">
		...
	</view>
</template>

<script>
	export default{
		...
		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
				});
			}
		}
	}
</script>

<style>
	...
</style>
  1. 在根目录 消息页 /pages/xiaoxi/xiaoxi.nvue
<template>
	<view>
		...
	</view>
</template>

<script>
	export default {
		data() {
			return {
				chatListIndex:-1, // 哪一条聊天记录
				menuEveHeight: 80, //每个菜单默认高度是60rpx
				...
			}
		},
		...
		methods: {
			clickType(e) {
				console.log('点击菜单',e);
				switch(e){
					case 'deleteChat':
					   this.deleteChat();
					   break;
					case 'zhiding':
					   break;
				}
				this.$refs.chatTooltip.hide();
			},
			//删除当前聊天
			deleteChat(){
				this.chatList.splice(this.chatListIndex,1);
			},
			...,
			Longpressfn(e) {
				console.log('长按的是消息页', e);
				this.$refs.chatTooltip.show(e.x, e.y);
				this.chatListIndex = e.index;
			},
		}
	}
</script>

<style>
	...
</style>

# 2. 置顶聊天

可以把置顶聊天和正常消息分为两组处理,置顶消息放在最前面,正常消息放在后面,可以给每条信息加一个是否置顶的标识符:isZhiding:true/false

    <!-- 消息列表 -->
	<!-- 置顶消息列表 -->
	<view v-for="(item,index) in chatList" :key="'zhiding_' + index">
		<chat-chatlist v-if="item.isZhiding"
		:item="item" :index="index" @click="openChat" 
		@Longpress="Longpressfn"></chat-chatlist>
	</view>
	<!-- 正常消息列表 -->
	<view v-for="(item,index) in chatList" :key="'normal_' + index">
		<chat-chatlist v-if="!item.isZhiding"
		:item="item" :index="index" @click="openChat" 
		@Longpress="Longpressfn"></chat-chatlist>
	</view>
	...
	<script>
	    export default {
			methods: {
				clickType(e) {
					console.log('点击菜单',e);
					switch(e){
						case 'deleteChat':
						this.deleteChat();
						break;
						case 'zhiding':
						this.zhiding();
						break;
					}
					this.$refs.chatTooltip.hide();
				},
				...
				// 置顶或取消置顶
				zhiding(){
					let item = this.chatList[this.chatListIndex];
					let isZhiding = item.isZhiding;
					// item.isZhiding = isZhiding ? false : true;
					item.isZhiding = !item.isZhiding;
				},
				...
				Longpressfn(e) {
					console.log('长按的是消息页', e);
					this.chatListIndex = e.index;
					this.menuList[0].name = e.item.isZhiding ? '取消置顶' :'置顶';
					this.$refs.chatTooltip.show(e.x, e.y);
				},
			}
		}
	</script>

# 九、兼容微信小程序

# 1. 微信小程序去掉原生导航栏

在根目录找到 pages.json配置文件:

    ...
    {
		"path" : "pages/xiaoxi/xiaoxi",
		"style" : 
		{
			"navigationBarTitleText" : "消息",
			"navigationStyle": "custom"
		}
	},
    ...

# 2. 组件 /components/chat-navbar/chat-navbar.vue 导航栏

<template>
	<view>
		<!-- 导航栏 -->
		<view class="bg-light" :class="[fixed ? 'fixed-top' : '']">
			<!-- 状态栏 -->
			<view :style="'height:' + statusBarHeight + 'px;'"></view>
			<!-- 导航 -->
			<!-- #ifdef APP || H5 -->
			<view class="flex justify-between align-center"
			style="height: 90rpx;">
				<!-- 左边 -->
				<view>
					<!-- 标题 -->
					<text class="ml-3" v-if="title">{{title}}</text>
				</view>
				<!-- 右边 -->
				<view class="flex align-center">
					<chat-navbar-icon-button @click="openUser">
						<text class="iconfont font-lg">&#xe650;</text>
					</chat-navbar-icon-button>
					<chat-navbar-icon-button @click="openPlus">
						<text class="iconfont font-md">&#xe655;</text>
					</chat-navbar-icon-button>
				</view>
			</view>
			<!-- #endif -->
			<!-- #ifdef MP -->
			<view class="flex align-center"
			style="height: 90rpx;justify-content:space-between;">
			   <!-- 左边 -->
			   <view class="flex align-center flex-1">
				   <chat-navbar-icon-button @click="openPlus">
				   	<text class="iconfont font-md">&#xe655;</text>
				   </chat-navbar-icon-button>
				   <chat-navbar-icon-button @click="openUser">
				   	<text class="iconfont font-lg">&#xe650;</text>
				   </chat-navbar-icon-button>
			   </view>
			   <!-- 中间 -->
			   <view class="flex align-center justify-center flex-1">
				   <text class="ml-3" v-if="title">{{title}}</text>
			   </view>
			   <!-- 右边 -->
			   <view class="flex-1 bg-success"></view>
			</view>
			<!-- #endif -->
			
		</view>
		<!-- 占位符:占用 状态栏 + 导航栏的高度 -->
		<view :style="fixedHeightStyle" v-if="fixed"></view>
		
		<!-- 导航栏点击加号的弹出菜单 -->
		<chat-tooltip ref="chatTooltip" :mask="true" 
		:maskTransparent="true" 
		:isBottom="false" :tooltipWidth="260"
		:tooltipHeight="tooltipHeight"
		tooltipClass="bg-dark"
		transformOrigin="right top">
			<view class="flex flex-column flex-1">
				<!-- 菜单内容 -->
				<view class="flex-1 align-center justify-start pl-2 flex" 
				hover-class="bg-hover-dark"
					v-for="(item,index) in menuList" :key="index" @click="clickType(item.type)">
					<text class="iconfont font-md mr-3 text-white">{{item.icon}}</text>
					<text class="text-white">{{item.name}}</text>
				</view>
				<!-- 菜单箭头 -->
				<view class="position-fixed right-0"
				:style="jiantouStyle">
					<text class="iconfont font-lg text-dark">&#xe631;</text>
				</view>
			</view>
		</chat-tooltip>
		
		
	</view>
</template>

<script>
	export default {
		...,
		methods: {
			...,
			openPlus(){
				console.log('点击了加号图标在组件');
				const info = uni.getSystemInfoSync();
				let left = 5;
				// #ifdef APP || H5
				left = info.windowWidth - uni.upx2px(260) - 5;
				// #endif
				let top = this.fixedHeight;
				this.$refs.chatTooltip.show(left, top);
			},
			...
		},
		computed:{
			...,
			jiantouStyle(){
				let x = `right: 26rpx;`;
				// #ifdef MP
				x = `left: 26rpx;`;
				// #endif
				return `top:${this.fixedHeight - 6}px;${x}`;
			}
		}
	}
</script>

<style>
	...
</style>

# 十、消息页静态功能完整代码

# 1. 组件 /components/chat-navbar-icon-button/chat-navbar-icon-button.vue 导航栏图标按钮

<template>
	<view style="width: 90rpx;height: 90rpx;"
	class="flex align-center justify-center"
	hover-class="bg-hover-light"
	@click="$emit('click')">
		<slot></slot>
	</view>
</template>

<script>
	export default {
		name:"chat-navbar-icon-button",
	}
</script>

<style>
</style>

# 2. 组件 /components/chat-navbar/chat-navbar.vue 导航栏

<template>
	<view>
		<!-- 导航栏 -->
		<view class="bg-light" :class="[fixed ? 'fixed-top' : '']">
			<!-- 状态栏 -->
			<view :style="'height:' + statusBarHeight + 'px;'"></view>
			<!-- 导航 -->
			<!-- #ifdef APP || H5 -->
			<view class="flex justify-between align-center"
			style="height: 90rpx;">
				<!-- 左边 -->
				<view>
					<!-- 标题 -->
					<text class="ml-3" v-if="title">{{title}}</text>
				</view>
				<!-- 右边 -->
				<view class="flex align-center">
					<chat-navbar-icon-button @click="openUser">
						<text class="iconfont font-lg">&#xe650;</text>
					</chat-navbar-icon-button>
					<chat-navbar-icon-button @click="openPlus">
						<text class="iconfont font-md">&#xe655;</text>
					</chat-navbar-icon-button>
				</view>
			</view>
			<!-- #endif -->
			<!-- #ifdef MP -->
			<view class="flex align-center"
			style="height: 90rpx;justify-content:space-between;">
			   <!-- 左边 -->
			   <view class="flex align-center flex-1">
				   <chat-navbar-icon-button @click="openPlus">
				   	<text class="iconfont font-md">&#xe655;</text>
				   </chat-navbar-icon-button>
				   <chat-navbar-icon-button @click="openUser">
				   	<text class="iconfont font-lg">&#xe650;</text>
				   </chat-navbar-icon-button>
			   </view>
			   <!-- 中间 -->
			   <view class="flex align-center justify-center flex-1">
				   <text class="ml-3" v-if="title">{{title}}</text>
			   </view>
			   <!-- 右边 -->
			   <view class="flex-1 bg-success"></view>
			</view>
			<!-- #endif -->
			
		</view>
		<!-- 占位符:占用 状态栏 + 导航栏的高度 -->
		<view :style="fixedHeightStyle" v-if="fixed"></view>
		
		<!-- 导航栏点击加号的弹出菜单 -->
		<chat-tooltip ref="chatTooltip" :mask="true" 
		:maskTransparent="true" 
		:isBottom="false" :tooltipWidth="260"
		:tooltipHeight="tooltipHeight"
		tooltipClass="bg-dark"
		transformOrigin="right top">
			<view class="flex flex-column flex-1">
				<!-- 菜单内容 -->
				<view class="flex-1 align-center justify-start pl-2 flex" 
				hover-class="bg-hover-dark"
					v-for="(item,index) in menuList" :key="index" @click="clickType(item.type)">
					<text class="iconfont font-md mr-3 text-white">{{item.icon}}</text>
					<text class="text-white">{{item.name}}</text>
				</view>
				<!-- 菜单箭头 -->
				<view class="position-fixed right-0"
				:style="jiantouStyle">
					<text class="iconfont font-lg text-dark">&#xe631;</text>
				</view>
			</view>
		</chat-tooltip>
		
		
	</view>
</template>

<script>
	export default {
		name:"chat-navbar",
		props:{
			// 导航栏标题
			title:{
				type:String,
				default:''
			},
			// 是否固定到顶部
			fixed:{
				type:Boolean,
				default:true
			}
		},
		data() {
			return {
				statusBarHeight:0,//状态栏高度动态计算
				fixedHeight:0, //占位:状态栏+导航栏
				menuEveHeight: 100, //每个菜单默认高度是rpx
				menuList: [
					{
						name: "发起群聊",
						type: 'qunliao',
						icon:'\ue60a',
					},
					{
						name: "添加朋友",
						type: 'addfriend',
						icon:'\ue65d',
					},
					{
						name: "扫一扫",
						type: 'saoyisao',
						icon:'\ue661',
					},
				],
			}
		},
		mounted() {
			this.statusBarHeight = uni.getSystemInfoSync().statusBarHeight;
			// console.log('系统信息',uni.getSystemInfoSync());
			this.fixedHeight = this.statusBarHeight + uni.upx2px(90);
			// console.log('占位高度',this.fixedHeight);
		},
		methods: {
			openUser(){
				console.log('点击了通讯录图标');
			},
			openPlus(){
				console.log('点击了加号图标在组件');
				const info = uni.getSystemInfoSync();
				let left = 5;
				// #ifdef APP || H5
				left = info.windowWidth - uni.upx2px(260) - 5;
				// #endif
				let top = this.fixedHeight;
				this.$refs.chatTooltip.show(left, top);
			},
			clickType(e) {
				console.log('点击菜单',e);
			},
		},
		computed:{
			fixedHeightStyle(){
				return `height:${this.fixedHeight}px;`;
			},
			tooltipHeight() {
				return this.menuList.length * this.menuEveHeight;
			},
			jiantouStyle(){
				let x = `right: 26rpx;`;
				// #ifdef MP
				x = `left: 26rpx;`;
				// #endif
				return `top:${this.fixedHeight - 6}px;${x}`;
			}
		}
	}
</script>

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

# 3. 组件 /components/chat-tooltip/chat-tooltip.vue 弹出菜单

<template>
	<view style="z-index: 1000;overflow: hidden;" v-if="showstatus">
		<!-- 透明蒙版 , 在弹出菜单之后,不能点击其它地方-->
		<view v-if="mask" class="position-fixed top-0 left-0 right-0 bottom-0" :style="maskTransparentStyle"
			@click="hide"></view>
		<!-- 菜单内容 -->
		<view class="position-fixed chat-tooltip-animate" 
		:style="tooltipStyle" :class="[isBottomClass,tooltipClass]"
		ref="tooltipContent">
			<slot></slot>
		</view>
	</view>
</template>

<script>
	export default {
		name: "chat-tooltip",
		props: {
			//是否有蒙版,默认是有
			mask: {
				type: Boolean,
				default: true
			},
			//蒙版是否透明,默认是透明的
			maskTransparent: {
				type: Boolean,
				default: true
			},
			//弹出菜单位置,是否在底部,默认不在底部
			isBottom: {
				type: Boolean,
				default: false
			},
			// 弹出菜单宽度
			tooltipWidth: {
				type: Number,
				default: 0
			},
			// 弹出菜单高度
			tooltipHeight: {
				type: Number,
				default: 0
			},
			// 弹出菜单的class样式
			tooltipClass:{
				type: String,
				default: 'bg-white'
			},
			// 弹出菜单的动画方向
			transformOrigin:{
				type: String,
				default: 'left top'
			}
		},
		data() {
			return {
				showstatus: false,
				pageX: -1,
				pageY: -1,
				maxLeft: 0,
				maxTop: 0
			}
		},
		mounted() {
			const info = uni.getSystemInfoSync();
			console.log('设备信息', info);
			this.maxLeft = info.windowWidth - uni.upx2px(this.tooltipWidth);
			this.maxTop = info.windowHeight - uni.upx2px(this.tooltipHeight);
			// console.log(this.maxLeft,this.maxTop);
		},
		computed: {
			//蒙版是否透明
			maskTransparentStyle() {
				const opacity = this.maskTransparent ? 0.0 : 0.3;
				return `background-color: rgba(0, 0, 0, ${opacity});`;
			},
			//弹出菜单位置是否在底部
			isBottomClass() {
				return this.isBottom ? `left-0 right-0 bottom-0` : `border rounded`;
			},
			// 弹出菜单的位置根据坐标定
			tooltipStyle() {
				let left = this.pageX >= 0 ? `left: ${this.pageX}px;` : ``;
				let top = this.pageY >= 0 ? `top: ${this.pageY}px;` : ``;
				return `${left}${top}
				width:${uni.upx2px(this.tooltipWidth)}px;
				height:${this.tooltipHeight}rpx;`
			}
		},
		methods: {
			show(left, top) {
				console.log(this.maxLeft, this.maxTop);
				this.pageX = left > this.maxLeft ? this.maxLeft : left;
				this.pageY = top > this.maxTop ? this.maxTop : top;
				this.showstatus = true

				// #ifdef APP-PLUS-NVUE
				// 针对app端的nvue页面的动画
				this.$nextTick(() => {
					const animation = weex.requireModule('animation');
					animation.transition(this.$refs.tooltipContent, {
						styles: {
							opacity: 1,
							transform: 'scale(1, 1)',
							transformOrigin: this.transformOrigin,
						},
						duration: 100, //ms
						timingFunction: 'ease',
						needLayout: false,
						delay: 0 //ms
					}, ()=> {
						console.log('菜单动画完成');
					});
				});
				// #endif
			},
			hide() {
				// #ifdef APP-PLUS-NVUE
				const animation = weex.requireModule('animation');
				animation.transition(this.$refs.tooltipContent, {
					styles: {
						opacity: 0,
						transform: 'scale(0, 0)',
						transformOrigin:this.transformOrigin,
					},
					duration: 100, //ms
					timingFunction: 'ease',
					needLayout: false,
					delay: 0 //ms
				}, ()=> {
					console.log('菜单动画完成');
					this.showstatus = false
				});
				// #endif
				// #ifndef APP-PLUS-NVUE
				this.showstatus = false
				// #endif
			}
		}
	}
</script>

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

	/* #endif */
	/* #ifdef APP-PLUS-NVUE */
	.chat-tooltip-animate {
		opacity: 0;
		transform: scale(0, 0);
	}

	/* #endif */
</style>

# 4. 组件 /components/chat-chatlist/chat-chatlist.vue 聊天列表

<template>
	<view class="flex align-stretch" hover-class="bg-hover-light"
	@click="onClick(item,index)" @longpress="onLongpress($event,index,item)"
	:class="item.isZhiding ? 'bg-light': 'bg-white'">
		<!-- 头像 -->
		<view style="width: 150rpx;" class="flex align-center justify-center 
		position-relative ">
			<!-- 头像地址 -->
			<!-- <image :src="item.avatar" mode="widthFix" style="width: 90rpx;height: 90rpx;" 
			class="rounded">
			</image> -->
			<u--image :src="item.avatar" mode="widthFix"
			width="90rpx" height="90rpx" radius="8rpx"></u--image>
			<!-- 消息数量 角标-->
			<!-- <text class="bg-danger rounded-circle text-white font-sm position-absolute"
			style="padding-left: 14rpx;padding-right: 14rpx;
			padding-top: 2rpx;padding-bottom: 2rpx;
			top: 16rpx;right:10rpx;z-index: 100;">9</text> -->
			<u-badge :isDot="false" :value="item.datacount" 
			absolute :offset="['16rpx','10rpx']"
			max="999" shape="circle" numberType="limit"></u-badge>
		</view>
		<!-- 右边 -->
		<view class="flex flex-column border-bottom border-light-secondary flex-1 py-3 pr-3">
			<!-- 上面:昵称 + 时间 -->
			<view class="flex justify-between align-center mb-1">
				<text class="font-md">{{item.nickname}}</text>
				<text class="font-sm text-light-muted">{{item.chat_time}}</text>
			</view>
			<!-- 下面:聊天内容 -->
			<view class="pr-5">
				<text class="font text-light-muted u-line-1">{{item.data}}</text>
			</view>
		</view>
	</view>
</template>

<script>
	export default{
		name:"chat-chatlist",
		props:{
			item:Object,
			index:Number,
		},
		methods:{
			onClick(item,index){
				console.log('点击的是组件',item);
				this.$emit('click',{
					item,
					index
				});
			},
			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
				});
			}
		}
	}
</script>

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

# 5. 消息页 /pages/xiaoxi/xiaoxi.nvue

<template>
	<view>
		<!-- 导航栏 -->
		<chat-navbar title="消息(100)" :fixed="true"></chat-navbar>
		
		<!-- 消息列表 -->
		<!-- 置顶消息 -->
		<view v-for="(item,index) in chatList" :key="'zhiding_' + index">
			<chat-chatlist v-if="item.isZhiding"
			:item="item" :index="index" 
			@click="openChat" @Longpress="Longpressfn"></chat-chatlist>
		</view>
		<!-- 普通消息 -->
		<view v-for="(item,index) in chatList" :key="'normal_' + index">
			<chat-chatlist v-if="!item.isZhiding"
			:item="item" :index="index" 
			@click="openChat" @Longpress="Longpressfn"></chat-chatlist>
		</view>

		<!-- 弹出菜单 -->
		<chat-tooltip ref="chatTooltip" :mask="true" 
		:maskTransparent="false" :isBottom="false" 
		:tooltipWidth="180"
			:tooltipHeight="tooltipHeight">
			<view class="flex flex-column flex-1">
				<view class="flex-1 align-start justify-center pl-2" hover-class="bg-hover-light"
					v-for="(item,index) in menuList" :key="index" @click="clickType(item.type)">
					<text>{{item.name}}</text>
				</view>
			</view>
		</chat-tooltip>

	</view>
</template>

<script>
	export default {
		data() {
			return {
				chatListIndex:-1, //哪一条消息的索引
				menuEveHeight: 80, //每个菜单默认高度是60rpx
				menuList: [{
						name: "置顶",
						type: 'zhiding'
					},
					{
						name: "删除",
						type: 'deleteChat'
					},
				],
				chatList: [{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-01.png',
						nickname: '晓明哥',
						chat_time: '20:29',
						data: '5月30日我的武汉演唱会记得来给我当助邀嘉宾啊',
						datacount: 0,
						isZhiding:false,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-02.png',
						nickname: '热巴',
						chat_time: '20:20',
						data: '我的电影通告设计完成了吗',
						datacount: 9,
						isZhiding:true,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-03.png',
						nickname: 'GIGI',
						chat_time: '19:27',
						data: '我参加的时光音乐会节目发挥得怎么样',
						datacount: 19,
						isZhiding:false,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-04.png',
						nickname: '基仔',
						chat_time: '18:35',
						data: '最近做什么有没有新电影发布',
						datacount: 599,
						isZhiding:true,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-05.png',
						nickname: '娜扎',
						chat_time: '17:47',
						data: '最近正在拍一部古装剧,化妆师有点拉,你给我设计一下',
						datacount: 7991,
						isZhiding:false,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-06.png',
						nickname: '彦祖',
						chat_time: '16:11',
						data: '晚上过来吃饭,我今天在家里下厨,尝一下我的手艺',
						datacount: 79914,
						isZhiding:false,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
						nickname: '力宏哥',
						chat_time: '15:07',
						data: '明天一起出海晒晒太阳,这几天太忙了',
						isZhiding:false,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-08.png',
						nickname: '华仔',
						chat_time: '14:20',
						data: '今晚8点记得锁定我直播间啊',
						isZhiding:false,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-09.png',
						nickname: '黄奕姐',
						chat_time: '11:59',
						data: '干嘛呢,我的宣传海报还有几天可以给我啊',
						isZhiding:false,
					},
					{
						avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-10.png',
						nickname: '阿帅',
						chat_time: '10:20',
						data: '下午去钓鱼,去不',
						isZhiding:false,
					},
				],
			}
		},
		onLoad() {

		},
		computed: {
			tooltipHeight() {
				return this.menuList.length * this.menuEveHeight;
			}
		},
		methods: {
			clickType(e) {
				console.log('点击菜单',e);
				switch (e){
					case 'deleteChat':
					    this.deleteChat();
						break;
					case 'zhiding':
					    this.zhiding();
						break;
				}
				this.$refs.chatTooltip.hide();
			},
			// 删除某个聊天
			deleteChat(){
				this.chatList.splice(this.chatListIndex,1);
			},
			// 置顶、取消置顶
			zhiding(){
				let item = this.chatList[this.chatListIndex];
				let isZhiding = item.isZhiding;
				// item.isZhiding = isZhiding ? false : true;
				item.isZhiding = !item.isZhiding;
			},
			openChat(e) {
				console.log('点击的是消息页', e);
			},
			Longpressfn(e) {
				console.log('长按的是消息页', e);
				this.chatListIndex = e.index;
				this.menuList[0].name = e.item.isZhiding ? '取消置顶' : '置顶';
				this.$refs.chatTooltip.show(e.x, e.y);
			},
		}
	}
</script>

<style>
	/* #ifdef H5 */
	@import '/common/css/common.nvue.vue.css';
	/* #endif */
</style>
更新时间: 2025年6月15日星期日上午10点46分