# 一、撤回消息功能实现

# 1. 页面渲染数据赋值改动

在文件 /pages/chat/plusIconAction.js

...
//发送消息
sendMessage(msgType, option = {}){
    return new Promise((resolve,reject) =>{
        console.log('发送消息',msgType);
        ...
        // 组织一下消息格式和服务器一致
        ...
        // console.log('发消息格式数据',serverMsg); return;
        // 显示到页面上
        msg.avatar = serverMsg.from_avatar.startsWith('/') ? 
                        requestUrl.http + serverMsg.from_avatar : serverMsg.from_avatar ;            
        msg.nickname = serverMsg.from_name;
        msg.user_id = serverMsg.from_id;
        // 直接渲染到页面的msg信息重新赋值一下
        msg = {...serverMsg, ...msg};
        console.log('----直接渲染到页面的msg信息重新赋值一下---', msg);
        // 发送消息的状态
        // msg.sendStatus = 'pending';
        // 拿到要发送消息的索引
        let sendmsgIndex = -1;
        if(msg.previewFileIndex != undefined && msg.previewFileIndex != -1){
            // 如果是预览数据则替换预览数据
            ...
            // 替换的数据
            ...
        }else{
            ...
        }
        // 发给服务器消息
        if(msg.isSendToServer){
            console.log('发消息给服务器的数据', serverMsg);
            this.chatClass.sendmessage(serverMsg).then(result => {
                console.log('页面接收服务器返回结果',result);
                // 拿到刚发送的消息赋值
                ...
                // 执行后续操作
                ...
            }).catch(error => {
                ...
            });
        }else{
           ...
        }
        // 清空发送的内容然后还要滚动到底部
        ...
    });

},
...		

# 2. 撤回消息服务器交互

在组件 /components/chat-item/chat-item.vue

<template>
	<view class="px-3">
		<!-- 时间 -->
		...
		<!-- 撤回消息 -->
		<view v-if="item.isremove"
		class="flex align-center justify-center py-3">
			<text class="font-sm text-light-muted">{{ isMe ? '您' : item.nickname}}撤回了一条信息</text>
		</view>
		<!-- 进群首条消息提示 -->
		...
		<!-- 聊天内容 -->
		...
	</view>
</template>

<script>
    ...
	export default{
		...,
		methods:{
			...,
			clickType(e) {
				console.log('点击菜单',e);
				switch (e){
					case 'copy':
						break;
					case 'removeChatItem':
					    console.log('撤回消息', this.item);
						this.chatClass.revokeChatInfo(this.item).then((res)=>{
							// 自己的聊天页消息撤回
							this.item.isremove = true;
							// 消息页显示处理
							console.log('看一下撤回消息服务器返回的结果',res);
							// 重点注意:消息页的id单聊是聊天用户id,群聊是群id
							// 对应到res里面的是to_id
							res.id = res.to_id;
							// 消息页的聊天列表更新一下
							this.chatClass.updateXiaoXiList(res);
						});
						break;
				}
				this.$refs.chatTooltip.hide();
			},
			...
		}
	}
</script>

# 3. 撤回消息服务器方法

在类文件 /common/js/chatClass.js

    ...
    // 处理接收到的消息
	async doMessage(msg){
		console.log('处理接收到的消息',msg);
		if(msg.type == 'system'){
			console.log('系统消息单独处理');
		}else if(msg.type == 'singleChat'){
			let msgData = msg.data;
			if(msgData.actionType && msgData.actionType == 'revoke'){
				// 撤回处理
				// 获取历史记录, 传key值则找指定聊天记录
				// 不传key则构建一个聊天对象因为对方可能不在聊天页
				// this.ToObject.chatType = msgData.chatType;
				// this.ToObject.id = msgData.chatType == 'single' ? msgData.from_id : msgData.to_id; //接收人|群id 
				let keyId = msgData.chatType == 'single' ? msgData.from_id : msgData.to_id; //接收人|群id
				// 因为this.ToObject初始化默认false,无法赋值,所以我们直接传key
				let key = `chatDetail_${this.user.id}_${msgData.chatType}_${keyId}`;
				let chatInfoList = this.getChatInfo(key);
				console.log('----撤回处理获取所有历史记录----',chatInfoList);
				// 根据id找到要撤回的消息
				let index = chatInfoList.findIndex(v => v.id == msgData.id);
				if(index != -1){
					// 找到了
					let revokeMsg = chatInfoList[index];
					console.log('----找到了要撤回的消息历史记录----', revokeMsg);
					revokeMsg.isremove = 1;
					// 找到k属性
					let k = revokeMsg.k;
					// 更新指定的历史记录信息(不急着更新可以异步)
					await this.updateChatInfo(revokeMsg, k, false);
					// 消息页列表要不要更新:取决于撤回的消息是不是最新的消息
					if(chatInfoList.length - 1 == index){
						// 是最新的消息
						// 消息页修改data 和 type
						revokeMsg.data = msgData.data;
						revokeMsg.type = msgData.type;
						revokeMsg.update_time = (new Date()).getTime();
						// 消息页的聊天列表更新一下
						this.updateXiaoXiList(revokeMsg, false);
						// 全局通知数据--聊天页做更新
					}else{
						// 不是最新消息
						// 全局通知数据--聊天页做更新
					}
					// 全局通知数据--聊天页做更新
					uni.$emit('onMessage', {
						actionType: msgData.actionType,
						...revokeMsg,
					});
				}
			}else{
				// 把聊天信息存在本地
				let { data } = this.addChatInfo(msgData, false);
				// 消息页的聊天列表更新一下
				this.updateXiaoXiList(data, false);
				// 全局通知数据
				uni.$emit('onMessage', data);
			}
		}
	}
    ...
    // 撤回消息(包括跟个人或者群聊天的某条自己发送的消息)
	revokeChatInfo(options){
		return new Promise((resolve,reject)=>{
			uni.$u.http.post(requestUrl.http + `/api/chat/revokeMessage`, {
				to_id: options.to_id,
				to_name: options.to_name,
				to_avatar: options.to_avatar,
				id: options.id,
				chatType: options.chatType,
				create_time: options.create_time,
			}, {
				header: {
					token: this.user.token,
				},
			}).then(res => {
				console.log('服务器撤回消息', res);
				// 删除(与某个群所有或者某条聊天信息,与某个人的所有或者某条聊天信息)
				this.clearChatInfo(options.to_id, options.chatType, options.id)
				.then(()=>{
					console.log('撤回消息已完成我自己页面消息的撤回效果');
					resolve(res.data.data);
				});
			}).catch(err => {
				console.log('服务器撤回消息失败', err);
				uni.showToast({title: err.data.data, icon:'none'});
				reject(err);
			});
		});
	}
	

# 4. 撤回消息服务器方法接口说明

接口说明具体查看:三十二、撤回消息接口说明

# 5. 通过websocket撤回消息的用户处理

在页面 /pages/chat/chat.nvue

onLoad(e) {
    ...
    try{
        ...
        // 监听处理接收到的消息
        uni.$on('onMessage', v => {
            console.log('-----监听处理接收到的消息在聊天页------',v);	
            if((v.from_id == this.arg.id && v.chatType == 'single') ||
            (v.chatType == 'group' && this.arg.id == v.to_id)){
                if(v.actionType && v.actionType == 'revoke'){
                    // 撤回处理
                    let index = this.chatDataList.findIndex(e => e.id == v.id);
                    if(index != -1){
                        console.log('找到撤回的消息index', index);
                        this.chatDataList[index].isremove = 1;
                    }
                }else{
                    // 正常接收显示
                    this.chatDataList.push(this.formatServerMsg(v));
                }
            }
        });
        
    }catch{
        ...
    }
},
methods: {
	...
	// 接收的或者历史记录格式化成需要的聊天数据属性渲染页面
	formatServerMsg(v){
		...
		// 渲染到聊天页的数据
		let chatdata = {
			...v,
			...
			type: v.type, //image,video
			isremove: v.isremove,
			...
		};
		console.log('接收的或者历史记录渲染到页面的最终数据', chatdata);
		return chatdata;
	},

# 6. 消息页处理

在组件 /components/chat-chatlist/chat-chatlist.vue

computed:{
    ...
    // 昵称下面的小字
    showText(){
        if(this.item.type == 'text'){
            // 纯文字 和 头像文字
            if(typeof this.item.data == 'string'){
                return this.item.datadesc + this.item.data;
            }else if(typeof this.item.data == 'object'){
                return this.item.datadesc + this.item.data.data;
            }
        }else if(this.item.type == 'systemNotice'){
            let data = ``;
            if(typeof this.item.data == 'string'){
                data = this.item.data;
            }else if(typeof this.item.data == 'object'){
                data = this.item.data.data;
            }
            let mynickname = this.me.nickname || this.me.username;
            console.log('计算属性看一下我的昵称',mynickname);
            console.log('计算属性看一下data', data);
            if(data.indexOf(mynickname) > -1){
                data = data.replace(mynickname, '您');
            }
            return data;
        }else{
            return this.item.datadesc;
        }
    },
}

# 二、删除聊天页某条消息功能实现

说明:

  1. 我们把撤回消息删除消息一起讲,主要是为了让大家理解两者的区别,相对来说,删除消息比撤回消息要简单一些,因为撤回消息不仅要撤回我本地的消息,还要通知其他人,撤回他们记录中的对应消息。而删除消息,则只需要删除我本地记录中的消息。
    当然删除消息也有跟撤回消息不同的地方,就是如果你删除的消息还有上一条,则消息页应该显示上一条消息,如果聊天页就一条消息被你删除了,则聊天页应该删除这个对话。
  2. 如果同学们在学习本节课的过程中,特别是最后调试出现的效果跟老师的不一样,那可能是老师对类文件 /common/js/chatClass.js做了微调而没有及时更新文档,同学们可以下载本节课的课件,把课件中的类文件替换到你当前类文件代码,在进行调试。
  3. 或者你直接赋值下面的类文件代码,贴到你的项目中: 在类文件 /common/js/chatClass.js
import {requestUrl} from '@/common/mixins/configData.js';
import {registerGuest} from '@/pages/loginCenter/visitor.js'; //导入具体的方法
import store from '@/store'; // 引入vuex store
class chatClass {
	// 构造函数
	constructor() {
		// ws地址
		this.url = '';
		if (requestUrl.http.startsWith('http')) {
			this.url = 'ws' + requestUrl.http.replace('http', '');
		} else if (requestUrl.http.startsWith('https')) {
			this.url = 'wss' + requestUrl.http.replace('https', '');
		}
		// 是否上线
		this.isOnline = false;
		// socketTask对象
		this.chatSocket = null;
		// 我的信息 vuex 或者从本地获取
		let user = uni.getStorageSync('chatuser') ?
			JSON.parse(uni.getStorageSync('chatuser')) : '';
		this.user = user;
		// 连接websocket
		if (this.user && this.user.token) {
			this.connectSocket();
		}
		// 心跳
		this.heartbeatTimer = null;
		//聊天对象信息
		this.ToObject = false;
		//消息页整个聊天列表数据的未读数
		this.xiaoxiNoreadNum = 0;
	}

	// 连接websocket
	connectSocket() {
		this.chatSocket = uni.connectSocket({
			// http://192.168.2.7:7001
			// ws://192.168.2.7:7001/ws
			// https://lesson07.51yrc.com
			// wss://lesson07.51yrc.com/ws
			url: this.url + `/ws?token=${this.user.token}`,
			complete: () => {},
			timeout: 10000, // 10秒超时
		});

		// 连接成功
		this.chatSocket.onOpen(() => {
			console.log('websocket连接成功');
			// 调用方法
			this.onOpen();
			//启动心跳
			this.heartbeatTimer = setInterval(() => {
				if (this.chatSocket && this.chatSocket.readyState === 1) {
					this.chatSocket.send(JSON.stringify({
						type: 'ping'
					}));
				}
			}, 25000); // 每隔25秒发送一次心跳
		});
		// 接收信息
		this.chatSocket.onMessage(res => {
			// 处理一下不同平台消息格式
			try {
				const data = typeof res.data === 'string' ?
					JSON.parse(res.data) : res.data;
				//调用方法
				this.onMessage(data);

				if (data.type === 'ping') {
					// 响应心跳
					this.chatSocket.send(JSON.stringify({
						type: 'pong'
					}));
				}
			} catch (e) {
				console.error('接收信息错误', e);
			}
		});
		// 断开连接
		this.chatSocket.onClose(res => {
			console.log('断开连接的原因', res);
			clearInterval(this.heartbeatTimer); // 清除心跳
			this.heartbeatTimer = null;
			//调用方法
			this.onClose();

			// 尝试重新连接
			setTimeout(() => {
				console.log('断开连接尝试重新连接websocket');
				//dispatch('initChatuserAction');
				this.connectSocket();
			}, 3000);
		});
		// 错误处理
		this.chatSocket.onError(err => {
			console.error('websocket 错误:', err);
			clearInterval(this.heartbeatTimer); // 清除心跳
			this.heartbeatTimer = null;
			// 尝试重新连接
			setTimeout(() => {
				console.log('错误处理尝试重新连接websocket');
				//dispatch('initChatuserAction');
				this.connectSocket();
			}, 5000);
		});
	}

	// 连接成功
	onOpen() {
		// 用户上线
		this.isOnline = true;
		// 获取离线消息(不在线的时候别人或者群发的消息)
		this.chatGetmessageOffLine();
	}

	// 断开连接
	onClose() {
		// 用户下线
		this.isOnline = false;
		this.chatSocket = null;
	}

	// 接收信息
	onMessage(data) {
		console.log('websocket接收信息', data);
		// 处理接收到的消息
		this.doMessage(data);
	}

	// 关闭链接
	close() {
		// 用户退出登录
		// 调用socketTask 对象 close方法
		this.chatSocket.close();
	}

	// 创建聊天对象信息
	createChatToObject(arg) {
		this.ToObject = arg;
		console.log('聊天对象信息', this.ToObject);
	}

	// 销毁聊天对象信息
	destroyChatToObject() {
		this.ToObject = false;
	}

	// 页面发消息的格式和服务器要一致
	formatSendMessage(args) {
		return {
			id: 0, // 自动生成 UUID,唯一id, 聊天记录id,方便撤回消息
			from_avatar: this.user.avatar, // 发送者头像
			from_name: this.user.nickname || this.user.username, // 发送者名称
			from_id: this.user.id, // 发送者id
			to_id: this.ToObject.id, // 接收者id
			to_name: this.ToObject.name, // 接收者名称
			to_avatar: this.ToObject.avatar, // 接收者头像
			chatType: this.ToObject.chatType, // 聊天类型 单聊
			type: args.type, // 消息类型
			data: args.data, // 消息内容
			options: args.options ? args.options : {}, // 其它参数
			create_time: (new Date()).getTime(), // 创建时间
			isremove: 0, // 0未撤回 1已撤回
			// 发送状态 pending 发送中 success 成功 fail 失败
			sendStatus: args.sendStatus ? args.sendStatus : 'pending',
		}
	}

	// 发送消息(单聊)
	sendmessage(msg) {
		return new Promise((result, reject) => {
			// 把发送的聊天信息存在本地
			let { k } = this.addChatInfo(msg);
			// 消息页的聊天列表更新一下
			this.updateXiaoXiList(msg);
			// 查看我是否在线websocket是否正常连接
			if (!this.meIsOnline()) return reject('我掉线了');
			// 发送消息
			uni.$u.http.post(requestUrl.http + `/api/chat/socket/sendmessage`, {
				sendto_id: this.ToObject.id,
				chatType: this.ToObject.chatType, // 单聊 single 群聊 group
				type: msg.type,
				data: msg.data,
				options: encodeURIComponent(JSON.stringify(msg.options)), // 选填
			}, {
				header: {
					token: uni.getStorageSync('chatuser_token'),
				}
			}).then(res => {
				console.log('发送消息到服务器结果res', res);
				msg.id = res.data.data.id;
				msg.sendStatus = 'success';
				console.log('发送消息成功之后的msg', msg);
				// 更新本地的历史记录 send不用传因为已经发送完成了
				this.updateChatInfo(msg, k);
				// 成功返回给页面
				result(res);
			}).catch(err => {
				//console.log('发送消息到服务器失败', err);
				msg.sendStatus = 'fail';
				// 更新本地的历史记录 send不用传因为已经发送完成了
				this.updateChatInfo(msg, k);
				// 失败返回给页面
				reject(err);
			});
		});
	}
	
	// 把聊天信息存在本地
	addChatInfo(msg, isSend = true) {
		// 存本地key值设计 chatDetail_我的id_单聊群聊_和谁聊接收人(个人还是群)id
		// key:`chatDetail_${this.user.id}_${msg.chatType}_${xx}`
		// 重点分析接收人id
		// 如果是单聊则是用户id,如果是群聊,则是群id(群聊id放在消息to_id中)
		let id = msg.chatType == 'single' ? (isSend ? msg.to_id : msg.from_id) : msg.to_id;
		let key = `chatDetail_${this.user.id}_${msg.chatType}_${id}`;
		// 先获取历史记录
		let list = this.getChatInfo(key);
		console.log('获取历史记录', list);
		// 做个标识,方便之后拿具体某条历史消息
		msg.k = 'k' + list.length;
		// 将消息放入历史记录
		list.push(msg);
		// 重新存历史记录到本地(因为加了新消息)
		uni.setStorageSync(key, JSON.stringify(list));
		// 返回
		return {
			data: msg,
			k: msg.k,
		}
	}
	
	
	// 获取本地历史记录, 传key值则找指定聊天记录
	getChatInfo(key = false) {
		if (!key) {
			// 没有传key 则找当前会话聊天记录
			key = `chatDetail_${this.user.id}_${this.ToObject.chatType}_${this.ToObject.id}`;
		}
		console.log('获取历史记录, 传key值则找指定聊天记录的key,当前key值',key);
		let list = uni.getStorageSync(key);
		// console.log('获取历史记录得到的数据', list);
		if (list) {
			if (typeof list == 'string') {
				list = JSON.parse(list);
			}
		} else {
			list = [];
		}
		return list;
	}
	
	// 更新指定的本地历史记录信息(不急着更新可以异步)
	async updateChatInfo(msg, k, isSend = true) {
		// 获取原来的历史记录
		// 存本地key值设计 chatDetail_我的id_单聊群聊_和谁聊接收人(个人还是群)id
		// key:`chatDetail_${this.user.id}_${msg.chatType}_${xx}`
		// 重点分析接收人id 
		// isSend = true 代表我是发送人from_id
		// 接收人就是to_id
		// 如果是单聊则是用户id,如果是群聊,则是群id(群聊id放在消息to_id中)
		let id = msg.chatType == 'single' ? (isSend ? msg.to_id : msg.from_id) : msg.to_id; //接收人|群id 
		let key = `chatDetail_${this.user.id}_${msg.chatType}_${id}`;
		console.log('更新指定的历史记录信息key', key);
		// 先获取历史记录
		let list = this.getChatInfo(key);
		console.log('先获取历史记录', list);
		// 根据标识k去查找要更新的历史记录
		let index = list.findIndex(e => e.k == k);
		// 没找到
		if (index == -1) return;
		// 找到了,修改指定消息
		list[index] = msg;
		// 改完之后,整个重新存一下
		console.log('改完之后,整个重新存一下', key, list);
		uni.setStorageSync(key, list);
	}
	
	// 删除消息页指定的本地历史记录信息
	deleteChatInfo(to_id, chatType){
		return new Promise((resolve,reject) => {
			try{
				let xiaoxiList = this.getXiaoXiList();
				// 找到当前聊天
				let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
				if(index != -1){
					// 找到了
					// 删除这个历史记录
					xiaoxiList.splice(index, 1);
					// 处理完了之后,存储消息页列表本地历史信息
					this.setXiaoXiList(xiaoxiList);
					// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
					this.updateXiaoXiListNoreadNum();
					// 消息页,整个消息列表的数据也存入了vuex中
					// 更新一下vuex中的消息列表的数据
					uni.$emit('updateXiaoXiList', xiaoxiList);
					// 执行后续操作
					return resolve();
				}
			}catch{
				return reject();
			}
		});
	}
	
	
	// 删除或者(撤回)(与某个群所有或者某条聊天信息,与某个人的所有或者某条聊天信息)
	clearChatInfo(to_id, chatType, msgid = false, doaction = 'revoke'){
		let key = `chatDetail_${this.user.id}_${chatType}_${to_id}`;
		// 根据key获取信息
		let chatDetail = uni.getStorageSync(key);
		chatDetail = chatDetail && typeof chatDetail == 'string' ? 
		             JSON.parse(chatDetail) : chatDetail;
		console.log('本地根据key获取的聊天详细信息', chatDetail);
		if(msgid){
			// 删除与某人或者某群对话中的某一条聊天记录
			if(chatDetail){
				// 拿到具体的某条聊天信息
				let k = chatDetail.findIndex(v=> v.id === msgid);
				if(k > -1){
					console.log('删除或者(撤回)与某人或者某群对话中的某一条聊天记录', chatDetail[k]);
					if(doaction && doaction == 'delete'){
						// 删除这条记录
						chatDetail.splice(k, 1);
					}else{
						// 撤回操作
						// 将这条记录的属性isremove改成1或者true 就会自动隐藏
						chatDetail[k].isremove = 1; 
					}
					// 重新存储
					uni.setStorageSync(key, chatDetail);
				}
			}
		}else{
			// 删除与某人或者某群对话中的本地所有记录
			uni.removeStorageSync(key);
			console.log('删除与某人或者某群对话中的本地所有记录');
		}
		// 消息页数据重新处理
		return new Promise((resolve,reject) => {
			let xiaoxiList = this.getXiaoXiList();
			// 找到当前聊天
			let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
			if(index != -1){
				// 找到了
				if(msgid){
					if(chatDetail){
						// 执行后续操作 - 如:更新消息页
						return resolve(index);
					}else{
						return reject();
					}
				}else{
					// 消息页删除这个记录
					xiaoxiList.splice(index, 1);
					// 处理完了之后,存储消息页列表本地历史信息
					this.setXiaoXiList(xiaoxiList);
					// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
					this.updateXiaoXiListNoreadNum();
					// 消息页,整个消息列表的数据也存入了vuex中
					// 更新一下vuex中的消息列表的数据
					uni.$emit('updateXiaoXiList', xiaoxiList);
					// 执行后续操作
					return resolve();
				}
			}
			return reject();
		});
	}
	
	
	// 修改某个对话信息(单聊和群聊, 处理设置功能)
	// [如:置顶、免打扰、是否展示昵称、是否提醒、是否确认进群等]
	updateSomeOneChatItem(someone, updatedata){
		return new Promise((resolve,reject) => {
			let xiaoxiList = this.getXiaoXiList();
			// 找到当前聊天
			let index = xiaoxiList.findIndex(v => v.id == someone.id && 
			v.chatType == someone.chatType);
			if(index != -1){
				// 找到了
				console.log('传递过来的要更新的数据',updatedata);
				// 更新数据
				xiaoxiList[index] = {
					...xiaoxiList[index],
					// 重新赋值
					istop: updatedata.istop, //置顶
					nowarn: updatedata.nowarn, //免打扰
					stongwarn: updatedata.stongwarn, //是否提醒 
					shownickname: updatedata.shownickname, //是否显示群成员昵称
				};
				// 处理完了之后,存储消息页列表本地历史信息
				this.setXiaoXiList(xiaoxiList);
				// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
				// this.updateXiaoXiListNoreadNum();
				// 消息页,整个消息列表的数据也存入了vuex中
				// 更新一下vuex中的消息列表的数据
				uni.$emit('updateXiaoXiList', xiaoxiList);
				// 执行后续操作
				return resolve(xiaoxiList[index]);
			}
			return reject();
		});
	}
	
	
	// 消息页将某条消息更新成你指定的消息
	XiaoXiListUpdataZhiDingMsg(XiaoXiListId,chatType,msg){
		console.log('---消息页将某条消息更新成你指定的消息id---',XiaoXiListId);
		console.log('---消息页将某条消息更新成你指定的消息内容---',msg);
		// 获取消息页列表本地历史信息(消息页消息列表)
		let list = this.getXiaoXiList();
		console.log('获取消息页列表旧历史', list);
		// 查消息页列表索引
		let index = list.findIndex(v => v.chatType == chatType && v.id == XiaoXiListId);
		if(index != -1){
			// 主要是更新data部分
			let data = msg.data;
			try {
				if(typeof msg.data == 'string'){
					data = JSON.parse(msg.data);
				}
			}catch {
				data = msg.data;
			}
			// 当发送消息是图片视频等,消息页列表最新聊天的最后一条消息显示[图片][视频]等
			let isSend = msg.from_id === this.user.id ? true : false;
			let datadesc = this.XiaoXiListAnyOneLastMsgFormat(data, msg, isSend);
			// 找到了消息列表的数据,将这个数据换成指定的数据
			let findItem = list[index];
			// 更新以下内容:时间 内容 类型等等
			findItem.update_time = msg.create_time;
			findItem.data = data;
			findItem.datadesc = datadesc;
			findItem.type = msg.type;
			// findItem.avatar = avatar; // 消息页的头像不用更新,因为是跟当前聊天对象的记录
			// findItem.name = name; // 消息页的name不用更新,因为是跟当前聊天对象的记录
			findItem.isremove = msg.isremove;
			
			
			// 处理完了之后,存储消息页列表本地历史信息
			this.setXiaoXiList(list);
			// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
			this.updateXiaoXiListNoreadNum();
			// 消息页,整个消息列表的数据也存入了vuex中
			// 更新一下vuex中的消息列表的数据
			uni.$emit('updateXiaoXiList', list);
		}
	}
	
	// 消息页的聊天列表更新一下
	updateXiaoXiList(msg, isSend = true) {
		console.log('消息页最新的一条消息',msg);
		// 获取消息页列表本地历史信息(消息页消息列表)
		let list = this.getXiaoXiList();
		console.log('获取消息页列表旧历史', list);
		// 判断是不是正在和对方聊天,正在聊天页
		// 如果正在聊天页,就没有必要更新消息页的列表了
		let isCurrentChat = false; // 默认不在聊天页
		// 消息页每条数据需要配合服务器的数据,(消息页消息列表)大概有这些字段
		/*
		{
			// 单聊
			id: `用户|群id`,
			avatar: `用户|群头像`,
			name: `用户|群昵称`,
			chatType:'单聊|群聊',
			update_time: '最新的时间',
			data: '最新一条消息',
			type:'最新一条消息类型',
			noreadnum:'未读数',
			istop:'置顶情况',
			shownickname:'是否展示昵称',
			nowarn:'消息免打扰',
			stongwarn: '消息提醒'
			
			// 群聊还有以下字段
			user_id: '群管理员id',
			remark:'群公告',
			invite_confirm:'确认进群'
			...
		}
		*/
		// 重点处理上面的这几个字段
		let id = 0; //接收人|群 id
		let avatar = ''; //接收人|群 头像
		let name = ''; // 接收人|群 称呼
		// 先判断是单聊还是群聊
		if (msg.chatType == 'single') {
			//单聊 
			//先看聊天对象是否存在
			/*
			if(this.ToObject){
				// 存在聊天对象则在聊天页根据isSend判断
				// isSend为true 则我是发送者
				// 则这条消息msg.to_id 就是接收人的id 与聊天人 this.ToObject.id
				// 如果二者相等,则说明正在跟当前接收人聊天
				// 那么消息页就不用更新当前聊天人的信息比如提示发了几条消息等
				isCurrentChat = isSend ? this.ToObject.id === msg.to_id :
				// 否则我不是发送者刚好反过来
				this.ToObject.id === msg.from_id;
			}else{
				// 不存在聊天对象肯定就不在聊天页
				isCurrentChat = false;
			} */
			isCurrentChat = this.ToObject ? isSend ? this.ToObject.id === msg.to_id :
				this.ToObject.id === msg.from_id : false;
			// 处理 接收人|群 id avatar name
			id = isSend ? msg.to_id : msg.from_id;
			avatar = isSend ? msg.to_avatar : msg.from_avatar;
			name = isSend ? msg.to_name : msg.from_name;
	
		} else if (msg.chatType == 'group') {
			//群聊
			//先看聊天对象是否存在
			isCurrentChat = this.ToObject && this.ToObject.id === msg.to_id;
			// 处理 接收人|群 id avatar name
			id = msg.to_id ;
			avatar = msg.to_avatar ;
			name = msg.to_name ;
		}
	
		// 接下来看消息页消息列表是否存在跟当前聊天人的对话
		let index = list.findIndex(v => {
			// 查消息类型和接收人聊天人id
			return v.chatType == msg.chatType && v.id == id;
		});
		// 最后把消息页最新聊天的最后一条消息展示处理一下
		// let data = typeof msg.data == 'string' ? JSON.parse(msg.data) : msg.data;
		let data = msg.data;
		try {
			if(typeof msg.data == 'string'){
				data = JSON.parse(msg.data);
			}
		}catch {
			data = msg.data;
		}
		// 当发送消息是图片视频等,消息页列表最新聊天的最后一条消息显示[图片][视频]等
		let datadesc = this.XiaoXiListAnyOneLastMsgFormat(data, msg, isSend);
		
		// 字段noreadnum 未读消息数量判断
		// isSend为true说明现在处于聊天页
		// 处于聊天页或者聊天当中,那么消息页,聊天列表就没必要+1
		let noreadnum = (isSend || isCurrentChat) ? 0 : 1;
		// 看能不能查到跟当前聊天人的对话
		if (index == -1) {
			// 如果查不到,则新建一个跟当前聊天人的对话信息放到消息页列表最上面
			// 新建对话信息
			// 单聊
			let chatItem = {
				id: id,
				avatar: avatar,
				name: name,
				chatType: msg.chatType,
				update_time: (new Date()).getTime(),
				data: data,
				datadesc: datadesc,
				type: msg.type,
				noreadnum: noreadnum,
				istop: false, //是否置顶
				shownickname: 0, //是否显示昵称
				nowarn: 0, //消息免打扰
				stongwarn: 0, //是否提示来消息了
				isremove: msg.isremove, // 是否撤回
				redirect: msg.redirect ? msg.redirect : false, // 跳转字段
			};
			// 群聊
			if (msg.chatType == 'group') {
				console.log('群聊此时的消息处理', msg);
				chatItem = {
					...chatItem,
					user_id: msg.group && msg.group.user_id ? msg.group.user_id : 0, //群主
					remark: msg.group && msg.group.remark ? msg.group.remark : '', // 群公告
					// 是否需要管理员确认才能进群 默认不需要0
					invite_confirm: msg.group && msg.group.invite_confirm ? msg.group.invite_confirm : 0, 
					// 是否显示群成员昵称
					shownickname: true, //群聊默认显示
				}
			}
			// 放在最上面
			list = [chatItem, ...list];
		} else {
			// 查到了,则更新消息页,消息列表中的这条对话信息让它是最新的
			let findItem = list[index];
			// 则更新以下内容:时间 内容 类型等等
			findItem.update_time = (new Date()).getTime();
			findItem.data = data;
			findItem.datadesc = datadesc;
			findItem.type = msg.type;
			findItem.avatar = avatar;
			findItem.name = name;
			findItem.isremove = msg.isremove;
			findItem.redirect = msg.redirect ? msg.redirect : false;
			// 未读数更新
			findItem.noreadnum += noreadnum;
			console.log('查到了,则更新消息页最新一条消息', findItem);
			// 把这条消息放在消息页,消息列表最上面
			list = this.arrToFirst(list, index);
		}
	
		// 重新存一下 存储消息页列表本地历史信息
		this.setXiaoXiList(list);
	
		// 更新(获取)消息页,整个消息列表的未读数
		this.updateXiaoXiListNoreadNum(list);
	
		// 消息页,整个消息列表的数据也存入了vuex中
		// 更新一下vuex中的消息列表的数据
		uni.$emit('updateXiaoXiList', list);
		// 最后返回
		console.log('获取或更新消息页列表为最新数据', list);
		return list;
	
	}
	
	// 当发送消息是图片视频等,消息页列表最新聊天的最后一条消息显示[图片][视频]等
	XiaoXiListAnyOneLastMsgFormat(data, msg, isSend){
		console.log('消息页显示[图片][视频]等的data处理数据',data);
		// 显示到消息列表的新属性
		let datadesc = ``;
		let dataType = data.dataType ? data.dataType : msg.dataType;
		switch(dataType){
			case 'image':
			    if(data && data.otherData && data.otherData.type){
				    if(data.otherData.type == 'iconMenus'){
					    datadesc = `[表情]`;
						if(data.otherData.typedata && data.otherData.typedata.name){
							datadesc += `[${data.otherData.typedata.name}]`;
						}
				    }else if(data.otherData.type == 'image'){
						datadesc = `[图片]`;
					}
			    }else{
					if(msg && msg.otherData && msg.otherData.type){
						if(msg.otherData.type == 'iconMenus'){
						    datadesc = `[表情]`;
							if(msg.otherData.typedata && msg.otherData.typedata.name){
								datadesc += `[${msg.otherData.typedata.name}]`;
							}
						}else if(msg.otherData.type == 'image'){
							datadesc = `[图片]`;
						}
					}
				}
			    break;
			case 'audio':
			    datadesc = `[语音]`;
			    break;
			case 'video':
			    datadesc = `[视频]`;
			    break;
			case 'file':
			    datadesc = `[文件]`;
			    break;
			case 'pdf':
			    datadesc = `[pdf文件]`;
			    break;
			case 'docx':
			    datadesc = `[word文档]`;
			    break;
		}
		// 是否显示发送者
		datadesc = isSend ? datadesc : `${msg.from_name}: ${datadesc}`;
		console.log('消息页显示[图片][视频]等的显示数据',datadesc);
		return datadesc;
	}
	
	// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
	async updateXiaoXiListNoreadNum(list = false) {
		// 获取消息页列表本地历史信息
		list = !list ? this.getXiaoXiList() : list;
		// 循环list里面的每一项把属性noreadnum相加一起
		let num = 0;
		list.forEach(v => {
			num += v.noreadnum;
		});
		// 可在这里执行更新,或者赋值给实例属性在页面调用
		// 实例属性:消息页整个聊天列表数据的未读数
		this.xiaoxiNoreadNum = num;
		console.log('消息页整个聊天列表数据的未读数:', num);
		// 消息总未读数变化触发
		uni.$emit('totalNoReadNum', num);
		// 还可以返回
		return num;
	}
	
	// 数组元素置顶
	arrToFirst(arr, index) {
		// 判断:因为等于0本来就在最上面
		if (index != 0) {
			arr.unshift(arr.splice(index, 1)[0]);
		}
		return arr;
	}
	
	// 获取消息页列表本地历史信息
	getXiaoXiList() {
		// 定义消息列表key,支持多用户切换
		let key = 'chatlist_' + this.user.id;
		let list = uni.getStorageSync(key);
		return list ? JSON.parse(list) : [];
	}

	// 存储消息页列表本地历史信息
	setXiaoXiList(list) {
		// 定义消息列表key,支持多用户切换
		let key = 'chatlist_' + this.user.id;
		uni.setStorageSync(key, JSON.stringify(list));
	}

	// 查看我是否在线websocket是否正常连接
	meIsOnline() {
		if (!this.isOnline) {
			// 我不在线可以提示我确认重新连接websocket
			this.connectWebsocketcomfirm();
			return false;
		}
		return true;
	}

	// 提示我确认重新连接websocket
	connectWebsocketcomfirm(msdata = null, confirmCallback = false, cancelCallback = false) {
		uni.showModal({
			title: msdata && msdata.title ? msdata.title :  '系统提示',
			content: msdata && msdata.content ? msdata.content : '由于服务器或者网络原因,您已经掉线了,是否重新连接',
			showCancel: true,
			cancelText: msdata && msdata.cancelText ? msdata.cancelText : '取消',
			confirmText: msdata && msdata.confirmText ? msdata.confirmText : '重新连接',
			success: res => {
				if (res.confirm) {
					if(confirmCallback && typeof confirmCallback == 'function'){
						confirmCallback();
					}else{
						this.connectSocket();
					}
				}else{
					console.log('点了取消');
					if(cancelCallback && typeof cancelCallback == 'function'){
						cancelCallback();
					}
				}
			},
		});
	}
	
	// 处理接收到的消息
	async doMessage(msg){
		console.log('处理接收到的消息',msg);
		if(msg.type == 'system'){
			console.log('系统消息单独处理');
		}else if(msg.type == 'singleChat'){
			let msgData = msg.data;
			if(msgData.actionType && msgData.actionType =='revoke'){
				// 撤回操作
				// 获取历史记录, 传key值则找指定聊天记录
				// 不传key则构建一个聊天对象因为对方可能不在聊天页
				// this.ToObject.chatType = msgData.chatType;
				// this.ToObject.id = msgData.chatType == 'single' ? msgData.from_id : msgData.to_id; //接收人|群id 
				let keyId = msgData.chatType == 'single' ? msgData.from_id : msgData.to_id; //接收人|群id
				// 因为this.ToObject初始化默认false,无法赋值,所以我们直接传key
				let key = `chatDetail_${this.user.id}_${msgData.chatType}_${keyId}`;
				let chatInfoList = this.getChatInfo(key);
				console.log('----撤回处理获取所有历史记录----',chatInfoList);
				// 根据id找到要撤回的消息
				let index = chatInfoList.findIndex(v => v.id == msgData.id);
				if(index != -1){
					// 找到了
					let revokeMsg = chatInfoList[index];
					console.log('----找到了要撤回的消息历史记录----', revokeMsg);
					revokeMsg.isremove = 1;
					// 找到k属性
					let k = revokeMsg.k;
					// 更新指定的历史记录信息(不急着更新可以异步)
					await this.updateChatInfo(revokeMsg, k, false);
					// 消息页列表要不要更新:取决于撤回的消息是不是最新的消息
					if(chatInfoList.length - 1 == index){
						// 是最新的消息
						// 消息页修改data 和 type
						revokeMsg.data = msgData.data;
						revokeMsg.type = msgData.type;
						revokeMsg.update_time = (new Date()).getTime();
						// 消息页的聊天列表更新一下
						this.updateXiaoXiList(revokeMsg, false);
						// 全局通知数据--聊天页做更新
					}else{
						// 不是最新消息
						// 全局通知数据--聊天页做更新
					}
					// 全局通知数据--聊天页做更新
					uni.$emit('onMessage', {
						actionType: msgData.actionType,
						...revokeMsg,
					});
				}
			}else{
				// 把聊天信息存在本地
				let { data } = this.addChatInfo(msgData, false);
				// 消息页的聊天列表更新一下
				this.updateXiaoXiList(data, false);
				// 全局通知数据
				uni.$emit('onMessage', data);
				
			}
		}
	}
		
	// 进入聊天页,将消息页当前聊天用户的未读数清零
	async goChatPageUpdateXiaoXiNoReadNum(to_id, chatType){
		let xiaoxiList = this.getXiaoXiList();
		// 找到当前聊天
		let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
		if(index != -1){
			// 找到了
			xiaoxiList[index].noreadnum = 0;
			// 修改完了之后,存储消息页列表本地历史信息
			this.setXiaoXiList(xiaoxiList);
			// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
			this.updateXiaoXiListNoreadNum();
			// 消息页,整个消息列表的数据也存入了vuex中
			// 更新一下vuex中的消息列表的数据
			uni.$emit('updateXiaoXiList', xiaoxiList);
			
		}
	}

    // 聊天页设置相关信息获取
    getChatPageSet(to_id, chatType){
        let xiaoxiList = this.getXiaoXiList();
        // 找到当前聊天
        let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
        if(index != -1){
            // 找到了
            return xiaoxiList[index];
        }
        return null;
    }
	
	// 获取离线消息(不在线的时候别人或者群发的消息)
	chatGetmessageOffLine(){
		uni.$u.http.post(requestUrl.http + `/api/chat/chatGetmessageOffLine`, {}, {
			header: {
				token: this.user.token,
			},
		}).then(res => {
			console.log('服务器返回离线消息', res);
		}).catch(err => {
			console.log('服务器返回离线消息失败', err);
			if(err.data && err.data.data == 'Token 令牌不合法!'){
				if(this.user.role == 'visitor'){
					console.log('游客如果token令牌错误,重新获取token并连接websocket');
					this.registerGuestAndConnect();
				}else if(this.user.role == 'user'){
					console.log('登录用户token不正确,说明在的别的设备登录了,则清空本地登录信息,在换成游客模式');
					store.dispatch('logoutAction', ()=>{
						this.doRegisterGuest();
					});
				}
			}
		});
	}
	
	// 游客如果token令牌错误,重新获取token并连接websocket
	async registerGuestAndConnect(){
		try{
			this.connectWebsocketcomfirm({
				title:'来自系统的提示',
				content:'您之前在其它的设备打开过现在已掉线,是否在本设备重新连接',
			}, ()=>{
				this.doRegisterGuest();
			});
		}catch(error){
			console.error('游客获取token失败',error);
		}
	}
	
	// 游客如果token令牌错误,重新获取token并连接websocket
	async doRegisterGuest(){
		const userData = await registerGuest(store);
		console.log('游客重新获取token等信息', userData);
		
		if(userData && userData.token){
			// 更新用户信息
			this.user = userData;
			// 连接websocket
			this.connectSocket();
		}
	}
	
	// 撤回消息
	revokeChatInfo(options){
		return new Promise((resolve,reject)=>{
			uni.$u.http.post(requestUrl.http + `/api/chat/revokeMessage`,options,{
				header:{
					token: this.user.token, 
				}
			}).then(res=>{
				console.log('服务器撤回消息结果',res);
				// 删除或者(撤回)(与某个群所有或者某条聊天信息,与某个人的所有或者某条聊天信息)
				this.clearChatInfo(options.to_id, options.chatType, options.id).then(()=>{
					resolve(res.data.data);
				});
			}).catch(err=>{
				console.log('服务器撤回消息失败结果',err);
				uni.showToast({title: err.data.data, icon:'none'});
				reject(err);
			});
		});
	}
	
}

export default chatClass;

# 1. 删除聊天页某条本地消息(并返回消息页对应聊天对象索引)方法

在类文件 /common/js/chatClass.js

    // 删除或者(撤回)(与某个群所有或者某条聊天信息,与某个人的所有或者某条聊天信息)
	clearChatInfo(to_id, chatType, msgid = false, doaction = 'revoke'){
		let key = `chatDetail_${this.user.id}_${chatType}_${to_id}`;
		// 根据key获取信息
		let chatDetail = uni.getStorageSync(key);
		chatDetail = chatDetail && typeof chatDetail == 'string' ? 
		             JSON.parse(chatDetail) : chatDetail;
		console.log('本地根据key获取的聊天详细信息', chatDetail);
		if(msgid){
			// 删除与某人或者某群对话中的某一条聊天记录
			if(chatDetail){
				// 拿到具体的某条聊天信息
				let k = chatDetail.findIndex(v=> v.id === msgid);
				if(k > -1){
					console.log('删除或者(撤回)与某人或者某群对话中的某一条聊天记录', chatDetail[k]);
					if(doaction && doaction == 'delete'){
						// 删除这条记录
						chatDetail.splice(k, 1);
					}else{
						// 撤回操作
						// 将这条记录的属性isremove改成1或者true 就会自动隐藏
						chatDetail[k].isremove = 1; 
					}
					// 重新存储
					uni.setStorageSync(key, chatDetail);
				}
			}
		}else{
			// 删除与某人或者某群对话中的本地所有记录
			uni.removeStorageSync(key);
			console.log('删除与某人或者某群对话中的本地所有记录');
		}
		// 消息页数据重新处理
		return new Promise((resolve,reject) => {
			let xiaoxiList = this.getXiaoXiList();
			// 找到当前聊天
			let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
			if(index != -1){
				// 找到了
				if(msgid){
					if(chatDetail){
						// 执行后续操作 - 如:更新消息页
						return resolve(index);
					}else{
						return reject();
					}
				}else{
					// 消息页删除这个记录
					xiaoxiList.splice(index, 1);
					// 处理完了之后,存储消息页列表本地历史信息
					this.setXiaoXiList(xiaoxiList);
					// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
					this.updateXiaoXiListNoreadNum();
					// 消息页,整个消息列表的数据也存入了vuex中
					// 更新一下vuex中的消息列表的数据
					uni.$emit('updateXiaoXiList', xiaoxiList);
					// 执行后续操作
					return resolve();
				}
			}
			return reject();
		});
	}
	

# 2. 删除聊天页本地某条聊天消息

在组件 /components/chat-item/chat-item.vue

data(){
	return {
		...
		menuList: [{
				name: "删除",
				type: 'deleteChatItem'
			},
			...
		],
		...
	}
},
...
clickType(e) {
	console.log('点击菜单',e);
	switch (e){
		case 'deleteChatItem':
			// 删除自己的某条聊天记录(本地)
			// 重点处理to_id
			let to_id = this.item.chatType == 'single' ?  
			this.me.id == this.item.to_id ? 
			this.item.from_id : this.item.to_id : this.item.to_id;
			// 删除本地记录
			this.chatClass.clearChatInfo(to_id, this.item.chatType, 
			this.item.id, 'delete').then((xiaoxiListIndex)=>{
				// 通知聊天页删除这条记录
				uni.$emit('deleteChatItem',{
					item: this.item,
					xiaoxiListId: to_id, // 消息页对话的id
				});
			});
			break;
		case 'removeChatItem':
			...
			break;
	}
	...
},
...

# 3. 删除聊天页页面某条聊天消息

在页面 /pages/chat/chat.nvue

...
onLoad(e) {
	...
	try{
		...
		// 监听处理接收到的消息
		uni.$on('onMessage', v => {
			...
		});
		// 监听删除了某条聊天记录
		uni.$on('deleteChatItem', v => {
			console.log('---传过来的v---',v);
			console.log('---当前的聊天数据---',this.chatDataList);
			console.log('---v.xiaoxiListId---', v.xiaoxiListId); 
			// 聊天页最后一条消息索引
			let lastIndex = this.chatDataList.length - 1;
			// 要删除的消息索引
			let deleteIndex = this.chatDataList.findIndex(e => e.id === v.item.id);
			if(deleteIndex != -1){
				// 删除聊天页的指定聊天记录
				this.chatDataList.splice(deleteIndex, 1);
			}
			// 如果删除的消息就是最后一条消息,则还要更新一下消息页的显示
			if(deleteIndex == lastIndex){
				// 如果有上一条消息,消息页显示上一条消息
				if(deleteIndex > 0){
					let prevIndex = lastIndex - 1;
					let msg = this.chatDataList[prevIndex];
					console.log('删除时候有上一条记录', msg);
					// 消息页将某条消息更新成你指定的消息
					this.chatClass.XiaoXiListUpdataZhiDingMsg(v.xiaoxiListId, v.item.chatType, msg);
					
				}else{
					// 没有上一条消息,就一条消息被你删了
					// 删除消息页指定的历史记录信息
					this.chatClass.deleteChatInfo(v.xiaoxiListId, v.item.chatType);
				}
			}
		});
		
	}catch{
		...
	}

},
...

# 4. 调整消息页data的数据显示

在页面 /pages/xiaoxi/xiaoxi.nvue

methods: {
	// 获取消息列表
	getDataList(){
		console.log('消息列表', this.xiaoxiList);
		this.chatList = this.xiaoxiList.map(v => {
			return {
				...v,
				avatar: v.avatar.startsWith('http') ? v.avatar : requestUrl.http + v.avatar,
				nickname: v.name,
				chat_time: parseTimeJs.gettime(v.update_time),
				data: v.data && v.data.data ? v.data.data : v.data, // 调整data显示
				datacount: v.noreadnum,
				isZhiding: v.istop,
			}
		});
		console.log('消息列表最终显示到页面的数据', this.chatList);
	},
	...
}

# 5. 消息页页面显示

在组件 /components/chat-chatlist/chat-chatlist.vue

computed:{
	...
	// 昵称下面的小字
	showText(){
		if(this.item.isremove){
			return this.item.datadesc + `撤回了一条消息`;
		}
		if(this.item.type == 'text'){
			// 纯文字 和 头像文字
			if(typeof this.item.data == 'string' ||
				typeof this.item.data == 'number'){
				return this.item.datadesc + this.item.data;
			}else if(typeof this.item.data == 'object'){
				return this.item.datadesc + this.item.data.data;
			}
		}else if(this.item.type == 'systemNotice'){
			...
		}
		else{
			...
		}
	},
}

# 三、好友申请及同意添加为好友实时通知

由于我们已经使用了websocket,所以好友申请同意添加为好友,我们可以通过websocket推送过来的消息在消息页处理更直观,这部分内容属于后端处理的内容,感兴趣的可以看一下后端文档:[三、申请添加好友进行实时通知(后端文档)]。

# 1. 好友申请列表修复头像显示错误

在页面 /pages/applyMyfriend/applyMyfriend.vue

...
filters:{
	//头像
	avatarShow(item){
		if(item &&  item.user &&  item.user.avatar){
			let avatar = item &&  item.user.avatar;
			avatar = avatar && avatar.startsWith('http') ? avatar : `${requestUrl.http}${avatar}`;
			return avatar;
		}
		return ``;
	},
	...
}
...

# 2. 好友申请实时显示

如果有用户申请加你为好友,则你会收到webscoket推送过来的好友申请通知(这部分内容主要由后端处理),文档可查看:[三、申请添加好友进行实时通知(后端文档)]

# 3. 通知添加好友在消息页点击之后跳转到添加好友页面

在页面 /pages/xiaoxi/xiaoxi.nvue

...
openChat(e) {
	console.log('进入聊天详情页', e);
	// 更新一下未读消息数量
	this.chatClass.goChatPageUpdateXiaoXiNoReadNum(e.item.id,e.item.chatType);
	// 判断一下有没有跳转字段
	if(e.item.redirect && typeof e.item.redirect == 'object'){
		// 根据跳转情况判断
		uni[e.item.redirect.type]({
			url: e.item.redirect.url,
		});
		return;
	}else{
		// 进入聊天页 传一下用户的信息过去在页面展示
		const userchat = {
			id: e.item.id,
			name: e.item.name,
			avatar: e.item.avatar,
			chatType: e.item.chatType, // 单聊 single 群聊 group
		};
		uni.navigateTo({
			url: `/pages/chat/chat?arg=${encodeURIComponent(JSON.stringify(userchat))}`,
		});
	}
	
},
...

# 4. 同意添加为好友实时通知

如果对方同意加你为好友,则可以收到webscoket推送过来的消息(这部分内容主要由后端处理),文档可查看:[四、同意添加为好友实时通知(后端文档)]

# 四、删除好友

# 1. 删除好友接口说明

接口说明可查看:[三十三、删除好友]

# 2. 删除好友功能实现

在页面 /pages/setpageInfo/setpageInfo.vue

<template>
	<view>
		<!-- 导航栏 -->
		...
		<!-- 内容 -->
		<view>
			<!-- 聊天相关 -->
			<view v-if="setdata.action == 'chatset'">
				...
			</view>
			<!-- 聊天页设置 -->
			<view v-if="setdata.action == 'chatpageset'">
				<view v-if="chatpageset.id && chatpageset.chatType">
					<!-- 头像部分 -->
					...
					<!-- 群相关设置 -->
					<view v-if="chatpageset.chatType == 'group'">
						<!-- 群二维码 -->
						...
						<!-- 我在群里面的昵称 -->
						...
						<!-- 群名称 -->
						...
						<!-- 群公告 -->
						...
					</view>
					<!-- 查找聊天内容 -->
					...
					<!-- 消息设置 -->
					...
					<!-- 解散群或退群 -->
					<view>
						<view class="p-3">
							<u-button type="primary" plain :text="deleteOrQuitGroup"
							@click="deleteOrQuitGroupfn"></u-button>
						</view>
						<u-gap height="10" bgColor="#EDEDED"></u-gap>
					</view>
				</view>
			</view>
			<!-- 群二维码 -->
			...
			<!-- 扫码之后的页面 -->
			...
		</view>
	</view>
</template>

<script>
	...
	export default {
		...
		methods: {
			// 加入群聊
			...
			// 解散群或者退群或者删除该好友
			deleteOrQuitGroupfn(){
				if(this.chatpageset.chatType == 'group'){
					const arg = {
						apiurl: '/api/chat/groupDeleteOrQuit',
						data: {
							id: this.chatpageset.id,
						},
					};
					this.groupUpdate(arg).then(res => {
						uni.showToast({title:'操作成功', icon:'none'});
						if(this.chatpageset.user_id){
							return this.navigateBack();
						}else{
							uni.switchTab({
								url:'/pages/xiaoxi/xiaoxi'
							});
						}
					});
				}else if(this.chatpageset.chatType == 'single'){
					console.log('删除好友');
					uni.showModal({
						content:'确认删除该好友?',
						success: (res) => {
							if(res.confirm){
								// 执行删除
								uni.$u.http.post(requestUrl.http +
								`/api/chat/deletegoodfriend/${this.chatpageset.id}`,{},{
									header:{
										token: this.me.token,
									}
								}).then(res=>{
									console.log('删除好友服务器结果', res);
									uni.showToast({title: res.data.data,icon:'none'});
									// 清除与好友的聊天记录
									this.chatClass.clearChatInfo(this.chatpageset.id,
									this.chatpageset.chatType).then(()=>{
										// 清空完成之后聊天页的数据也要清空
										uni.$emit('clearChatInfo');
									});
									// 跳转到消息页
									setTimeout(()=>{
										uni.switchTab({
											url:'/pages/xiaoxi/xiaoxi'
										});
									},2000);
								}).catch(err=>{
									uni.showToast({title: err.data.data,icon:'none'});
								});
							}
						}
					});
				}
				
			},
			...
		}
	}
</script>


# 五、修改我的头像昵称等信息

# 1. 修改我的头像昵称接口说明

接口说明可查看:[三十四、修改我的信息(修改我的头像昵称等信息)]

# 2. 新增修改账号信息设置入口

在页面 /pages/setpage/setpage.vue

<template>
	<view>
		<!-- 导航栏 -->
		...
		<!-- 内容 -->
		<view>
			<!-- 账号 -->
			<u-cell-group>
				<u-cell title="账号信息设置"
				:customStyle="{'background-color':'#ffffff'}"
				isLink titleStyle="font-size:18px;"
				@click="setfun('userinfoSet','账号信息设置')"></u-cell>
			</u-cell-group>
			<!-- 聊天相关 -->
			...
			<!-- 退出 -->
			...
		</view>
	</view>
</template>

# 3. 修改我的头像昵称功能实现页面部分

在页面 /pages/setpageInfo/setpageInfo.vue

<template>
	<view>
		<!-- 导航栏 -->
		...
		<!-- 内容 -->
		<view>
			<!-- 聊天相关 -->
			<view v-if="setdata.action == 'chatset'">
				...
			</view>
			<!-- 聊天页设置 -->
			<view v-if="setdata.action == 'chatpageset'">
				...
			</view>
			<!-- 群二维码 -->
			<view v-if="setdata.action == 'groupQrcode'">
				...
			</view>
			<!-- 扫码之后的页面 -->
			<view v-if="setdata.action == 'autoAddGroup'">
				...
			</view>
			<!-- 账号信息设置 -->
			<view v-if="setdata.action == 'userinfoSet'">
				<!-- 账号(不可修改) -->
				<u-cell-group>
					<u-cell>
						<view slot="title">
							<view class="mb-2">
								<text class="font-weight-bold">账号(不可修改)</text>
							</view>
							<u-input v-model="chatpageset.username"
							border="none" :maxlength="20"
							:adjustPosition="false" clearable disabled
					        customStyle="padding:20rpx;"></u-input>
						</view>
					</u-cell>
				</u-cell-group>
				<u-gap height="10" bgColor="#EDEDED"></u-gap>
				<!-- 昵称 -->
				<u-cell-group>
					<u-cell>
						<view slot="title">
							<view class="mb-2">
								<text class="font-weight-bold">昵称</text>
							</view>
							<u-input v-model="chatpageset.nickname"
							border="none" :maxlength="20"
							:adjustPosition="false" clearable
							placeholder="昵称最多20个字"
							@confirm="updateUserInfo('nickname')"
							customStyle="padding:20rpx;"></u-input>
						</view>
					</u-cell>
				</u-cell-group>
				<u-gap height="10" bgColor="#EDEDED"></u-gap>
				<!-- 头像 -->
				<u-cell-group>
					<u-cell>
						<view slot="title">
							<view class="mb-2">
								<text class="font-weight-bold">头像</text>
							</view>
							<u-avatar :src="chatpageset.avatar" shape="square"
							customStyle="border:1px solid #eeeeee;" :size="36"
							@click="updateUserInfo('avatar')"></u-avatar>
						</view>
					</u-cell>
				</u-cell-group>
				<u-gap height="10" bgColor="#EDEDED"></u-gap>
				<!-- ActionSheet 操作菜单 -->
				<u-action-sheet :actions="actionSheetList" 
				title="有以下方式供您更换头像" :show="actionSheetShow"
				safeAreaInsetBottom
				@select="selectActionSheet"
				@close="actionSheetShow = false"></u-action-sheet>
			</view>
		</view>
	</view>
</template>

<script>
	...
	import updateUserInfoJs from '@/pages/setpageInfo/updateUserInfo.js';
	export default {
		mixins:[toolJs,updateUserInfoJs],
		...,
		onLoad(e) {
			...
			// 动态生成数据
			if(e.action){
				this.setdata.action = e.action;
				if(this.setdata.action == 'chatset'){
					...
				}else if(this.setdata.action == 'chatpageset'){
					...
				}else if(this.setdata.action == 'groupQrcode'){
					...
				}else if(this.setdata.action == 'autoAddGroup'){
					...
				}else if(this.setdata.action == 'userinfoSet'){
					console.log('账号信息设置', this.me);
					this.chatpageset = this.me;
				}
			}
		},
		...,
		methods: {
			...,
			// 更新群信息
			groupUpdate(arg){
				return new Promise((resolve,reject) => {
					uni.$u.http.post(requestUrl.http + `${arg.apiurl}`, arg.data , {
						header: {
							token: this.me.token,
						},
					}).then(res => {
						console.log('服务器返回群名称更新结果', res);
						// if(res.data.data == 'ok'){
						// 	resolve();
						// }
						resolve(res);
						
					}).catch(err =>{
						...
					});
				});
			},
			...
		}
	}
</script>

# 4. 修改我的头像昵称功能实现js部分

新建js文件 /pages/setpageInfo/updateUserInfo.js

import UniPermission from '@/common/mixins/uni_permission.js';
import { requestUrl, uploadfileSaveType } from '@/common/mixins/configData.js';
export default {
	data(){
		return {
			// 更换头像是拍照还是从相册选择一张
			actionSheetList:[
				{
					name:'相册',
					subname:"可从相册选择一张图片来当头像",
					color:'#1BA035',
					fontSize:'20',
					eventType:'photo',
				},
				{
					name:'拍一张',
					subname:"可拍摄一张照片当头像",
					color:'#1BA035',
					fontSize:'20',
					eventType:'cameraPhoto',
				},
			],
			actionSheetShow:false,
		}
	},
	methods: {
		// 修改我的账号信息
		updateUserInfo(fieldname) {
			let arg = {};
			switch (fieldname) {
				case 'nickname':
					arg = {
						apiurl: '/api/chat/updateUserinfo',
						data: {
							fieldname: fieldname,
							fieldValue: this.chatpageset.nickname,
						},
					};
					// 我的信息更新一下
					this.me[fieldname] = this.chatpageset.nickname;
					break;
				case 'avatar':
				    // 因为换图片需要从相册选择图片或者拍照,先看是否有权限
					return this.getUniPermission();
					break;
			}
			// 服务器处理 
			this.groupUpdate(arg).then(res => {
				// 成功处理
				uni.showToast({
					title: res.data.data,
					icon: 'none'
				});
				// 更新我在本地的信息chatuser及vuex
				this.$store.dispatch('regloginAction', this.me);
			});
		},
		// 因为换图片需要从相册选择图片或者拍照,先看是否有权限
		getUniPermission(){
			console.log('弹窗选择是拍照还是相册选择');
			this.actionSheetShow = true;
		},
		// 点击了ActionSheet 操作菜单
		async selectActionSheet(item){
			this.actionSheetShow = false;
			console.log('点击了ActionSheet 操作菜单', item);
			switch (item.eventType){
				case 'photo':
				    await this.handlePermission({
						permission:{
							name:'photo',
							title:'本功能需要您打开相册',
							desc:'需要访问您的相册来选择图片',
							grantedText:'用户已授权开启相册,可以选择照片了',
							nograntedText:'您没有授权开启相册,无法发图片',
						},
						methodsName:'chooseImage',
					});
					break;
				case 'cameraPhoto':
				    await this.handlePermission({
				    	permission:{
				    		name:'camera',
				    		title:'本功能需要您打开摄像头',
				    		desc:'需要访问您的摄像头来拍摄照片',
				    		grantedText:'用户已授权打开摄像头,可以拍摄照片了',
				    		nograntedText:'您没有授权打开摄像头,无法拍摄照片',
				    	},
				    	methodsName:'cameraPhoto',
				    });
					break;
			}
		},
		// 发相册图片|发相册视频 权限申请
		async handlePermission(options){
			try{
			   const permission = new UniPermission();
			   const granted =  await permission.requestPermission(options.permission.name,
			   options.permission.desc,options.permission.title);
			   if(granted){
				   console.log(options.permission.grantedText);
				   // 调用对应的方法
				   this[options.methodsName]();
				   
			   }else{
				   uni.showToast({
					  title: options.permission.nograntedText,
					  icon:'none',
					  duration:3000
				   });
			   }
			}catch(error){
				console.error('权限申请异常:' + error);
				uni.showToast({
					title:'权限申请失败:' + error.message,
					icon:'none',
					duration:3000
				});
			}
		},
		//选择相册照片发送
		chooseImage(){
			this.sendPhotoAlbumOrCamera({
				count:1,
				sourceType:'album',
			});
		},
		// 拍照片发送
		cameraPhoto(){
			this.sendPhotoAlbumOrCamera({
				count:1,
				sourceType:'camera',
			});
		},
		// 发照片:相册发照片| 拍照片
		sendPhotoAlbumOrCamera(option){
			const that = this;
			uni.chooseImage({
				count:option.count,
				//sizeType:['original','compressed'],
				sizeType: ['compressed'],
				sourceType:[option.sourceType],
				success: (chooseRes) => {
					console.log('选择照片chooseRes',chooseRes);
					const tempFilePath = chooseRes.tempFilePaths[0];
					// 显示加载中
					uni.showLoading({
						title: '上传中...',
						mask: true
					});
					
					// 根据配置选择上传方式
					if (uploadfileSaveType.image === 'myserver') {
						that.uploadImageToServer(tempFilePath);
					} else if (uploadfileSaveType.image === 'AliyunOSS') {
						that.uploadImageToAliyunOSS(tempFilePath);
					}
				},
			});
		},
		
		// 上传图片到服务器
		uploadImageToServer(filePath) {
			const that = this;
			// 自定义文件夹路径 - 使用avatars专门存放头像
			const imagepath = `avatars`;

			uni.uploadFile({
				url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
				filePath: filePath,
				name: 'file',
				formData: {
					token: this.me.token
				},
				header: {
					'token': this.me.token
				},
				success: (uploadRes) => {
					uni.hideLoading();
					console.log('上传到本地服务器图片结果',uploadRes);
					if (uploadRes.statusCode === 200) {
						try {
							const data = JSON.parse(uploadRes.data);
							console.log('上传到本地服务器图片数据',data);
							if (data && data.data && data.data.url) {
								// 更新头像
								that.updateAvatar(data.data.url);
							} else {
								uni.showToast({
									title: '上传失败',
									icon: 'none'
								});
							}
						} catch (e) {
							uni.showToast({
								title: '解析响应数据失败',
								icon: 'none'
							});
						}
					} else {
						uni.showToast({
							title: '上传失败',
							icon: 'none'
						});
					}
				},
				fail: (err) => {
					uni.hideLoading();
					uni.showToast({
						title: '上传失败',
						icon: 'none'
					});
					console.error('上传失败', err);
				}
			});
		},

		// 上传图片到阿里云OSS
		uploadImageToAliyunOSS(filePath) {
			const that = this;
			uni.uploadFile({
				url: requestUrl.http + `/api/chat/uploadAliyun`,
				filePath: filePath,
				name: 'img',
				formData: {
					token: this.me.token,
					imageClassId: 0
				},
				header: {
					'token': this.me.token
				},
				success: (uploadRes) => {
					uni.hideLoading();
					console.log('上传到阿里云OSS图片结果',uploadRes);
					if (uploadRes.statusCode === 200) {
						try {
							const data = JSON.parse(uploadRes.data);
							if (data && data.data && data.data.length && data.data[0].url) {
								// 更新头像
								that.updateAvatar(data.data[0].url);
							} else {
								uni.showToast({
									title: '上传失败',
									icon: 'none'
								});
							}
						} catch (e) {
							uni.showToast({
								title: '解析响应数据失败',
								icon: 'none'
							});
						}
					} else {
						uni.showToast({
							title: '上传失败',
							icon: 'none'
						});
					}
				},
				fail: (err) => {
					uni.hideLoading();
					uni.showToast({
						title: '上传失败',
						icon: 'none'
					});
					console.error('上传失败', err);
				}
			});
		},

		// 更新头像到服务器
		updateAvatar(avatarUrl) {
			const that = this;
			uni.$u.http.post(requestUrl.http + '/api/chat/updateUserinfo', {
				fieldname: 'avatar',
				fieldValue: avatarUrl
			}, {
				header: {
					token: this.me.token
				}
			}).then(res => {
				// 更新本地数据
				that.chatpageset.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
				that.me.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;

				// 更新vuex中的用户信息
				that.$store.dispatch('regloginAction', that.me);

				uni.showToast({
					title: '头像更新成功',
					icon: 'success'
				});
			}).catch(err => {
				uni.showToast({
					title: err.data.data || '更新失败',
					icon: 'none'
				});
			});
		},
	}
}

# 5. 新增更新头像(头像剪裁)处理

关于头像剪裁的说明:

  1. 头像剪裁涉及到canvas的使用,关于canvas之前课程没有讲到,后期会在专题课中讲解。
  2. 头像剪裁功能可以看成是一个公共独立功能,开发一次之后,以后在其它项目就直接拿来用,因此代码相对比较固定,对应不经常变动代码且实用的功能,我们称之为轮子, 我们应该避免重复造轮子,因此这部分代码给大家说一下流程,就不敲代码了,大家按照老师的流程,把代码补全就可以了。

# ① 页面部分

在页面 /pages/setpageInfo/setpageInfo.vue

<template>
	<view>
		<!-- 导航栏 -->
		<chat-navbar v-if="setdata.action != 'avatarCut'"
		...>
		</chat-navbar>
		<!-- 内容 -->
		<view>
			<!-- 聊天相关 -->
			<view v-if="setdata.action == 'chatset'">
				...
			</view>
			<!-- 聊天页设置 -->
			<view v-if="setdata.action == 'chatpageset'">
				...
			</view>
			<!-- 群二维码 -->
			<view v-if="setdata.action == 'groupQrcode'">
				...
			</view>
			<!-- 扫码之后的页面 -->
			<view v-if="setdata.action == 'autoAddGroup'">
				...
			</view>
			<!-- 账号信息设置 -->
			<view v-if="setdata.action == 'userinfoSet'">
				...
			</view>
            <!-- 头像剪裁500rpx * 500rpx -->
			<view v-if="setdata.action == 'avatarCut'" 
			class="flex flex-column" 
			style="height: 100vh; background-color: rgba(0,0,0,0.9);">
			    <!-- 导航栏 -->
			    <view class="position-fixed left-0 right-0 top-0">
			        <chat-navbar
			        :title="title"  :fixed="true" :showPlus="false" 
			        :showUser="false" :showBack="true"
			        navbarClass="bg-black text-white" :h5WeiXinNeedNavbar="false">
			        </chat-navbar>
			    </view>
			    
			    <!-- 剪裁区域 -->
			    <view class="flex-1 flex justify-center align-center" 
			    :style="{paddingTop: navBarHeight + 'px', paddingBottom: '100rpx'}">
			        <!-- 剪裁窗口,固定大小500rpx * 500rpx -->
			        <view class="position-relative" 
			        style="width: 500rpx; height: 500rpx;">
			            <canvas canvas-id="avatarCanvas" id="avatarCanvas" 
			                    class="position-absolute"
			                    :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', left: '0', top: '0' }"
			                    @touchstart="touchStart"
			                    @touchmove="touchMove"
			                    @touchend="touchEnd"></canvas>
			            <!-- 剪裁框 -->
			            <view class="position-absolute" 
			            style="width: 500rpx; height: 500rpx; left: 0; top: 0; box-sizing: border-box; border: 1px solid #fff; pointer-events: none;">
			                <!-- 辅助线 -->
			                <view class="position-absolute" 
			                style="width: 1px; height: 100%; left: 33.33%; background-color: rgba(255,255,255,0.4);"></view>
			                <view class="position-absolute" 
			                style="width: 1px; height: 100%; left: 66.66%; background-color: rgba(255,255,255,0.4);"></view>
			                <view class="position-absolute" 
			                style="width: 100%; height: 1px; top: 33.33%; background-color: rgba(255,255,255,0.4);"></view>
			                <view class="position-absolute" 
			                style="width: 100%; height: 1px; top: 66.66%; background-color: rgba(255,255,255,0.4);"></view>
			            </view>
			        </view>
			    </view>
			    
			    <!-- 操作按钮 -->
			    <view class="flex justify-between align-center px-5 position-fixed left-0 right-0 bottom-0 bg-black" 
			    style="height: 100rpx; z-index: 999; bottom: 180rpx;">
			        <text class="text-white" style="font-size: 32rpx;" @click="cancelCrop">取消</text>
			        <text class="text-white" style="font-size: 32rpx;" @click="confirmCrop">确定</text>
			    </view>
			</view>


		</view>
	</view>
</template>
<script>
	...
	import updateUserInfoJs from '@/pages/setpageInfo/updateUserInfo.js';
	import avatarCutJs from '@/pages/setpageInfo/avatarCut.js';
	export default {
		mixins:[toolJs,updateUserInfoJs,avatarCutJs],
		...,
	}
<script>

# ② js部分

新建处理图片剪裁功能的js文件 /pages/setpageInfo/avatarCut.js

import { requestUrl, uploadfileSaveType } from '@/common/mixins/configData.js';
export default {
    data() {
        return {
            tempFilePath: '', // 原始图片路径
            cropperReady: false, // 剪裁器是否就绪
            canvasWidth: 0, // 画布宽度
            canvasHeight: 0, // 画布高度
            scale: 1, // 缩放比例
            minScale: 0.5, // 最小缩放
            maxScale: 3, // 最大缩放
            cropperContext: null, // 画布上下文
            imageWidth: 0, // 图片宽度
            imageHeight: 0, // 图片高度
            offsetX: 0, // 图片偏移X
            offsetY: 0, // 图片偏移Y
            startX: 0, // 触摸开始X
            startY: 0, // 触摸开始Y
            initialOffsetX: 0, // 初始偏移X
            initialOffsetY: 0, // 初始偏移Y
            initialScale: 1, // 初始缩放比例
            initialDistance: 0, // 初始两指距离
            croppedWidth: 500, // 剪裁宽度(rpx,需要转换为px)
            croppedHeight: 500, // 剪裁高度(rpx,需要转换为px)
            windowWidth: 0, // 窗口宽度(px)
            windowHeight: 0, // 窗口高度(px)
            navBarHeight: 0, // 导航栏高度(px)
            isPinching: false, // 是否正在缩放
        }
    },
    methods: {
        // 初始化剪裁器
        initCropper() {
            const that = this;
            
            // 获取系统信息
            uni.getSystemInfo({
                success: function(res) {
                    that.windowWidth = res.windowWidth;
                    that.windowHeight = res.windowHeight;
                    
                    // 计算导航栏高度(假设自定义导航栏高度为90rpx + 状态栏高度)
                    const statusBarHeight = res.statusBarHeight;
                    that.navBarHeight = uni.upx2px(90) + statusBarHeight;
                    
                    // 将rpx转换为px
                    const croppedWidthPx = uni.upx2px(that.croppedWidth);
                    const croppedHeightPx = uni.upx2px(that.croppedHeight);
                    
                    // 计算可用高度(屏幕高度 - 导航栏高度 - 操作按钮高度)
                    const availableHeight = that.windowHeight - that.navBarHeight - uni.upx2px(100);
                    
                    // 画布大小设置为剪裁窗口大小
                    that.canvasWidth = croppedWidthPx;
                    that.canvasHeight = croppedHeightPx;
                    
                    // 获取图片信息
                    uni.getImageInfo({
                        src: that.tempFilePath,
                        success: function(imageInfo) {
                            that.imageWidth = imageInfo.width;
                            that.imageHeight = imageInfo.height;
                            
                            // 初始化画布
                            that.cropperContext = uni.createCanvasContext('avatarCanvas', that);
                            
                            // 计算初始缩放比例,使图片适应剪裁框
                            const widthScale = croppedWidthPx / imageInfo.width;
                            const heightScale = croppedHeightPx / imageInfo.height;
                            that.scale = Math.max(widthScale, heightScale) * 1.2; // 稍微放大一点,确保填满剪裁框
                            that.minScale = that.scale; // 最小缩放比例设为初始比例,确保图片至少填满剪裁框
                            that.maxScale = that.scale * 3;
                            
                            // 重置偏移量
                            that.offsetX = 0;
                            that.offsetY = 0;
                            
                            // 绘制图片
                            that.drawImage();
                            that.cropperReady = true;
                        },
                        fail: function(err) {
                            console.error('获取图片信息失败', err);
                            uni.showToast({
                                title: '图片加载失败',
                                icon: 'none'
                            });
                            setTimeout(() => {
                                uni.navigateBack();
                            }, 1500);
                        }
                    });
                }
            });
        },
        
        // 绘制图片到画布
        drawImage() {
            const ctx = this.cropperContext;
            
            // 清空画布
            ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
            
            // 绘制图片
            const scaledWidth = this.imageWidth * this.scale;
            const scaledHeight = this.imageHeight * this.scale;
            
            // 计算图片绘制位置,确保居中显示
            const drawX = (this.canvasWidth - scaledWidth) / 2 + this.offsetX;
            const drawY = (this.canvasHeight - scaledHeight) / 2 + this.offsetY;
            
            ctx.drawImage(
                this.tempFilePath,
                drawX, drawY,
                scaledWidth, scaledHeight
            );
            
            ctx.draw(false); // 使用非同步绘制
        },
        
        // 取消剪裁
        cancelCrop() {
            uni.navigateBack();
        },
        
        // 确认剪裁
        confirmCrop() {
            const that = this;
            if (!that.cropperReady) {
                uni.showToast({
                    title: '剪裁器未就绪',
                    icon: 'none'
                });
                return;
            }
            
            uni.showLoading({
                title: '正在剪裁...',
                mask: true
            });
            
            // 剪裁图片
            setTimeout(() => {
                uni.canvasToTempFilePath({
                    x: 0,
                    y: 0,
                    width: that.canvasWidth,
                    height: that.canvasHeight,
                    destWidth: that.croppedWidth,
                    destHeight: that.croppedHeight,
                    canvasId: 'avatarCanvas',
                    success: function(res) {
                        uni.hideLoading();
                        // 上传剪裁后的图片
                        that.uploadCroppedImage(res.tempFilePath);
                    },
                    fail: function(err) {
                        uni.hideLoading();
                        uni.showToast({
                            title: '剪裁失败',
                            icon: 'none'
                        });
                        console.error('剪裁失败', err);
                    }
                }, that);
            }, 300);
        },
        
        // 上传剪裁后的图片
        uploadCroppedImage(filePath) {
            // 显示加载中
            uni.showLoading({
                title: '上传中...',
                mask: true
            });
            
            const that = this;
            
            // 根据配置选择上传方式
            if (uploadfileSaveType.image === 'myserver') {
                that.uploadImageToServer(filePath);
            } else if (uploadfileSaveType.image === 'AliyunOSS') {
                that.uploadImageToAliyunOSS(filePath);
            }
        },
        
        // 上传图片到服务器
        uploadImageToServer(filePath) {
            const that = this;
            // 自定义文件夹路径 - 使用avatars专门存放头像
            const imagepath = `avatars`;

            uni.uploadFile({
                url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
                filePath: filePath,
                name: 'file',
                formData: {
                    token: this.me.token
                },
                header: {
                    'token': this.me.token
                },
                success: (uploadRes) => {
                    uni.hideLoading();
                    console.log('上传到本地服务器图片结果',uploadRes);
                    if (uploadRes.statusCode === 200) {
                        try {
                            const data = JSON.parse(uploadRes.data);
                            console.log('上传到本地服务器图片数据',data);
                            if (data && data.data && data.data.url) {
                                // 更新头像
                                that.updateAvatar(data.data.url);
                            } else {
                                uni.showToast({
                                    title: '上传失败',
                                    icon: 'none'
                                });
                            }
                        } catch (e) {
                            uni.showToast({
                                title: '解析响应数据失败',
                                icon: 'none'
                            });
                        }
                    } else {
                        uni.showToast({
                            title: '上传失败',
                            icon: 'none'
                        });
                    }
                },
                fail: (err) => {
                    uni.hideLoading();
                    uni.showToast({
                        title: '上传失败',
                        icon: 'none'
                    });
                    console.error('上传失败', err);
                }
            });
        },

        // 上传图片到阿里云OSS
        uploadImageToAliyunOSS(filePath) {
            const that = this;
            uni.uploadFile({
                url: requestUrl.http + `/api/chat/uploadAliyun`,
                filePath: filePath,
                name: 'img',
                formData: {
                    token: this.me.token,
                    imageClassId: 0
                },
                header: {
                    'token': this.me.token
                },
                success: (uploadRes) => {
                    uni.hideLoading();
                    console.log('上传到阿里云OSS图片结果',uploadRes);
                    if (uploadRes.statusCode === 200) {
                        try {
                            const data = JSON.parse(uploadRes.data);
                            if (data && data.data && data.data.length && data.data[0].url) {
                                // 更新头像
                                that.updateAvatar(data.data[0].url);
                            } else {
                                uni.showToast({
                                    title: '上传失败',
                                    icon: 'none'
                                });
                            }
                        } catch (e) {
                            uni.showToast({
                                title: '解析响应数据失败',
                                icon: 'none'
                            });
                        }
                    } else {
                        uni.showToast({
                            title: '上传失败',
                            icon: 'none'
                        });
                    }
                },
                fail: (err) => {
                    uni.hideLoading();
                    uni.showToast({
                        title: '上传失败',
                        icon: 'none'
                    });
                    console.error('上传失败', err);
                }
            });
        },

        // 更新头像到服务器
        updateAvatar(avatarUrl) {
            const that = this;
            uni.$u.http.post(requestUrl.http + '/api/chat/updateUserinfo', {
                fieldname: 'avatar',
                fieldValue: avatarUrl
            }, {
                header: {
                    token: this.me.token
                }
            }).then(res => {
                // 更新本地数据
                that.chatpageset.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
                that.me.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;

                // 更新vuex中的用户信息
                that.$store.dispatch('regloginAction', that.me);

                uni.showToast({
                    title: '头像更新成功',
                    icon: 'success'
                });
                // 在剪切头像页面返回上一页
                setTimeout(()=>{
                    uni.navigateBack({
                        delta: 1
                    });
                },1000);
            }).catch(err => {
                uni.showToast({
                    title: err.data.data || '更新失败',
                    icon: 'none'
                });
            });
        },
        
        // 计算两点之间的距离
        getDistance(p1, p2) {
            const dx = p2.x - p1.x;
            const dy = p2.y - p1.y;
            return Math.sqrt(dx * dx + dy * dy);
        },
        
        // 计算两点之间的中心点
        getMidpoint(p1, p2) {
            return {
                x: (p1.x + p2.x) / 2,
                y: (p1.y + p2.y) / 2
            };
        },
        
        // 触摸事件处理(实现拖拽和缩放功能)
        touchStart(e) {
            if (!this.cropperReady) return;
            
            // 单指触摸 - 拖拽
            if (e.touches.length === 1) {
                this.startX = e.touches[0].clientX;
                this.startY = e.touches[0].clientY;
                this.initialOffsetX = this.offsetX;
                this.initialOffsetY = this.offsetY;
                this.isPinching = false;
            } 
            // 双指触摸 - 缩放
            else if (e.touches.length === 2) {
                const touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
                const touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
                
                this.initialDistance = this.getDistance(touch1, touch2);
                this.initialScale = this.scale;
                this.isPinching = true;
                
                // 计算缩放中心点
                const midpoint = this.getMidpoint(touch1, touch2);
                this.scaleCenterX = midpoint.x;
                this.scaleCenterY = midpoint.y;
            }
        },
        
        touchMove(e) {
            if (!this.cropperReady) return;
            
            // 单指触摸 - 拖拽
            if (e.touches.length === 1 && !this.isPinching) {
                if (!this.startX) return;
                
                const moveX = e.touches[0].clientX - this.startX;
                const moveY = e.touches[0].clientY - this.startY;
                
                this.offsetX = this.initialOffsetX + moveX;
                this.offsetY = this.initialOffsetY + moveY;
                
                // 限制移动范围
                const scaledWidth = this.imageWidth * this.scale;
                const scaledHeight = this.imageHeight * this.scale;
                const maxX = Math.max(0, (scaledWidth - this.canvasWidth) / 2);
                const maxY = Math.max(0, (scaledHeight - this.canvasHeight) / 2);
                
                this.offsetX = Math.min(Math.max(this.offsetX, -maxX), maxX);
                this.offsetY = Math.min(Math.max(this.offsetY, -maxY), maxY);
                
                this.drawImage();
            } 
            // 双指触摸 - 缩放
            else if (e.touches.length === 2) {
                const touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
                const touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
                
                const currentDistance = this.getDistance(touch1, touch2);
                
                if (this.initialDistance > 0) {
                    // 计算缩放比例
                    let newScale = this.initialScale * (currentDistance / this.initialDistance);
                    
                    // 限制缩放范围
                    newScale = Math.min(Math.max(newScale, this.minScale), this.maxScale);
                    
                    // 计算缩放中心点在图片上的位置
                    const scaledWidth = this.imageWidth * this.scale;
                    const scaledHeight = this.imageHeight * this.scale;
                    
                    // 计算缩放中心点在图片上的相对位置
                    const imgX = (this.scaleCenterX - this.canvasWidth / 2 - this.offsetX) / this.scale;
                    const imgY = (this.scaleCenterY - this.canvasHeight / 2 - this.offsetY) / this.scale;
                    
                    // 更新缩放比例
                    this.scale = newScale;
                    
                    // 计算新的偏移量,使缩放中心点保持不变
                    const newScaledWidth = this.imageWidth * this.scale;
                    const newScaledHeight = this.imageHeight * this.scale;
                    
                    this.offsetX = this.scaleCenterX - this.canvasWidth / 2 - imgX * this.scale;
                    this.offsetY = this.scaleCenterY - this.canvasHeight / 2 - imgY * this.scale;
                    
                    // 限制移动范围
                    const maxX = Math.max(0, (newScaledWidth - this.canvasWidth) / 2);
                    const maxY = Math.max(0, (newScaledHeight - this.canvasHeight) / 2);
                    
                    this.offsetX = Math.min(Math.max(this.offsetX, -maxX), maxX);
                    this.offsetY = Math.min(Math.max(this.offsetY, -maxY), maxY);
                    
                    this.drawImage();
                }
            }
        },
        
        touchEnd(e) {
            this.startX = 0;
            this.startY = 0;
            
            // 如果只剩下一根手指,结束缩放
            if (e.touches.length === 0 && this.isPinching) {
                this.isPinching = false;
                this.initialDistance = 0;
            }
        }
    },
}

# 6. 关于修改我的头像昵称等信息几个文件完整代码

提供完整代码,供大家参考

# ① 页面 /pages/setpageInfo/setpageInfo.vue

<template>
	<view>
		<!-- 导航栏 -->
		<chat-navbar v-if="setdata.action != 'avatarCut'"
		:title="title"  :fixed="true" :showPlus="false" :showUser="false" :showBack="true"
			navbarClass="bg-white" :h5WeiXinNeedNavbar="false">
		</chat-navbar>
		<!-- 内容 -->
		<view>
			<!-- 聊天相关 -->
			<view v-if="setdata.action == 'chatset'">
				<!-- 游客(未登录用户) -->
				<u-cell-group title="游客(未登录用户)" :border="false"
				:customStyle="{'background-color':'#eeeeee'}">
				   <u-cell v-if="setdata.visitor.list.length"
				   :customStyle="{'background-color':'#ffffff'}">
				       <view slot="title" class="flex align-center border">
						   <view class="flex justify-center py-2 flex-1"
						   v-for="(item,index) in setdata.visitor.list" :key="index"
						   :class="[setdata.visitor.current == index ? 
						   'bg-success text-white' : 'bg-light']"
						   @click="setdata.visitor.current = index">
							   <text style="font-size: 24rpx;">{{item.title}}</text>
						   </view>
					   </view>
				   </u-cell>
				</u-cell-group>
				<!-- 登录用户(非好友) -->
				<u-cell-group title="登录用户(非好友)" :border="false"
				:customStyle="{'background-color':'#eeeeee'}">
				   <u-cell v-if="setdata.user.list.length"
				   :customStyle="{'background-color':'#ffffff'}">
				       <view slot="title" class="flex align-center border">
						   <view class="flex justify-center py-2 flex-1"
						   v-for="(item,index) in setdata.user.list" :key="index"
						   :class="[setdata.user.current == index ? 
						   'bg-success text-white' : 'bg-light']"
						   @click="setdata.user.current = index">
							   <text style="font-size: 24rpx;">{{item.title}}</text>
						   </view>
					   </view>
				   </u-cell>
				</u-cell-group>
				<!-- 提交设置 -->
				<view class="p-3">
					<u-button type="primary" text="确 定" @click="submitSet"
					:loading="btn.loading" :loadingText="btn.loadingText"
					:disabled="btn.disabled"></u-button>
				</view>
			</view>
			<!-- 聊天页设置 -->
			<view v-if="setdata.action == 'chatpageset'">
				<view v-if="chatpageset.id && chatpageset.chatType">
					<!-- 头像部分 -->
					<view class="flex flex-wrap p-3">
						<!-- 单聊 -->
						<view v-if="chatpageset.chatType == 'single'"
						class="flex flex-column align-center justify-center mr-2">
							<u-avatar :src="chatpageset|avatarShow" shape="square"
							customStyle="border:1px solid #eeeeee;" :size="36"></u-avatar>
							<text class="font-small mt-1 text-muted">{{chatpageset.name}}</text>
						</view>
						<!-- 群聊 循环头像昵称 -->
						<view v-if="chatpageset.chatType == 'group'"
						class="flex flex-row flex-wrap">
							<view v-for="(item,index) in chatpageset.users" :key="index"
							class="flex flex-column align-center justify-center mb-2 mr-1">
								<u-avatar :src="item|avatarShow" shape="square"
								customStyle="border:1px solid #eeeeee;" :size="36"></u-avatar>
								<text class="font-small mt-1 text-muted">{{item|groupusername}}</text>
							</view>
						</view>
						<!-- 加入群聊 -->
						<view class="border flex align-center justify-center"
						style="width: 36px;height: 36px;border-style: dashed;"
						@click="clickCell('选择好友', 'addUserToGroup')">
						   <u-icon name="plus" size="24" color="#999999"></u-icon>
						</view>
						<!-- 删除群聊用户 -->
						<view v-if="chatpageset.chatType == 'group'"
						class="border flex align-center justify-center ml-1"
						style="width: 36px;height: 36px;border-style: dashed;"
						@click="clickCell('群组删除某个用户', 'deleteUserFromGroup')">
						   <u-icon name="minus" size="24" color="#999999"></u-icon>
						</view>
					</view>
					<u-gap height="10" bgColor="#EDEDED"></u-gap>
					<!-- 群相关设置 -->
					<view v-if="chatpageset.chatType == 'group'">
						<!-- 群二维码 -->
						<u-cell-group>
							<u-cell  title="群二维码"
							:border="false" clickable isLink
							:customStyle="customStyles"
							:iconStyle="iconStyles"
							:titleStyle="titleStyles"
							@click="clickCell('群二维码', 'groupQrcode')"></u-cell>
						</u-cell-group>
						<u-gap height="10" bgColor="#EDEDED"></u-gap>
						<!-- 我在群里面的昵称 -->
						<u-cell-group>
							<u-cell>
								<view slot="title">
									<view class="mb-2">
										<text class="font-weight-bold">我在群里面的昵称</text>
									</view>
									<u-input v-model="chatpageset.nickname"
									border="none" :maxlength="20"
									:adjustPosition="false" clearable
									placeholder="群昵称最多20个字"
									@confirm="groupnickname"
									customStyle="padding:20rpx;"></u-input>
								</view>
							</u-cell>
						</u-cell-group>
						<u-gap height="10" bgColor="#EDEDED"></u-gap>
						<!-- 群名称 -->
						<u-cell-group>
							<u-cell>
								<view slot="title">
									<view class="mb-2">
										<text class="font-weight-bold">群名称</text>
									</view>
									<u-input v-model="chatpageset.name"
									border="none" :maxlength="20"
									:adjustPosition="false" clearable
									placeholder="群名称最多20个字"
									@confirm="groupUpdateName"
									:disabled="!chatpageset.user_id"
									customStyle="padding:20rpx;"></u-input>
								</view>
							</u-cell>
						</u-cell-group>
						<u-gap height="10" bgColor="#EDEDED"></u-gap>
						<!-- 群公告 -->
						<u-cell-group>
							<u-cell>
								<view slot="title">
									<view class="mb-2">
										<text class="font-weight-bold">群公告</text>
									</view>
									<u-textarea v-model="chatpageset.remark"
									border="none" :maxlength="500"
									:adjustPosition="false" 
									placeholder="群公告设置"
									count autoHeight
									:disabled="!chatpageset.user_id"
									@confirm="groupremark"></u-textarea>
								</view>
							</u-cell>
						</u-cell-group>
						<u-gap height="10" bgColor="#EDEDED"></u-gap>
					</view>
					<!-- 查找聊天内容 -->
					<u-cell-group>
						<u-cell  title="查找聊天内容"
						:border="false" clickable isLink
						:customStyle="customStyles"
						:iconStyle="iconStyles"
						:titleStyle="titleStyles"
						@click="clickCell('查找聊天内容', 'searhChatData')"></u-cell>
					</u-cell-group>
					<u-gap height="10" bgColor="#EDEDED"></u-gap>
					<!-- 消息设置 -->
					<u-cell-group>
						<u-cell  v-if="chatpageset.chatType == 'group'"
						title="是否显示群成员昵称"
						:border="false" clickable 
						:customStyle="customStyles"
						:iconStyle="iconStyles"
						:titleStyle="titleStyles">
						    <view slot="value">
						         <u-switch v-model="chatpageset.shownickname" 
								 @change="changeSwitch('shownickname')"></u-switch>
							</view>
						</u-cell>
						<u-cell  title="消息免打扰"
						:border="false" clickable 
						:customStyle="customStyles"
						:iconStyle="iconStyles"
						:titleStyle="titleStyles">
						    <view slot="value">
						         <u-switch v-model="chatpageset.nowarn" @change="changeSwitch('nowarn')"></u-switch>
							</view>
						</u-cell>
						<u-cell  title="置顶聊天"
						:border="false" clickable 
						:customStyle="customStyles"
						:iconStyle="iconStyles"
						:titleStyle="titleStyles">
						    <view slot="value">
						         <u-switch v-model="chatpageset.istop" @change="changeSwitch('istop')"></u-switch>
							</view>
						</u-cell>
						<u-cell  title="提醒"
						:border="false" clickable 
						:customStyle="customStyles"
						:iconStyle="iconStyles"
						:titleStyle="titleStyles">
						    <view slot="value">
						        <u-switch v-model="chatpageset.stongwarn" @change="changeSwitch('stongwarn')"></u-switch>
							</view>
						</u-cell>
					</u-cell-group>
					<u-gap height="10" bgColor="#EDEDED"></u-gap>
					<u-cell-group>
						<u-cell  title="清空聊天记录"
						:border="false" clickable isLink
						:customStyle="customStyles"
						:iconStyle="iconStyles"
						:titleStyle="titleStyles"
						@click="clickCell('清空聊天记录', 'deleteChatData')"></u-cell>
					</u-cell-group>
					<u-gap height="10" bgColor="#EDEDED"></u-gap>
					<!-- 解散群或退群或者删除好友 -->
					<view>
						<view class="p-3">
							<u-button type="primary" plain :text="deleteOrQuitGroup"
							@click="deleteOrQuitGroupfn"></u-button>
						</view>
						<u-gap height="10" bgColor="#EDEDED"></u-gap>
					</view>
				</view>
			</view>
			<!-- 群二维码 -->
			<view v-if="setdata.action == 'groupQrcode'">
				<view class="m-3 bg-light p-3">
					<!-- 群名称 -->
					<view class="mb-3 flex align-center justify-center p-1">
						<text class="text-muted">群名称:{{chatpageset.name}}</text>
					</view>
					<!-- 群二维码 -->
					<view class="mb-3 flex align-center justify-center">
						<view class="p-1 border bg-white flex align-center justify-center">
							<image :src="chatpageset.src"
							style="width: 600rpx;height: 600rpx;"></image>
						</view>
					</view>
					<!-- 提示 -->
					<view class="mb-3 flex align-center justify-center">
						<text class="text-muted">{{chatpageset.sysinfo}}</text>
					</view>
				</view>
			</view>
			<!-- 扫码之后的页面 -->
			<view v-if="setdata.action == 'autoAddGroup'">
				<!-- 群头像及介绍 -->
				<view class="m-3 bg-light p-3 flex flex-row align-center">
					<!-- 群头像 -->
					<view v-if="avatar && avatar.length"
					style="width: 150rpx;" class="flex align-center justify-center position-relative">
						<!-- 一张 -->
						<view v-if="avatar.length == 1">
							<u--image :src="avatar[0]" mode="widthFix"
							width="90rpx" height="90rpx" radius="8rpx"></u--image>
						</view>
						<!-- 多张 -->
						<view v-else style="width: 90rpx;background-color: #eeeeee;"
						class="rounded">
						    <!-- 2张 3张 4张 -->
							<view v-if="avatar.length == 2 || avatar.length == 3 || avatar.length == 4"
							class="flex flex-row flex-wrap align-center justify-center"
							style="height: 90rpx;align-content: center;">
							    <u-avatar v-for="(v,k) in avatar" :key="k"
								:src="v" shape="square" size="45rpx"></u-avatar>
							</view>
							<!-- 5张到9张 -->
							<view v-if="avatar.length >= 5"
							class="flex flex-row flex-wrap align-center justify-center"
							style="height: 90rpx;align-content: center;">
							    <u-avatar v-for="(v,k) in avatar" :key="k"
								:src="v" shape="square" size="30rpx"></u-avatar>
							</view>
						</view>
						
					</view>
				    <!-- 群名称及介绍 -->
					<view class="flex flex-column justify-center ml-2">
						<text class="mb-1 u-line-1">{{chatpageset.name}}</text>
						<text class="text-muted mb-1 font-sm">{{chatpageset.users.length}}人加入</text>
					</view>
				</view>
				<!-- 加群按钮 -->
				<view class="p-3">
					<u-button text="加入群聊" type="success" @click="addGroupfn"></u-button>
				</view>
				<!-- 群公告 -->
				<view v-if="chatpageset.remark"
				class="p-3 flex flex-column">
				    <text class="font mb-2">群介绍</text>
					<view class="bg-light p-3"
					style="text-align: justify;">
						<text class="font-sm">{{chatpageset.remark}}</text>
					</view>
				</view>
				<!-- h5端页面在微信打开没有导航栏给它一个底部导航栏 -->
				<view v-if="h5weixin">
					<u-tabbar
					  :value="0"
					  :fixed="true"
					  :placeholder="true"
					  :safeAreaInsetBottom="true"
					>
					  <u-tabbar-item text="首页" icon="home"
					  @click="clickCell('首页','switchTab', '/')"></u-tabbar-item>
					  <u-tabbar-item text="消息" icon="chat" :badge="1"
					  @click="clickCell('消息','switchTab', '/pages/xiaoxi/xiaoxi')"></u-tabbar-item>
					  <u-tabbar-item text="我的" icon="account"
					  @click="clickCell('我的','navigateTo', '/pages/wode/wode')"></u-tabbar-item>
					</u-tabbar>
				</view>
			</view>
			<!-- 账号信息设置 -->
			<view v-if="setdata.action == 'userinfoSet'">
				<!-- 账号(不可修改) -->
				<u-cell-group>
					<u-cell>
						<view slot="title">
							<view class="mb-2">
								<text class="font-weight-bold">账号(不可修改)</text>
							</view>
							<u-input v-model="chatpageset.username"
							border="none" :maxlength="20"
							:adjustPosition="false" clearable disabled
					        customStyle="padding:20rpx;"></u-input>
						</view>
					</u-cell>
				</u-cell-group>
				<u-gap height="10" bgColor="#EDEDED"></u-gap>
				<!-- 昵称 -->
				<u-cell-group>
					<u-cell>
						<view slot="title">
							<view class="mb-2">
								<text class="font-weight-bold">昵称</text>
							</view>
							<u-input v-model="chatpageset.nickname"
							border="none" :maxlength="20"
							:adjustPosition="false" clearable
							placeholder="昵称最多20个字"
							@confirm="updateUserInfo('nickname')"
							customStyle="padding:20rpx;"></u-input>
						</view>
					</u-cell>
				</u-cell-group>
				<u-gap height="10" bgColor="#EDEDED"></u-gap>
				<!-- 头像 -->
				<u-cell-group>
					<u-cell>
						<view slot="title">
							<view class="mb-2">
								<text class="font-weight-bold">头像</text>
							</view>
							<u-avatar :src="chatpageset.avatar" shape="square"
							customStyle="border:1px solid #eeeeee;" :size="36"
							@click="updateUserInfo('avatar')"></u-avatar>
						</view>
					</u-cell>
				</u-cell-group>
				<u-gap height="10" bgColor="#EDEDED"></u-gap>
				<!-- ActionSheet 操作菜单 -->
				<u-action-sheet :actions="actionSheetList" 
				title="有以下方式供您更换头像" :show="actionSheetShow"
				safeAreaInsetBottom
				@select="selectActionSheet"
				@close="actionSheetShow = false"></u-action-sheet>
			</view>
			<!-- 头像剪裁500rpx * 500rpx -->
			<view v-if="setdata.action == 'avatarCut'" 
			class="flex flex-column" 
			style="height: 100vh; background-color: rgba(0,0,0,0.9);">
			    <!-- 导航栏 -->
			    <view class="position-fixed left-0 right-0 top-0">
			        <chat-navbar
			        :title="title"  :fixed="true" :showPlus="false" 
			        :showUser="false" :showBack="true"
			        navbarClass="bg-black text-white" :h5WeiXinNeedNavbar="false">
			        </chat-navbar>
			    </view>
			    
			    <!-- 剪裁区域 -->
			    <view class="flex-1 flex justify-center align-center" 
			    :style="{paddingTop: navBarHeight + 'px', paddingBottom: '100rpx'}">
			        <!-- 剪裁窗口,固定大小500rpx * 500rpx -->
			        <view class="position-relative" 
			        style="width: 500rpx; height: 500rpx;">
			            <canvas canvas-id="avatarCanvas" id="avatarCanvas" 
			                    class="position-absolute"
			                    :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', left: '0', top: '0' }"
			                    @touchstart="touchStart"
			                    @touchmove="touchMove"
			                    @touchend="touchEnd"></canvas>
			            <!-- 剪裁框 -->
			            <view class="position-absolute" 
			            style="width: 500rpx; height: 500rpx; left: 0; top: 0; box-sizing: border-box; border: 1px solid #fff; pointer-events: none;">
			                <!-- 辅助线 -->
			                <view class="position-absolute" 
			                style="width: 1px; height: 100%; left: 33.33%; background-color: rgba(255,255,255,0.4);"></view>
			                <view class="position-absolute" 
			                style="width: 1px; height: 100%; left: 66.66%; background-color: rgba(255,255,255,0.4);"></view>
			                <view class="position-absolute" 
			                style="width: 100%; height: 1px; top: 33.33%; background-color: rgba(255,255,255,0.4);"></view>
			                <view class="position-absolute" 
			                style="width: 100%; height: 1px; top: 66.66%; background-color: rgba(255,255,255,0.4);"></view>
			            </view>
			        </view>
			    </view>
			    
			    <!-- 操作按钮 -->
			    <view class="flex justify-between align-center px-5 position-fixed left-0 right-0 bottom-0 bg-black" 
			    style="height: 100rpx; z-index: 999; bottom: 180rpx;">
			        <text class="text-white" style="font-size: 32rpx;" @click="cancelCrop">取消</text>
			        <text class="text-white" style="font-size: 32rpx;" @click="confirmCrop">确定</text>
			    </view>
			</view>
		
		</view>
	</view>
</template>

<script>
	import toolJs from '@/common/mixins/tool.js';
	import {requestUrl} from '@/common/mixins/configData.js';
	import {mapState,mapGetters,mapMutations,mapActions} from 'vuex';
	import updateUserInfoJs from '@/pages/setpageInfo/updateUserInfo.js';
	import avatarCutJs from '@/pages/setpageInfo/avatarCut.js';
	export default {
		mixins:[toolJs,updateUserInfoJs,avatarCutJs],
		data() {
			return {
				title:'',
				setdata:{
					visitor:{},
					user:{},
				},
				btn:{
					loading:false,
					loadingText:'正在提交中',
					disabled:false,
				},
				chatpageset:{
					users:[],
					avatar:'',
				},
			}
		},
		onLoad(e) {
			console.log(e);
			// if(!e.action || !this.me || this.me.role == 'visitor') return this.navigateBack();
			if(!e.action || !this.me) return this.navigateBack();
			if(e.title){
				this.title = decodeURIComponent(e.title);
				uni.setNavigationBarTitle({
					title:this.title,
				});
			}
			// 动态生成数据
			if(e.action){
				this.setdata.action = e.action;
				if(this.setdata.action == 'chatset'){
					// 针对聊天的设置信息
					this.setdata.visitor = {
						list:[
							{ title: '禁止聊天(先登录)', value:0 },
							{ title: '可以向我发一条消息', value:1 },
							{ title: '可以聊天', value:2 },
						],
						current:1,
					};
					this.setdata.user = {
						list:[
							{ title: '禁止聊天(先加好友)', value:0 },
							{ title: '可以向我发一条消息', value:1 },
							{ title: '可以聊天', value:2 },
						],
						current:1,
					};
					// 获取用户的设置信息
					uni.$u.http.get(requestUrl.http + `/api/userinfo/${this.me.uuid}`).then(res => {
						console.log('获取用户的设置信息结果', res);
						let userset = res.data.data.userset;
						if(userset){
							userset = JSON.parse(userset);
							console.log('userset转对象的结果',userset);
							this.setdata.visitor.current = userset.chatset.visitor.sendCount;
							this.setdata.user.current = userset.chatset.user.sendCount;
						}
					})
				}else if(this.setdata.action == 'chatpageset'){
					this.chatpageset.id = e.id;
					this.chatpageset.chatType = e.chatType;
					this.chatpageset.avatar = decodeURIComponent(e.avatar);
					this.chatpageset.name = decodeURIComponent(e.name);
					// 聊天页设置相关信息获取
					let getChatPageSet = this.chatClass.getChatPageSet(this.chatpageset.id, this.chatpageset.chatType);
					console.log('聊天页设置相关信息获取',getChatPageSet);
					// 设置相关
					this.chatpageset = {
						// 单聊
						...this.chatpageset,
						istop: getChatPageSet && getChatPageSet.istop ? true : false,
						shownickname: getChatPageSet && getChatPageSet.shownickname ? true : false,
						nowarn: getChatPageSet && getChatPageSet.nowarn ? true : false,
						stongwarn: getChatPageSet && getChatPageSet.stongwarn ? true : false,
						
						// 群聊还有以下字段
						user_id: 0,
						remark: '',
						invite_confirm: 0,
					}
					console.log('设置相关', this.chatpageset);
					// 群聊设置
					if(this.chatpageset.chatType == 'group'){
						this.getgroupinfo();
					}
				}else if(this.setdata.action == 'groupQrcode'){
					this.chatpageset.id = e.id;
					this.chatpageset.chatType = e.chatType;
					this.chatpageset.avatar = decodeURIComponent(e.avatar);
					this.chatpageset.name = decodeURIComponent(e.name);
					console.log('二维码页面信息',this.chatpageset);
					
					// 获取二维码
					// #ifdef H5
					// 二维码返回的地址举例:http://192.168.2.6:8081/#/pages/setpageInfo/setpageInfo?action=autoAddGroup&title=群介绍&id=33&chatType=group
					// 生成二维码
					// 得的当前完整的网址
					const fullURL = window.location.href;
					// 截取#之前的部分
					const http = fullURL.split('#')[0];
					this.chatpageset.src = requestUrl.http + `/api/chat/groupQrcode/${this.chatpageset.id}?token=${this.me.token}&type=H5&http=${encodeURIComponent(http)}&chatType=${this.chatpageset.chatType}`;
					// #endif
					// #ifdef APP || MP
					this.chatpageset.src = requestUrl.http + `/api/chat/groupQrcode/${this.chatpageset.id}?token=${this.me.token}`;
					// #endif
					
					//二维码提示说明
					let sysinfo = uni.getSystemInfoSync().appName;
					// #ifdef H5
					this.chatpageset.sysinfo = `使用微信或者手机浏览器扫码加群`;
					// #endif
					// #ifdef MP
					this.chatpageset.sysinfo = `使用${sysinfo}小程序扫码加群`;
					// #endif
					// #ifdef APP
					this.chatpageset.sysinfo = `使用${sysinfo}APP扫码加群`;
					// #endif
				}else if(this.setdata.action == 'autoAddGroup'){
					console.log('扫码之后打开的地址');
					this.chatpageset.id = e.id;
					this.chatpageset.chatType = e.chatType;
					// 获取群资料
					if(this.chatpageset.chatType == 'group'){
						this.getgroupinfo();
					}
				}else if(this.setdata.action == 'userinfoSet'){
					console.log('账号信息设置', this.me);
					this.chatpageset = this.me;
				}else if(this.setdata.action == 'avatarCut'){
					this.tempFilePath = decodeURIComponent(e.tempFilePath);
					this.title = decodeURIComponent(e.title);
					uni.setNavigationBarTitle({
						title: this.title,
					});
					
					// 初始化剪裁器
					this.$nextTick(() => {
						setTimeout(() => {
							this.initCropper();
						}, 300);
					});
				}
			}
		},
		filters:{
			//头像
			avatarShow(item){
				if(item){
					if(item.avatar){
						return item.avatar.startsWith('http') ? item.avatar : `${requestUrl.http}${item.avatar}`;
					}
					return item.user.avatar.startsWith('http') ? item.user.avatar : `${requestUrl.http}${item.user.avatar}`;
				}
				return ``;
			},
			// 群用户称呼
			groupusername(item){
				if(item){
					if(item.nickname){
						return item.nickname.length > 3 ? item.nickname.substring(0,3) + `...` : item.nickname;
					}
					if(item.user.nickname){
						return item.user.nickname.length > 3 ? item.user.nickname.substring(0,3) + `...` : 
						item.user.nickname;
					}
					if(item.user.username){
						return item.user.username.length > 3 ? item.user.username.substring(0,3) + `...` : 
						item.user.username;
					}
				}
				return ``;
			}
		},
		computed:{
			...mapState({
				me : state => state.Chatuser.regloginUser,
				chatClass : state => state.Chatuser.chatClass,
			}),
			customStyles(){
				return `padding-top:20rpx;padding-bottom:20rpx`;
			},
			iconStyles(){
				return `font-size:52rpx;margin-right:30rpx;color:#5696da`;
			},
			titleStyles(){
				return `font-size:32rpx;`;
			},
			// 解散群或退群
			deleteOrQuitGroup(){
				if(this.chatpageset.chatType == 'group'){
					return this.chatpageset.user_id ? '解散该群' : '退出该群';
				}else if(this.chatpageset.chatType == 'single'){
					return '删除该好友';
				}
				return ``;
			},
			// 群头像
			avatar(){
				if(this.chatpageset && this.chatpageset.avatar){
					let arr = this.chatpageset.avatar.split(',');
					arr = arr.map(v => {
						if(v.startsWith('http')){
							return v;
						}else{
							return requestUrl.http + v;
						}
					});
					// console.log('头像',arr);
					return arr;
				}
				return [];
			},
			// h5端在微信打开
			h5weixin(){
				return this.isWeixinBrowser();
			},
		},
		methods: {
			// 加入群聊
			addGroupfn(){
				console.log('加入群聊,看管理员怎么设置的');
			},
			// 解散群或者退群或者删除好友
			deleteOrQuitGroupfn(){
				if(this.chatpageset.chatType == 'group'){
					const arg = {
						apiurl: '/api/chat/groupDeleteOrQuit',
						data: {
							id: this.chatpageset.id,
						},
					};
					this.groupUpdate(arg).then(res => {
						uni.showToast({title:'操作成功', icon:'none'});
						if(this.chatpageset.user_id){
							return this.navigateBack();
						}else{
							uni.switchTab({
								url:'/pages/xiaoxi/xiaoxi'
							});
						}
					});
				}else if(this.chatpageset.chatType == 'single'){
					console.log('删除好友');
					uni.showModal({
						content:`确定删除这个好友吗?`,
						success: (res) => {
							if(res.confirm){
								// 执行删除
								uni.$u.http.post(requestUrl.http + 
								`/api/chat/deletegoodfriend/${this.chatpageset.id}`,{},{
									header:{
										token: this.me.token,
									}
								}).then(res=>{
									uni.showToast({title:res.data.data, icon:'none'});
									// 消息页清除聊天记录
									this.chatClass.clearChatInfo(this.chatpageset.id,
									this.chatpageset.chatType).then(()=>{
										// 清空完成之后聊天页的数据也要清空
										uni.$emit('clearChatInfo');
									});
									// 调转消息页
									setTimeout(()=>{
										uni.switchTab({
											url:'/pages/xiaoxi/xiaoxi'
										});
									},2000);
								}).catch(err=>{
									uni.showToast({title:err.data.data, icon:'none'});
								});
							}
						}
					});
				}
				
			},
			// 获取群资料
			getgroupinfo(){
				uni.$u.http.get(requestUrl.http + `/api/chat/groupinfo/${this.chatpageset.id}`, {
					header: {
						token: this.me.token,
					},
				}).then(res => {
					console.log('服务器返回群资料', res);
					let info = res.data.data;
					this.chatpageset.user_id = info.user_id;
					this.chatpageset.remark = info.remark;
					this.chatpageset.invite_confirm = info.invite_confirm;
					this.chatpageset.users = info.group_users;
					//console.log('显示到页面的群资料',this.chatpageset);
					this.chatpageset.user_id =  this.chatpageset.user_id == this.me.id ? true : false; 
					// 我在群里面的昵称
					let me_index = this.chatpageset.users.findIndex(v => v.user_id == this.me.id);
					if(me_index != -1){
						this.chatpageset.nickname = this.chatpageset.users[me_index].nickname;
					}
					// 群头像
					this.chatpageset.avatar = info.avatar;
					// 群名称
					this.chatpageset.name = info.name;
					console.log('显示到页面的群资料',this.chatpageset);
					
				}).catch(err =>{
					this.navigateBack();
				});
			},
			// 修改我在群里面的昵称
			groupnickname(){
				const arg = {
					apiurl: '/api/chat/groupnickname',
					data: {
						id: this.chatpageset.id,
						nickname: this.chatpageset.nickname,
					},
				};
				this.groupUpdate(arg).then(res => {
					uni.showToast({title:'群昵称设置成功', icon:'none'});
					this.getgroupinfo();
				});
			},
			// 更新群公告
			groupremark(){
				const arg = {
					apiurl: '/api/chat/groupremark',
					data: {
						id: this.chatpageset.id,
						remark: this.chatpageset.remark,
					},
				};
				this.groupUpdate(arg).then(res => {
					uni.showToast({title:'群公告修改成功', icon:'none'});
				});
			},
			// 更新群名称
			groupUpdateName(){
				const arg = {
					apiurl: '/api/chat/groupUpdateName',
					data: {
						id: this.chatpageset.id,
						name: this.chatpageset.name,
					},
				};
				this.groupUpdate(arg).then(res => {
					uni.showToast({title:'群名称修改成功', icon:'none'});
					// 使用vuex兼容H5端nvue页面标题
					this.$store.dispatch('groupUpdateName',{
						name: this.chatpageset.name
					});
				});
			},
			// 更新群信息
			groupUpdate(arg){
				return new Promise((resolve,reject) => {
					uni.$u.http.post(requestUrl.http + `${arg.apiurl}`, arg.data , {
						header: {
							token: this.me.token,
						},
					}).then(res => {
						console.log('服务器返回群名称更新结果', res);
						// if(res.data.data == 'ok'){
						// 	resolve();
						// }
						resolve(res);
						
					}).catch(err =>{
						uni.showModal({
							content:err.data.data,
							showCancel:false,
							confirmText:'我知道了'
						});
					});
				});
			},
			// 点击开关
			changeSwitch(key){
				console.log(`点击开关${key}`,this.chatpageset[key]);
				console.log('通过开关设置单聊或者群聊信息',this.chatpageset);
				this.chatClass.updateSomeOneChatItem({
					id: this.chatpageset.id,
					chatType: this.chatpageset.chatType,
				}, this.chatpageset).then(res =>{
					console.log('设置之后的数据', res);
					// 方式一: 全局通知聊天页
					// 方式二:直接在聊天页获取消息页的信息进行处理
				});
			},
			clickCell(title, type, url = ''){
				console.log(title, type);
				if(type == 'addUserToGroup'){
					// 单独建群
					if(this.chatpageset.chatType == 'single'){
						let op = {
							action: 'addUserToGroup',
							title: encodeURIComponent(title),
						};
						uni.navigateTo({
							url: `/pages/friendsList/friendsList?action=${op.action}&title=${op.title}`,
						});
					}else if(this.chatpageset.chatType == 'group'){
						console.log('往当前的群里面加人');
					}
					
				}else if(type == 'groupQrcode'){
					console.log('打开群二维码',this.chatpageset);
					let op = {
						action: 'groupQrcode',
						title: encodeURIComponent(title),
						id: this.chatpageset.id,
						name: encodeURIComponent(this.chatpageset.name),
						avatar: encodeURIComponent(this.chatpageset.avatar),
						chatType: this.chatpageset.chatType,
					};
					uni.redirectTo({
						url:`/pages/setpageInfo/setpageInfo?action=${op.action}&title=${op.title}&id=${op.id}&name=${op.name}&avatar=${op.avatar}&chatType=${op.chatType}`,
					});
				}else if(type == 'switchTab' || type == 'navigateTo'){
					uni[type]({
						url: url,
					});
				}else if(type == 'deleteChatData'){
					uni.showModal({
						content:'是否清空聊天记录?',
						success: (res) => {
							if(res.confirm){
								this.chatClass.clearChatInfo(this.chatpageset.id, 
								this.chatpageset.chatType).then(()=>{
									uni.showToast({title:'清空成功',icon:'success'});
									// 清空完成之后聊天页的数据也要清空
									uni.$emit('clearChatInfo');
								});
							}
						}
					})
				}
			},
			submitSet(){
				let userset = {};
				// 聊天设置
				if(this.setdata.action == 'chatset'){
					const chatset = {
						visitor: {
							sendCount:this.setdata.visitor.list[this.setdata.visitor.current].value,
							needFollow:false,
						},
						user:{
							sendCount:this.setdata.user.list[this.setdata.user.current].value,
							needFollow:false,
						} 
					}
					userset.chatset = chatset;
				}
				console.log('提交给服务器的设置数据',userset);
				this.btn.loading = true;
				this.btn.disabled = true;
				uni.$u.http.post(requestUrl.http + `/api/chat/userset`, {
					userset: JSON.stringify(userset),
				}, {
					header: {
						token: this.me.token,
					},
				}).then(res => {
					console.log('服务器返回聊天设置的结果', res);
					this.btn.loading = false;
					if(res.data.data == 'ok'){
						uni.showToast({
							title: '设置成功',
							icon: 'none',
							duration: 1000
						});
						setTimeout(() => {
							return this.navigateBack();
						}, 1000);
					}
				}).catch(err => {
                    this.btn.loading = false;
					if (err.data && err.data.data) {
						uni.showToast({
							title: err.data.data,
							icon: 'none',
							duration: 5000
						});
					}
				});
			}
		}
	}
</script>

<style>

</style>

# ② 混入文件 /pages/setpageInfo/updateUserInfo.js

import UniPermission from '@/common/mixins/uni_permission.js';
import { requestUrl, uploadfileSaveType } from '@/common/mixins/configData.js';
export default {
	data(){
		return {
			// 更换头像是拍照还是从相册选择一张
			actionSheetList:[
				{
					name:'相册',
					subname:"可从相册选择一张图片来当头像",
					color:'#1BA035',
					fontSize:'20',
					eventType:'photo',
				},
				{
					name:'拍一张',
					subname:"可拍摄一张照片当头像",
					color:'#1BA035',
					fontSize:'20',
					eventType:'cameraPhoto',
				},
			],
			actionSheetShow:false,
		}
	},
	methods: {
		// 修改我的账号信息
		updateUserInfo(fieldname) {
			let arg = {};
			switch (fieldname) {
				case 'nickname':
					arg = {
						apiurl: '/api/chat/updateUserinfo',
						data: {
							fieldname: fieldname,
							fieldValue: this.chatpageset.nickname,
						},
					};
					// 我的信息更新一下
					this.me[fieldname] = this.chatpageset.nickname;
					break;
				case 'avatar':
				    // 因为换图片需要从相册选择图片或者拍照,先看是否有权限
					return this.getUniPermission();
					break;
			}
			// 服务器处理 
			this.groupUpdate(arg).then(res => {
				// 成功处理
				uni.showToast({
					title: res.data.data,
					icon: 'none'
				});
				// 更新我在本地的信息chatuser及vuex
				this.$store.dispatch('regloginAction', this.me);
			});
		},
		// 因为换图片需要从相册选择图片或者拍照,先看是否有权限
		getUniPermission(){
			console.log('弹窗选择是拍照还是相册选择');
			this.actionSheetShow = true;
		},
		// 点击了ActionSheet 操作菜单
		async selectActionSheet(item){
			this.actionSheetShow = false;
			console.log('点击了ActionSheet 操作菜单', item);
			switch (item.eventType){
				case 'photo':
				    await this.handlePermission({
						permission:{
							name:'photo',
							title:'本功能需要您打开相册',
							desc:'需要访问您的相册来选择图片',
							grantedText:'用户已授权开启相册,可以选择照片了',
							nograntedText:'您没有授权开启相册,无法发图片',
						},
						methodsName:'chooseImage',
					});
					break;
				case 'cameraPhoto':
				    await this.handlePermission({
				    	permission:{
				    		name:'camera',
				    		title:'本功能需要您打开摄像头',
				    		desc:'需要访问您的摄像头来拍摄照片',
				    		grantedText:'用户已授权打开摄像头,可以拍摄照片了',
				    		nograntedText:'您没有授权打开摄像头,无法拍摄照片',
				    	},
				    	methodsName:'cameraPhoto',
				    });
					break;
			}
		},
		// 发相册图片|发相册视频 权限申请
		async handlePermission(options){
			try{
			   const permission = new UniPermission();
			   const granted =  await permission.requestPermission(options.permission.name,
			   options.permission.desc,options.permission.title);
			   if(granted){
				   console.log(options.permission.grantedText);
				   // 调用对应的方法
				   this[options.methodsName]();
				   
			   }else{
				   uni.showToast({
					  title: options.permission.nograntedText,
					  icon:'none',
					  duration:3000
				   });
			   }
			}catch(error){
				console.error('权限申请异常:' + error);
				uni.showToast({
					title:'权限申请失败:' + error.message,
					icon:'none',
					duration:3000
				});
			}
		},
		//选择相册照片发送
		chooseImage(){
			this.sendPhotoAlbumOrCamera({
				count:1,
				sourceType:'album',
			});
		},
		// 拍照片发送
		cameraPhoto(){
			this.sendPhotoAlbumOrCamera({
				count:1,
				sourceType:'camera',
			});
		},
		// 发照片:相册发照片| 拍照片
		sendPhotoAlbumOrCamera(option){
			const that = this;
			uni.chooseImage({
				count:option.count,
				//sizeType:['original','compressed'],
				sizeType: ['compressed'],
				sourceType:[option.sourceType],
				success: (chooseRes) => {
					console.log('选择照片chooseRes',chooseRes);
					const tempFilePath = chooseRes.tempFilePaths[0];
					
					
					// 跳转到头像剪裁页面
					uni.navigateTo({
						url: `/pages/setpageInfo/setpageInfo?action=avatarCut&tempFilePath=${encodeURIComponent(tempFilePath)}&title=${encodeURIComponent('头像剪裁')}`
					});
					
					// 显示加载中
					// uni.showLoading({
					// 	title: '上传中...',
					// 	mask: true
					// });
					// 根据配置选择上传方式
					// if (uploadfileSaveType.image === 'myserver') {
					// 	that.uploadImageToServer(tempFilePath);
					// } else if (uploadfileSaveType.image === 'AliyunOSS') {
					// 	that.uploadImageToAliyunOSS(tempFilePath);
					// }
				},
			});
		},
		
		// 上传图片到服务器
		uploadImageToServer(filePath) {
			const that = this;
			// 自定义文件夹路径 - 使用avatars专门存放头像
			const imagepath = `avatars`;

			uni.uploadFile({
				url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
				filePath: filePath,
				name: 'file',
				formData: {
					token: this.me.token
				},
				header: {
					'token': this.me.token
				},
				success: (uploadRes) => {
					uni.hideLoading();
					console.log('上传到本地服务器图片结果',uploadRes);
					if (uploadRes.statusCode === 200) {
						try {
							const data = JSON.parse(uploadRes.data);
							console.log('上传到本地服务器图片数据',data);
							if (data && data.data && data.data.url) {
								// 更新头像
								that.updateAvatar(data.data.url);
							} else {
								uni.showToast({
									title: '上传失败',
									icon: 'none'
								});
							}
						} catch (e) {
							uni.showToast({
								title: '解析响应数据失败',
								icon: 'none'
							});
						}
					} else {
						uni.showToast({
							title: '上传失败',
							icon: 'none'
						});
					}
				},
				fail: (err) => {
					uni.hideLoading();
					uni.showToast({
						title: '上传失败',
						icon: 'none'
					});
					console.error('上传失败', err);
				}
			});
		},

		// 上传图片到阿里云OSS
		uploadImageToAliyunOSS(filePath) {
			const that = this;
			uni.uploadFile({
				url: requestUrl.http + `/api/chat/uploadAliyun`,
				filePath: filePath,
				name: 'img',
				formData: {
					token: this.me.token,
					imageClassId: 0
				},
				header: {
					'token': this.me.token
				},
				success: (uploadRes) => {
					uni.hideLoading();
					console.log('上传到阿里云OSS图片结果',uploadRes);
					if (uploadRes.statusCode === 200) {
						try {
							const data = JSON.parse(uploadRes.data);
							if (data && data.data && data.data.length && data.data[0].url) {
								// 更新头像
								that.updateAvatar(data.data[0].url);
							} else {
								uni.showToast({
									title: '上传失败',
									icon: 'none'
								});
							}
						} catch (e) {
							uni.showToast({
								title: '解析响应数据失败',
								icon: 'none'
							});
						}
					} else {
						uni.showToast({
							title: '上传失败',
							icon: 'none'
						});
					}
				},
				fail: (err) => {
					uni.hideLoading();
					uni.showToast({
						title: '上传失败',
						icon: 'none'
					});
					console.error('上传失败', err);
				}
			});
		},

		// 更新头像到服务器
		updateAvatar(avatarUrl) {
			const that = this;
			uni.$u.http.post(requestUrl.http + '/api/chat/updateUserinfo', {
				fieldname: 'avatar',
				fieldValue: avatarUrl
			}, {
				header: {
					token: this.me.token
				}
			}).then(res => {
				// 更新本地数据
				that.chatpageset.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
				that.me.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;

				// 更新vuex中的用户信息
				that.$store.dispatch('regloginAction', that.me);

				uni.showToast({
					title: '头像更新成功',
					icon: 'success'
				});
				// 在剪切头像页面返回上一页
				setTimeout(()=>{
					uni.navigateBack({
						delta: 1
					});
				},1000);
			}).catch(err => {
				uni.showToast({
					title: err.data.data || '更新失败',
					icon: 'none'
				});
			});
		},
	}
}

# ③ 混入文件 /pages/setpageInfo/avatarCut.js

import { requestUrl, uploadfileSaveType } from '@/common/mixins/configData.js';
export default {
    data() {
        return {
            tempFilePath: '', // 原始图片路径
            cropperReady: false, // 剪裁器是否就绪
            canvasWidth: 0, // 画布宽度
            canvasHeight: 0, // 画布高度
            scale: 1, // 缩放比例
            minScale: 0.5, // 最小缩放
            maxScale: 3, // 最大缩放
            cropperContext: null, // 画布上下文
            imageWidth: 0, // 图片宽度
            imageHeight: 0, // 图片高度
            offsetX: 0, // 图片偏移X
            offsetY: 0, // 图片偏移Y
            startX: 0, // 触摸开始X
            startY: 0, // 触摸开始Y
            initialOffsetX: 0, // 初始偏移X
            initialOffsetY: 0, // 初始偏移Y
            initialScale: 1, // 初始缩放比例
            initialDistance: 0, // 初始两指距离
            croppedWidth: 500, // 剪裁宽度(rpx,需要转换为px)
            croppedHeight: 500, // 剪裁高度(rpx,需要转换为px)
            windowWidth: 0, // 窗口宽度(px)
            windowHeight: 0, // 窗口高度(px)
            navBarHeight: 0, // 导航栏高度(px)
            isPinching: false, // 是否正在缩放
        }
    },
    methods: {
        // 初始化剪裁器
        initCropper() {
            const that = this;
            
            // 获取系统信息
            uni.getSystemInfo({
                success: function(res) {
                    that.windowWidth = res.windowWidth;
                    that.windowHeight = res.windowHeight;
                    
                    // 计算导航栏高度(假设自定义导航栏高度为90rpx + 状态栏高度)
                    const statusBarHeight = res.statusBarHeight;
                    that.navBarHeight = uni.upx2px(90) + statusBarHeight;
                    
                    // 将rpx转换为px
                    const croppedWidthPx = uni.upx2px(that.croppedWidth);
                    const croppedHeightPx = uni.upx2px(that.croppedHeight);
                    
                    // 计算可用高度(屏幕高度 - 导航栏高度 - 操作按钮高度)
                    const availableHeight = that.windowHeight - that.navBarHeight - uni.upx2px(100);
                    
                    // 画布大小设置为剪裁窗口大小
                    that.canvasWidth = croppedWidthPx;
                    that.canvasHeight = croppedHeightPx;
                    
                    // 获取图片信息
                    uni.getImageInfo({
                        src: that.tempFilePath,
                        success: function(imageInfo) {
                            that.imageWidth = imageInfo.width;
                            that.imageHeight = imageInfo.height;
                            
                            // 初始化画布
                            that.cropperContext = uni.createCanvasContext('avatarCanvas', that);
                            
                            // 计算初始缩放比例,使图片适应剪裁框
                            const widthScale = croppedWidthPx / imageInfo.width;
                            const heightScale = croppedHeightPx / imageInfo.height;
                            that.scale = Math.max(widthScale, heightScale) * 1.2; // 稍微放大一点,确保填满剪裁框
                            that.minScale = that.scale; // 最小缩放比例设为初始比例,确保图片至少填满剪裁框
                            that.maxScale = that.scale * 3;
                            
                            // 重置偏移量
                            that.offsetX = 0;
                            that.offsetY = 0;
                            
                            // 绘制图片
                            that.drawImage();
                            that.cropperReady = true;
                        },
                        fail: function(err) {
                            console.error('获取图片信息失败', err);
                            uni.showToast({
                                title: '图片加载失败',
                                icon: 'none'
                            });
                            setTimeout(() => {
                                uni.navigateBack();
                            }, 1500);
                        }
                    });
                }
            });
        },
        
        // 绘制图片到画布
        drawImage() {
            const ctx = this.cropperContext;
            
            // 清空画布
            ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
            
            // 绘制图片
            const scaledWidth = this.imageWidth * this.scale;
            const scaledHeight = this.imageHeight * this.scale;
            
            // 计算图片绘制位置,确保居中显示
            const drawX = (this.canvasWidth - scaledWidth) / 2 + this.offsetX;
            const drawY = (this.canvasHeight - scaledHeight) / 2 + this.offsetY;
            
            ctx.drawImage(
                this.tempFilePath,
                drawX, drawY,
                scaledWidth, scaledHeight
            );
            
            ctx.draw(false); // 使用非同步绘制
        },
        
        // 取消剪裁
        cancelCrop() {
            uni.navigateBack();
        },
        
        // 确认剪裁
        confirmCrop() {
            const that = this;
            if (!that.cropperReady) {
                uni.showToast({
                    title: '剪裁器未就绪',
                    icon: 'none'
                });
                return;
            }
            
            uni.showLoading({
                title: '正在剪裁...',
                mask: true
            });
            
            // 剪裁图片
            setTimeout(() => {
                uni.canvasToTempFilePath({
                    x: 0,
                    y: 0,
                    width: that.canvasWidth,
                    height: that.canvasHeight,
                    destWidth: that.croppedWidth,
                    destHeight: that.croppedHeight,
                    canvasId: 'avatarCanvas',
                    success: function(res) {
                        uni.hideLoading();
                        // 上传剪裁后的图片
                        that.uploadCroppedImage(res.tempFilePath);
                    },
                    fail: function(err) {
                        uni.hideLoading();
                        uni.showToast({
                            title: '剪裁失败',
                            icon: 'none'
                        });
                        console.error('剪裁失败', err);
                    }
                }, that);
            }, 300);
        },
        
        // 上传剪裁后的图片
        uploadCroppedImage(filePath) {
            // 显示加载中
            uni.showLoading({
                title: '上传中...',
                mask: true
            });
            
            const that = this;
            
            // 根据配置选择上传方式
            if (uploadfileSaveType.image === 'myserver') {
                that.uploadImageToServer(filePath);
            } else if (uploadfileSaveType.image === 'AliyunOSS') {
                that.uploadImageToAliyunOSS(filePath);
            }
        },
        
        // 上传图片到服务器
        uploadImageToServer(filePath) {
            const that = this;
            // 自定义文件夹路径 - 使用avatars专门存放头像
            const imagepath = `avatars`;

            uni.uploadFile({
                url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
                filePath: filePath,
                name: 'file',
                formData: {
                    token: this.me.token
                },
                header: {
                    'token': this.me.token
                },
                success: (uploadRes) => {
                    uni.hideLoading();
                    console.log('上传到本地服务器图片结果',uploadRes);
                    if (uploadRes.statusCode === 200) {
                        try {
                            const data = JSON.parse(uploadRes.data);
                            console.log('上传到本地服务器图片数据',data);
                            if (data && data.data && data.data.url) {
                                // 更新头像
                                that.updateAvatar(data.data.url);
                            } else {
                                uni.showToast({
                                    title: '上传失败',
                                    icon: 'none'
                                });
                            }
                        } catch (e) {
                            uni.showToast({
                                title: '解析响应数据失败',
                                icon: 'none'
                            });
                        }
                    } else {
                        uni.showToast({
                            title: '上传失败',
                            icon: 'none'
                        });
                    }
                },
                fail: (err) => {
                    uni.hideLoading();
                    uni.showToast({
                        title: '上传失败',
                        icon: 'none'
                    });
                    console.error('上传失败', err);
                }
            });
        },

        // 上传图片到阿里云OSS
        uploadImageToAliyunOSS(filePath) {
            const that = this;
            uni.uploadFile({
                url: requestUrl.http + `/api/chat/uploadAliyun`,
                filePath: filePath,
                name: 'img',
                formData: {
                    token: this.me.token,
                    imageClassId: 0
                },
                header: {
                    'token': this.me.token
                },
                success: (uploadRes) => {
                    uni.hideLoading();
                    console.log('上传到阿里云OSS图片结果',uploadRes);
                    if (uploadRes.statusCode === 200) {
                        try {
                            const data = JSON.parse(uploadRes.data);
                            if (data && data.data && data.data.length && data.data[0].url) {
                                // 更新头像
                                that.updateAvatar(data.data[0].url);
                            } else {
                                uni.showToast({
                                    title: '上传失败',
                                    icon: 'none'
                                });
                            }
                        } catch (e) {
                            uni.showToast({
                                title: '解析响应数据失败',
                                icon: 'none'
                            });
                        }
                    } else {
                        uni.showToast({
                            title: '上传失败',
                            icon: 'none'
                        });
                    }
                },
                fail: (err) => {
                    uni.hideLoading();
                    uni.showToast({
                        title: '上传失败',
                        icon: 'none'
                    });
                    console.error('上传失败', err);
                }
            });
        },

        // 更新头像到服务器
        updateAvatar(avatarUrl) {
            const that = this;
            uni.$u.http.post(requestUrl.http + '/api/chat/updateUserinfo', {
                fieldname: 'avatar',
                fieldValue: avatarUrl
            }, {
                header: {
                    token: this.me.token
                }
            }).then(res => {
                // 更新本地数据
                that.chatpageset.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
                that.me.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;

                // 更新vuex中的用户信息
                that.$store.dispatch('regloginAction', that.me);

                uni.showToast({
                    title: '头像更新成功',
                    icon: 'success'
                });
                // 在剪切头像页面返回上一页
                setTimeout(()=>{
                    this.navigateBack();
                },1000);
            }).catch(err => {
                uni.showToast({
                    title: err.data.data || '更新失败',
                    icon: 'none'
                });
            });
        },
        
        // 计算两点之间的距离
        getDistance(p1, p2) {
            const dx = p2.x - p1.x;
            const dy = p2.y - p1.y;
            return Math.sqrt(dx * dx + dy * dy);
        },
        
        // 计算两点之间的中心点
        getMidpoint(p1, p2) {
            return {
                x: (p1.x + p2.x) / 2,
                y: (p1.y + p2.y) / 2
            };
        },
        
        // 触摸事件处理(实现拖拽和缩放功能)
        touchStart(e) {
            if (!this.cropperReady) return;
            
            // 单指触摸 - 拖拽
            if (e.touches.length === 1) {
                this.startX = e.touches[0].clientX;
                this.startY = e.touches[0].clientY;
                this.initialOffsetX = this.offsetX;
                this.initialOffsetY = this.offsetY;
                this.isPinching = false;
            } 
            // 双指触摸 - 缩放
            else if (e.touches.length === 2) {
                const touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
                const touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
                
                this.initialDistance = this.getDistance(touch1, touch2);
                this.initialScale = this.scale;
                this.isPinching = true;
                
                // 计算缩放中心点
                const midpoint = this.getMidpoint(touch1, touch2);
                this.scaleCenterX = midpoint.x;
                this.scaleCenterY = midpoint.y;
            }
        },
        
        touchMove(e) {
            if (!this.cropperReady) return;
            
            // 单指触摸 - 拖拽
            if (e.touches.length === 1 && !this.isPinching) {
                if (!this.startX) return;
                
                const moveX = e.touches[0].clientX - this.startX;
                const moveY = e.touches[0].clientY - this.startY;
                
                this.offsetX = this.initialOffsetX + moveX;
                this.offsetY = this.initialOffsetY + moveY;
                
                // 限制移动范围
                const scaledWidth = this.imageWidth * this.scale;
                const scaledHeight = this.imageHeight * this.scale;
                const maxX = Math.max(0, (scaledWidth - this.canvasWidth) / 2);
                const maxY = Math.max(0, (scaledHeight - this.canvasHeight) / 2);
                
                this.offsetX = Math.min(Math.max(this.offsetX, -maxX), maxX);
                this.offsetY = Math.min(Math.max(this.offsetY, -maxY), maxY);
                
                this.drawImage();
            } 
            // 双指触摸 - 缩放
            else if (e.touches.length === 2) {
                const touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
                const touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
                
                const currentDistance = this.getDistance(touch1, touch2);
                
                if (this.initialDistance > 0) {
                    // 计算缩放比例
                    let newScale = this.initialScale * (currentDistance / this.initialDistance);
                    
                    // 限制缩放范围
                    newScale = Math.min(Math.max(newScale, this.minScale), this.maxScale);
                    
                    // 计算缩放中心点在图片上的位置
                    const scaledWidth = this.imageWidth * this.scale;
                    const scaledHeight = this.imageHeight * this.scale;
                    
                    // 计算缩放中心点在图片上的相对位置
                    const imgX = (this.scaleCenterX - this.canvasWidth / 2 - this.offsetX) / this.scale;
                    const imgY = (this.scaleCenterY - this.canvasHeight / 2 - this.offsetY) / this.scale;
                    
                    // 更新缩放比例
                    this.scale = newScale;
                    
                    // 计算新的偏移量,使缩放中心点保持不变
                    const newScaledWidth = this.imageWidth * this.scale;
                    const newScaledHeight = this.imageHeight * this.scale;
                    
                    this.offsetX = this.scaleCenterX - this.canvasWidth / 2 - imgX * this.scale;
                    this.offsetY = this.scaleCenterY - this.canvasHeight / 2 - imgY * this.scale;
                    
                    // 限制移动范围
                    const maxX = Math.max(0, (newScaledWidth - this.canvasWidth) / 2);
                    const maxY = Math.max(0, (newScaledHeight - this.canvasHeight) / 2);
                    
                    this.offsetX = Math.min(Math.max(this.offsetX, -maxX), maxX);
                    this.offsetY = Math.min(Math.max(this.offsetY, -maxY), maxY);
                    
                    this.drawImage();
                }
            }
        },
        
        touchEnd(e) {
            this.startX = 0;
            this.startY = 0;
            
            // 如果只剩下一根手指,结束缩放
            if (e.touches.length === 0 && this.isPinching) {
                this.isPinching = false;
                this.initialDistance = 0;
            }
        }
    },
}
更新时间: 2025年9月6日星期六下午2点19分