# 一、 服务器通讯发表情包图片及发图片功能

# 1. 先更新聊天类完整代码

将聊天类文件 /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);
		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) => {
			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();
			}
			return reject();
		});
	}
	
	
	// 清空指定的所有历史记录信息
	//(如某个群所有聊天信息,与某个人的所有聊天信息)
	clearChatInfo(to_id, chatType){
		let key = `chatDetail_${this.user.id}_${chatType}_${to_id}`;
		// 删除本地记录
		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){
				// 找到了
				// 删除这个历史记录
				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();
		});
	}


	// 消息页的聊天列表更新一下
	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 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, //是否提示来消息了
			};
			// 群聊
			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.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 = ``;
		switch(data.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 = `[图片]`;
					}
			    }
			    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 res = msg.data;
			// 把聊天信息存在本地
			let { data } = this.addChatInfo(res, 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();
		}
	}
	
	
}

export default chatClass;

# 2. 设置上传图片等文件是服务器还是阿里云OSS

在文件 /common/mixins/configData.js

...

// 设置上传图片等文件是服务器还是阿里云OSS
export const uploadfileSaveType = {
	// 图片
	image: 'myserver', // 服务器 myserver 阿里云 AliyunOSS
};

# 3. 上传图片等文件到服务器或者阿里云接口说明

  1. 具体使用查看接口说明:三十、上传图片等文件到服务器或者阿里云
  2. 后端文档说明:一、uni-app项目发送文件[图片视频等]到服务器或者阿里云OSS

# 4. 上传功能实现

在文件 /pages/chat/plusIconAction.js

import UniPermission from '@/common/mixins/uni_permission.js';
import {requestUrl, uploadfileSaveType} from '@/common/mixins/configData.js';

// H5端获取视频封面:videoPath 是视频地址
...

export default{
	data(){
		return {
			...,
			iconMenus:[
				// { name:"微笑", icon:"/static/tabbar/index.png",
				// iconType:"image", eventType:"smile" },
				...
			],
			plusMenus:[ // 加号扩展菜单栏目
			    ...
			],
			chatDataList:[
				...
			],
										
		}
	},
	methods:{
		//点击加号扩展菜单的某一项
		...,
		// 拍照片发送
		...,
		//选择相册照片发送
		...,
		//选择相册视频发送
		...,
		// 拍视频发送
		...,
		// 发照片:相册发照片| 拍照片
		sendPhotoAlbumOrCamera(option){
			uni.chooseImage({
				...
				success: (res) => {
					console.log('选择照片res',res);
					// 发送到服务器或者第三方云存储
					// 页面效果渲染效果
					this.uploadFile(res).then(urls => {
					    console.log('所有文件上传成功', urls);
					    // 这里不需要手动发送消息,因为uploadFile中已经处理
					}).catch(error => {
					    console.error('上传失败', error);
					});
				},
				fail: (err) => {
					...
				}
			});
		},
		// 发视频 : 相册 | 拍摄
		...,
		// 发相册图片|发相册视频 权限申请
		...,
		
		//预览多张图片
		...,
		//发送消息
		sendMessage(msgType, option = {}){
			console.log('发送消息',msgType);
			let msg = {
				avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
				nickname: '小二哥',
				user_id: 2,
				chat_time: (new Date()).getTime(),
				data: '',
				type:msgType, //image,video
				isremove:false,
			};
			switch (msgType){
				case 'text':
				    msg.data = this.messageValue;
				    break;
				case 'iconMenus':
				    console.log('iconMenus的数据',option);
				    msg.data = option.icon;
					msg.dataType = option.iconType;
					msg.otherData = {
						type: 'iconMenus',
						typedata: option,
					}
				    break;
				case 'image':
				    console.log('image的数据',option);
					msg.data = option.path;
					msg.dataType = 'image';
					msg.otherData = {
						type: 'image',
						typedata: option,
					}
				    break;
				case 'audio':
				    ...
				    break;
				case 'video':
				    ...
				    break;
			}
			// 组织一下消息格式和服务器一致
			...
			// console.log('发消息格式数据',serverMsg); return;
			// 显示到页面上
			...
			// 发送消息的状态
			...
			// 拿到要发送消息的索引实际就是当前聊天列表的长度
			...
            // 在页面上显示
			console.log('看一下在页面显示的数据',msg);
			...
			// 发给服务器消息
			...
			// 清空发送的内容然后还要滚动到底部
			...

		},
		//格式化视频时长
		...,
		// 发送图片视频等文件到服务器或者云存储
		async uploadFile(res){
			console.log('图片上传原始数据',res);
			
			const that = this;
			const files = res.tempFiles || res.tempFilePaths.map(path => ({ path }));
			// 存储所有上传任务
			const uploadTasks = [];
			const uploadedUrls = [];
			for (let i = 0; i < files.length; i++) {
				const file = files[i];
				// 创建上传任务Promise
				const task = new Promise((resolve, reject) => {
					// 执行上传图片到服务器或者阿里云oss获取图片地址
					if (uploadfileSaveType.image == 'myserver') {
						console.log('图片上传到服务器');
						// 构建formData
						// let formData = new FormData();
						// formData.append('file', file);
						// 自定义文件路径
						const imagepath = `chatImgs`;
						
						// 上传
						/*
						const uploadTask = uni.uploadFile({
							url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
							filePath: file.path,
							name: 'file',
							formData: {
								token: that.me.token
							},
							header: {
								'token': that.me.token
							},
							success: (uploadRes) => {
								console.log('上传服务器的结果',uploadRes);
								if (uploadRes.statusCode === 200) {
									try {
										const data = JSON.parse(uploadRes.data);
										resolve(data.data.url);
									} catch (e) {
										reject(new Error('解析响应数据失败'));
									}
								} else {
									reject(new Error('上传失败,状态码: ' + uploadRes.statusCode));
								}
							},
							fail: (err) => {
								reject(err);
							}
						});
						
						// 监听上传进度
						uploadTask.onProgressUpdate((res) => {
							console.log('监听上传进度', res);
							console.log('上传进度' + res.progress);
							console.log('已经上传的数据长度' + res.totalBytesSent);
							console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
				
							// 测试条件,取消上传任务。
							// if (res.progress > 50) {
							// 	uploadTask.abort();
							// }
							// 可以在这里更新UI显示上传进度
							that.$set(file, 'progress', res.progress);
						});
						*/
					    this.uploadFileAjax(that, file, {
					    	url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
					    	filePath: file.path,
					    	name: 'file',
					    	formData: {
					    		token: that.me.token
					    	},
					    	header: {
					    		'token': that.me.token
					    	},
					    }, (progress)=>{
					    	console.log(`${i+1}个文件上传进度`, progress);
					    }).then(res =>{
					    	console.log('图片上传到服务器的结果',res);
							resolve(res.data.url);
					    }).catch(err =>{
					    	console.log('图片上传到服务器的失败结果',err);
							reject(err);
					    });
					}else if (uploadfileSaveType.image == 'AliyunOSS') {
						console.log('图片上传到阿里云oss');
						// 构建formData
						// let formData = new FormData();
						// formData.append('img', file);
						
						// 上传
						this.uploadFileAjax(that, file, {
							url: requestUrl.http + '/api/chat/uploadAliyun',
							filePath: file.path,
							name: 'img',
							formData: {
								token: that.me.token,
								imageClassId: 0 // 添加必要的参数
							},
							header: {
								'token': that.me.token
							},
						}, (progress)=>{
							console.log(`${i+1}个文件上传进度`, progress);
						}).then(ossRes =>{
							console.log('图片上传到阿里云的结果',ossRes);
							if(ossRes && ossRes.data.length){
								resolve(ossRes.data[0].url);
							}
						}).catch(err =>{
							console.log('图片上传到阿里云的失败结果',err);
							reject(err);
						});
					}
				});
				uploadTasks.push(task);
			}
			try {
				// 等待所有文件上传完成
				const urls = await Promise.all(uploadTasks);
				//console.log('等待所有文件上传完成',urls);return;
				
				// 所有文件上传成功
				for (let i = 0; i < urls.length; i++) {
					uploadedUrls.push(urls[i]);
					
					// 发送消息到聊天
					that.sendMessage('image', {
						path: urls[i],
						localPath: files[i].path // 保留本地路径用于显示
					});
				}
				
				return uploadedUrls;
			} catch (error) {
				console.error('文件上传失败:', error);
				uni.showToast({
					title: '上传失败: ' + error.message,
					icon: 'none',
					duration: 3000
				});
				throw error;
			}
		},
		// 发送图片视频等文件交互
		uploadFileAjax(that, file, option, onProgress = false){
			return new Promise((resolve,reject)=>{
				// 上传
				const uploadTask = uni.uploadFile({
					url: option.url,
					filePath: option.filePath,
					name: option.name,
					formData: option.formData,
					header: option.header,
					success: (uploadRes) => {
						console.log('上传服务器的结果',uploadRes);
						if (uploadRes.statusCode === 200) {
							try {
								const data = JSON.parse(uploadRes.data);
								resolve(data);
							} catch (e) {
								reject(new Error('解析响应数据失败'));
							}
						} else {
							reject(new Error('上传失败,状态码: ' + uploadRes.statusCode));
						}
					},
					fail: (err) => {
						reject(err);
					}
				});
				
				// 监听上传进度
				uploadTask.onProgressUpdate((res) => {
					console.log('监听上传进度', res);
					console.log('上传进度' + res.progress);
					console.log('已经上传的数据长度' + res.totalBytesSent);
					console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
								
					// 测试条件,取消上传任务。
					// if (res.progress > 50) {
					// 	uploadTask.abort();
					// }
					// 可以在这里更新UI显示上传进度
					that.$set(file, 'progress', res.progress);
					
					if(onProgress && typeof onProgress == 'function'){
						onProgress(res.progress);
					}
				});
			});
		},
	},
}

# 5. 页面渲染问题:聊天页处理

在页面 /pages/chat/chat.nvue

<template>
	<view>
		...
	</view>
</template>

<script>
    ...
	export default {
		...,
		methods: {
			...mapMutations(['regSendMessage']),
			// 接收的或者历史记录格式化成需要的聊天数据属性渲染页面
			formatServerMsg(v){
				console.log('接收的或者历史记录渲染到聊天页原始数据',v);
				let evedata = typeof v.data == 'string' ? 
				JSON.parse(v.data) : v.data;
				// 渲染到聊天页的数据
				let chatdata = {
					...v,
					chat_time: v.create_time,
					data: evedata.data,
					dataType: evedata.dataType,  // 新增属性
					otherData: evedata.otherData, // 新增属性
					type: v.type, //image,video
					isremove:false,
					avatar : v.from_avatar.startsWith('http') ? v.from_avatar :
					            requestUrl.http + v.from_avatar,
					nickname : v.from_name,
					user_id : v.from_id,
				};
				console.log('接收的或者历史记录渲染到聊天页最终数据', chatdata);
				return chatdata;
			},
			...
		},
		...
	}
</script>

# 6. 页面渲染问题:组件(/components/chat-item/chat-item.vue

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

<template>
	<view class="px-3">
		...
	</view>
</template>

<script>
    ...
	export default{
		...,
		mounted() {
			console.log('chat-item组件获取的item',this.item);
		},
		...
	}
</script>

# 7. 页面渲染问题:组件(/components/chat-item-image/chat-item-image.vue

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

<template>
	<view>
		...
		
		<!-- 优化后的 最终方案兼容多端 -->
		<image lazy-load :mode="imageMode"
		:src="avatarShow"
		:style="computedStyles[index]"
		:class="imageClass"
		@click="$emit('click')"
		@load="good_handleImageLoad($event,item,index)"></image>
	</view>
</template>

<script>
	...
	export default{
		...
		props:{
			...
		},
		data(){
			...
		},
		mounted() {
			console.log('chat-item-image组件获取的item',this.item);
		},
		computed:{
			// 处理通过服务器发送的图片
			avatarShow(){
				if(this.item && this.item.data){
					return this.item.data.startsWith('http') ? 
					this.item.data : requestUrl.http + this.item.data;
				}
				return ``;
			},
			// 图片等比例展示
			imageShowStyle(){
				return `width:${this.width}px;height: ${this.height}px;`;
			},
			...
		},
		...
	}
</script>


# 8. 消息页显示问题

在消息页组件 /components/chat-chatlist/chat-chatlist.vue

<template>
	<view ...>
		<!-- 头像 -->
		...
		<!-- 右边 -->
		<view ...>
			<!-- 上面:昵称 + 时间 -->
			...
			<!-- 下面:聊天内容 -->
			<view class="pr-5">
				<text class="text-light-muted u-line-1"
				style="font-size: 24rpx;">{{showText}}</text>
			</view>
		</view>
	</view>
</template>

<script>
	...
	export default{
		...,
		mounted() {
			console.log('消息页chat-chatlist组件数据',this.item);
		},
		...,
		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{
					return this.item.datadesc;
				}
			},
		}
	}
</script>

# 二、 服务器通讯发视频功能(及发图片功能的改进)

说明:

  1. 如果有同学在微信小程序端用真机调试,发现图片(包括头像等)不显示,是因为微信真机调试需要配置域名白名单,具体配置方法可以参考微信小程序官方文档(后期我们上线小程序项目也会讲到),那么大家先用微信开发者工具调试。

  2. 阿里云OSS提供了视频封面截取功能,方便快速获取视频封面。

# 1. 阿里云OSS视频封面截取方法

https://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250826/xxxx_xxxx.mp4?x-oss-process=video/snapshot,t_20,m_fast,w_260,f_png

# 2. 方法说明

参数 参数解释 单位说明 取值范围
t 截图时间(视频播放到那个时间的帧的画面) 单位:ms(毫秒) (视频时长范围内)[0,视频时长]
m 截图模式,不指定则为默认模式,根据时间精确截图,如果指定为fast则截取该时间点之前的最近的一个关键帧 枚举值: fast
w 截图宽度,如果指定为0则自动计算 单位:px(像素值) (视频宽度)[0,视频宽度]
h 截图高度,如果指定为0则自动计算,如果w和h都为0则输出为原视频宽高 单位:px(像素值) (视频高度)[0,视频高度]
f 输出截图图片格式 枚举值:jpg、png
  1. 服务器通讯发视频功能及多图发送功能比较复杂,需要分几节课进行讲解,大家每节课可以多听几遍,跟着敲代码。

# 1. 定义各种类型文件上传到哪里:是自己的服务器还是阿里云OSS

在文件 /common/mixins/configData.js


// 导出常量请求地址
...

// 游客注册的盐值
...

// 设置上传图片等文件是存到服务器还是阿里云oss
export const uploadfileSaveType = {
	// 服务器 myserver 阿里云 AliyunOSS
	// 图片
	image:'AliyunOSS', 
	// 视频
	video:'AliyunOSS',
};

# 2. 聊天页

在页面 /pages/chat/chat.nvue

<template>
	<view>
		...
	</view>
</template>

<script>
    ...
	export default {
		...
		methods: {
			...
			//点击聊天区域
			scrollViewClick(){
				// #ifdef APP
				console.log('点击聊天区域');
				this.KeyboardHeight = 0;
				uni.hideKeyboard();
				this.$refs.tooltipPlus.hide();
				this.sendMessageMode = "text";
				// #endif
			},
			...
		},
		
	}
</script>

# 3. 聊天页组件

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

<template>
	<view class="px-3">
		<!-- 时间 -->
		...
		<!-- 撤回消息 -->
		...
		<!-- 进群首条消息提示 -->
		...
		<!-- 聊天内容 -->
		...
    
	    <!-- 给服务器发消息状态 -->
		<view v-if="item.sendStatus && item.sendStatus != 'success' && 
		item.sendStatus == 'pending'"
		class="flex align-center justify-end pr-5 pb-5">
		    <text class="font-sm"
		    :class="[item.sendStatus == 'fail' ? 
		    'text-danger' : 'text-success']">
			    {{item.sendStatus == 'fail' ? '消息发送失败' : '消息正在发送中...'}}
			</text>
		</view>
	
	    <!-- 弹出菜单 -->
	    ...
	
	</view>
</template>

<script>
    ...
	export default{
		...
		mounted() {
			console.log('chat-item组件获取的信息item', this.item);
		},
		...
	}
</script>

# 4. 聊天页组件: 视频组件

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

<template>
	<view>
		<!-- <text class="font-sm">状态:{{item.sendStatus}}--进度:{{item.progress}}--数据:{{item}}--</text> -->
		<!-- 视频封面占位 -->
		<view v-if="item.otherData.showPoster"
		class="position-relative" @click="openVideoShow">
			<!-- 视频封面 -->
			<chat-item-image :item="item" :index="index"
			imageClass="rounded"
			:maxWidth="300" :maxHeight="400"></chat-item-image>
			<!-- 蒙版 -->
			<view class="position-absolute left-0 right-0 top-0 bottom-0 rounded"
			style="z-index: 98;"
			:style="maskStyle"></view>
			<!-- 视频时长 -->
			<view 
			class="position-absolute left-0 right-0 top-0 bottom-0 rounded flex flex-row justify-end align-end mr-1 mb-1"              style="z-index: 99;">
				<text class="font-sm text-white">{{item.otherData.duration}}</text>
			</view>
			<!-- 上传进度和播放按钮处理 -->
			<view class="position-absolute left-0 right-0 top-0 bottom-0 flex flex-row align-center justify-center"
			style="z-index: 100;">
			    <!-- 上传进度 -->
			    <view v-if="item.progress" 
			    class="mx-2 flex flex-column align-center justify-center">
				    <!-- 视频进度 -->
					<view v-if="item.progress < 100"
					class="flex flex-column align-center justify-center">
						<text class="text-white font-sm">视频已完成处理</text>
						<text class="text-white font mt-3">{{item.progress + '%'}}</text>
					</view>
					<!-- 封面处理中 -->
					<text v-if="item.progress == 100" 
					class="text-white font-sm">视频消息处理中</text>
			    </view>
			    <!-- 播放按钮 -->
			    <view v-else
			    class="flex flex-row flex-1 align-center justify-center">
			    	<text class="iconfont text-white"
			    	:style="videoPlayIconStyle">&#xe710;</text>
			    </view>
			</view>
			
		</view>
		<!-- 视频占位 -->
		<view v-else
		style="width: 200rpx;height: 350rpx;"
		class="position-relative">
			<video :src="item.otherData.videoData"
			style="width: 200rpx;height: 350rpx;"
			muted autoplay :controls="false"
			object-fit="cover">
			   <!-- 遮罩  -->
			   <!-- #ifndef MP -->
			   <cover-view
			   class="position-absolute left-0 right-0 top-0 bottom-0 flex flex-column align-center justify-center"
			   style="z-index: 98;background-color: rgba(0, 0, 0, 0.7);"
			   @click="openVideoShow">
			        <!-- 上传进度 -->
			        <view v-if="item.progress" 
			        class="mx-2 flex flex-column align-center justify-center">
			            <!-- 视频进度 -->
			        	<view v-if="item.progress < 100"
			        	class="flex flex-column align-center justify-center">
			        		<text class="text-white font-sm">视频已完成处理</text>
			        		<text class="text-white font mt-3">{{item.progress + '%'}}</text>
			        	</view>
			        	<!-- 封面处理中 -->
			        	<text v-if="item.progress == 100" 
			        	class="text-white font-sm">视频消息处理中</text>
			        </view>
			        <!-- 播放按钮 -->
			        <view v-else
			        class="flex flex-row flex-1 align-center justify-center">
			        	<text class="iconfont text-white"
			        	:style="videoPlayIconStyle">&#xe710;</text>
			        </view>
			   </cover-view>
			   <!-- #endif -->
			</video>
			<!-- #ifdef MP -->
			<view 
			   class="position-absolute left-0 right-0 top-0 bottom-0 flex flex-column align-center justify-center"
			   style="z-index: 98;background-color: rgba(0, 0, 0, 0.7);"
			   @click="openVideoShow">
			        <!-- 上传进度 -->
			        <view v-if="item.progress" 
			        class="mx-2 flex flex-column align-center justify-center">
			            <!-- 视频进度 -->
			        	<view v-if="item.progress < 100"
			        	class="flex flex-column align-center justify-center">
			        		<text class="text-white font-sm">视频已完成处理</text>
			        		<text class="text-white font mt-3">{{item.progress + '%'}}</text>
			        	</view>
			        	<!-- 封面处理中 -->
			        	<text v-if="item.progress == 100" 
			        	class="text-white font-sm">视频消息处理中</text>
			        </view>
			        <!-- 播放按钮 -->
			        <view v-else
			        class="flex flex-row flex-1 align-center justify-center">
			        	<text class="iconfont text-white"
			        	:style="videoPlayIconStyle">&#xe710;</text>
			        </view>
			</view>
			<!-- #endif -->
		</view>
	</view>
</template>

<script>
	export default{
		...,
		mounted() {
			console.log('chat-item-video-poster组件获取的信息item', this.item);
		},
		computed:{
			...,
			// 蒙版样式
			maskStyle(){
				let opacity = this.item.progress ? 0.7 : 0.4;
				return `background-color: rgba(0, 0, 0, ${opacity});`;
			},
		},
		...
	}
</script>

# 5. 聊天页组件:图片组件

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

<template>
	<view class="position-relative">
		<!-- 优化后的 最终方案兼容多端 -->
		<image
		lazy-load :mode="imageMode"
		:src="avatarShow"
		:style="computedStyles[index]"
		:class="imageClass"
		@click="$emit('click')"
		@load="good_handleImageLoad($event,item,index)"></image>
		<!-- 上传进度 -->
		<view v-if="item.progress && item.type == 'image'"
		class="position-absolute left-0 right-0 top-0 bottom-0 flex flex-row align-center justify-center"
		style="z-index: 100;"
		:style="maskStyle">
		    <!-- 上传进度 -->
		    <view class="mx-2 flex flex-column align-center justify-center">
			    <!-- 进度 -->
				<view v-if="item.progress < 100"
				class="flex flex-column align-center justify-center">
					<text class="text-white font-sm">图片已完成处理</text>
					<text class="text-white font mt-3">{{item.progress + '%'}}</text>
				</view>
				<!-- 处理中 -->
				<text v-if="item.progress == 100" 
				class="text-white font-sm">图片消息处理中</text>
		    </view>
		</view>
	</view>
</template>

<script>
	...
	export default{
		...,
		mounted() {
			console.log('chat-item-image组件获取的信息item', this.item);
		},
		computed:{
			// 蒙版样式
			maskStyle(){
				let opacity = this.item.progress ? 0.7 : 0.1;
				return `background-color: rgba(0, 0, 0, ${opacity});`;
			},
			// 处理通过服务器发送的图片
			avatarShow(){
				if(this.item && this.item.data){
					if(this.item.type == 'image'){
						return this.item.data.startsWith('/') ?
						requestUrl.http + this.item.data : this.item.data;
					}else{
						return this.item.data;
					}
				}
				return ``;
			},
			// 图片等比例展示
			...
		},
		...
	}
</script>

# 6. 发视频发多张图片处理

在文件 /pages/chat/plusIconAction.js

import UniPermission from '@/common/mixins/uni_permission.js';
import {requestUrl, uploadfileSaveType} from '@/common/mixins/configData.js';

// H5端获取视频封面:videoPath 是视频地址
...

export default{
	data(){
		return {
			//属性
			videoPlaceHolderPoster:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/video/video_placeholder.jpg',
			recorderIcon:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/audio/audio-icon/recorder.gif',
			iconMenus:[...],
			// 加号扩展菜单栏目
			plusMenus:[...],
			chatDataList:[...],							
		}
	},
	methods:{
		//点击加号扩展菜单的某一项
		...,
		// 拍照片发送
		...,
		//选择相册照片发送
		...,
		//选择相册视频发送
		...,
		// 拍视频发送
		...,
		// 发照片:相册发照片| 拍照片
		sendPhotoAlbumOrCamera(option){
			uni.chooseImage({
				count:option.count,
				//sizeType:['original','compressed'],
				sourceType:[option.sourceType],
				success: (res) => {
					console.log('选择照片res',res);
				    // 发送到服务器或者第三方云存储阿里云oss,页面效果渲染效果
				    // const uploadedUrls = this.uploadFile(res, 'image');
					// console.log('图片上传成功', uploadedUrls);
					// 网速不好的时候,发照片需要时间,可以提出进度提示
					// 那么需要先预览图片,给进度提示,当照片上传完成后显示及发消息
					// 本地预览发一次消息【兼容多张照片】
					// 拿到当前聊天页数据最后一条数据的索引
					let lastIndex = this.chatDataList.length - 1;
					// 处理图片文件
					let files = res.tempFiles || 
					(res.tempFilePaths && res.tempFilePaths.map(path => ({ path }))) || [];
					// 兼容多张图片处理
					for(let i = 0; i < files.length; i++){
						const file = files[i];
						const doRes = {
							poster: file.path,
							durationFormatted: 0,
							showPoster: true,
						};
						this.sendMessage('image',{
							poster: doRes.poster,
							duration: doRes.durationFormatted,
							showPoster: doRes.showPoster,
							// 图片路径
							videoPath: file.path, 
							// 是否跟服务器通讯 将这条消息发给其他人
							isSendToServer: false,
							// 文件进度 初始进度为0
							progress: 0,
							// 发送消息的状态[准备发文件]
							sendStatus: 'pendingFile',
							// 加一个path属性
							path: file.path,
							// 预览数据索引
							previewFileIndex: lastIndex + (i + 1),
							// 标记预览数据
							isPreviewData: true,
						}).then(previewFileIndex => {
							// 拿到预览文件在聊天页的索引进行后续操作
							console.log('拿到预览文件在聊天页的索引进行后续操作', 
							previewFileIndex);
							// 创建一个只包含当前文件的res对象
							const singleFileRes = {
								tempFiles: [file],
								tempFilePaths: [file.path]
							};
							// 接下来发送给服务器或者第三方云存储(如:阿里云oss)及后续处理
							// 这个跟服务器和网络有关,网络越差时间越长:秒级
							// 发图片--使用服务器返回的图片URL发送消息
							this.uploadFile(singleFileRes, 'image', previewFileIndex)
							.then(uploadedUrls =>{
								console.log('图片上传成功', uploadedUrls);
								// 获取图片及发websocket消息
								// 图片路径判断
								const ImagePath = uploadedUrls[0].startsWith('/') ?
								requestUrl.http + uploadedUrls[0] : uploadedUrls[0];
								// 发websockt消息
								this.sendMsgToUsers(ImagePath, doRes, ImagePath,'image', previewFileIndex);
							}).catch(err => {
								console.error('图片上传失败', err);
								// 上传失败,可能需要更新预览消息的状态为失败
								this.chatDataList[previewFileIndex].sendStatus = 'fail';
							});
						}).catch(err => {
							console.error('发送预览消息失败', err);
						});				
					}
				},
				fail: (err) => {
					console.error('选择图片失败:', err);
					let errorMsg = '选择图片失败';
					if(err.errMsg.includes('permission')){
						errorMsg = '相册访问权限不足';
					}else if(err.errMsg.includes('cancel')){
						return; // 用户不授权不提示
					}
					uni.showToast({
						title:errorMsg,icon:'none',duration:3000
					});
				}
			});
		},
		// 发视频 : 相册 | 拍摄
		async sendVideoAlbumOrCamera(option){
			uni.chooseVideo({
				sourceType:[option.sourceType],
				// extension:['mp4'],
				compressed:true,
				maxDuration:60,
				camera:'back',
				success: async (res) => {
					console.log('选择相册视频res',res);
					if(res.tempFilePath){
						// 初始时候选择视频处理各个平台视频封面时长等
						const doRes = await this.VideoRender(res);
						console.log('初始时候选择视频处理各个平台视频封面时长等', doRes);
						// 发视频需要时间,视频发送给服务器过程中
						// 本人的聊天页面显示发送进度封面等信息
						// 本地预览发消息--这个很快毫秒级
						// 视频封面(H5端封面在本地预览时候就可以获取封面数据)
						// (小程序本地预览封面可以,但是发消息对方无法获取封面,因为是临时封面地址)
						// (app要用插件)
						// 拿到当前聊天页数据最后一条数据的索引
						let lastIndex = this.chatDataList.length - 1;
						// 本地预览发一次消息
						this.sendMessage('video',{
							poster: doRes.poster,
							duration: doRes.durationFormatted,
							showPoster: doRes.showPoster,
							// 视频路径是本地视频
							videoPath: res.tempFilePath, 
							// 是否跟服务器通讯 将这条消息发给其他人
							isSendToServer: false,
							// 文件进度 初始进度为0
							progress: 0,
							// 发送消息的状态[准备发文件]
							sendStatus: 'pendingFile',
							// 加一个path属性
							path: doRes.poster,
							// 预览数据索引
							previewFileIndex: lastIndex + 1,
							// 标记预览数据
							isPreviewData: true,
						}).then(previewFileIndex => {
							// 拿到预览文件在聊天页的索引进行后续操作
							console.log('拿到预览文件在聊天页的索引进行后续操作', 
							previewFileIndex);
							// 接下来发送给服务器或者第三方云存储(如:阿里云oss)及后续处理
							// 这个跟服务器和网络有关,网络越差时间越长:秒级
							// 发视频--使用服务器返回的视频URL发送消息
							this.uploadFile(res, 'video', previewFileIndex)
							.then(uploadedUrls =>{
								console.log('视频上传成功', uploadedUrls);
								// 获取视频封面及发websocket消息
								// 视频路径判断
								const videoPath = uploadedUrls[0].startsWith('/') ?
								requestUrl.http + uploadedUrls[0] : uploadedUrls[0];
								// 视频封面主要是微信小程序和app获取较麻烦(前面开发页面时候讲过)
								// 为了统一多端,这里在服务端获取视频封面
								let videoPoster = ``;
								// 如果视频上传到阿里云可以通过阿里云的接口获取视频封面
								if(uploadfileSaveType.video == 'AliyunOSS'){
									//通过阿里云的接口获取视频封面
									videoPoster = videoPath + 
									`?x-oss-process=video/snapshot,t_20,m_fast,w_260,f_png`;
									// 发websockt消息 
									this.sendMsgToUsers(videoPoster, doRes, 
									videoPath, 'video', previewFileIndex);
								}
								// 如果视频上传到服务器则需要服务器返回视频封面
								if(uploadfileSaveType.video == 'myserver'){
									console.log('视频上传到服务器服务器返回视频封面_视频地址',
									           videoPath);
									// 获取视频封面
									this.getVideoScreenshot(videoPath, 20, 260,'png')
									.then(VideoScreen=>{
										console.log('视频上传到服务器服务器返回视频封面_视频封面地址', VideoScreen);
										videoPoster = VideoScreen && VideoScreen.startsWith('/') ? 
										              requestUrl.http + VideoScreen : VideoScreen;
										// 发websockt消息 
										this.sendMsgToUsers(videoPoster, doRes, videoPath,'video', previewFileIndex);
									});
								}
								
							});
						});
					}
				},
				fail: (err) => {
					console.error('选择视频失败:', err);
					let errorMsg = '选择视频失败';
					if(err.errMsg.includes('permission')){
						errorMsg = '相册访问权限不足';
					}else if(err.errMsg.includes('cancel')){
						return; // 用户不授权不提示
					}
					uni.showToast({
						title:errorMsg,icon:'none',duration:3000
					});
				}
			});
		},
		// 发相册图片|发相册视频 权限申请
		async handlePermission(options){
			try{
			   ...
			   if(granted){
				   console.log(options.permission.grantedText);
				   // 调用对应方法
				   this[options.methodsName]();
				   // 弹出框隐藏 -- 点击聊天区域一样的效果
				   this.clickPlusHidetooltip();
			   }else{
				   ...
			   }
			}catch(error){
				...
			}
		},
		
		//预览多张图片
		...,
		//发送消息
		async sendMessage(msgType, option = {}){
			return new Promise((resolve,reject)=>{
				console.log('发送消息',msgType);
				let msg = {
					avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
					nickname: '小二哥',
					user_id: 2,
					chat_time: (new Date()).getTime(),
					data: '',
					type:msgType, //image,video
					isremove:false,
					// 是否跟服务器通讯
					isSendToServer: option.isSendToServer == false ? false : true, 
					// 上传进度字段
					progress: option.progress !== undefined ? 
					          option.progress : 0,
					// 发送消息的状态
					sendStatus: option.sendStatus !== undefined ? 
					            option.sendStatus : 'pending',
					// 预览数据索引
					previewFileIndex: option.previewFileIndex !== undefined ? 
					            option.previewFileIndex : undefined,
					// 标记预览数据
					isPreviewData: option.isPreviewData !== undefined ? 
					               option.isPreviewData : undefined,
				};
				switch (msgType){
					case 'text':
					    msg.data = this.messageValue;
					    break;
					case 'iconMenus':
					    console.log('iconMenus的数据',option);
					    msg.data = option.icon;
						msg.dataType = option.iconType;
						msg.otherData = {
							type: 'iconMenus',
							typedata: option,
						}
					    break;
					case 'image':
					    console.log('image的数据',option);
						msg.data = option.path;
						msg.dataType = 'image';
						msg.otherData = {
							type: 'image',
							typedata: option,
						}
					    break;
					case 'audio':
					    console.log('audio的数据',option);
						msg.data = option.tempFilePath;
						msg.dataType = 'audio';
						msg.otherData = {
							duration: Math.round(option.duration / 1000),
						}
					    break;
					case 'video':
					    console.log('video的数据',option);
						msg.data = option.poster;
						msg.dataType = 'video';
						msg.otherData = {
							duration:option.duration, // 视频时长
							poster:option.poster,
							videoData:option.videoPath,
							showPoster:option.showPoster, // 是否显示封面
						};
					    break;
				}
				// 组织一下消息格式和服务器一致
				let serverMsg = this.chatClass.formatSendMessage({
					type: msgType,
					data:{
						data: msg.data,
						dataType: msg.dataType ? msg.dataType : false,
						otherData: msg.otherData ? msg.otherData : null,
					},
					options: option,
				});
				// console.log('发消息格式数据',serverMsg); return;
				// 显示到页面上
				msg.avatar = serverMsg.from_avatar.startsWith('http') ? 
				             serverMsg.from_avatar :
				             requestUrl.http + serverMsg.from_avatar;
				msg.nickname = serverMsg.from_name;
				msg.user_id = serverMsg.from_id;
				// 发送消息的状态
				// msg.sendStatus = 'pending';
				// 拿到要发送消息的索引
				let sendmsgIndex = -1;
				if(msg.previewFileIndex != undefined && 
				msg.previewFileIndex != -1){
					// 如果是预览数据则替换预览数据
					sendmsgIndex = msg.previewFileIndex;
					// 替换的数据
					let replaceData = {...serverMsg, ...msg};
					console.log('-----如果是预览数据则替换之后的数据-------',
					            replaceData);
					this.chatDataList.splice(sendmsgIndex,1,replaceData); 
					
				}else{
					// 没有则是聊天列表长度
					sendmsgIndex = this.chatDataList.length;
					// 在页面上显示
					this.chatDataList.push(msg);
				}
				// 发给服务器消息
				if(msg.isSendToServer){
					console.log('-----发给服务器websocket消息最终数据------',
					           serverMsg);
					this.chatClass.sendmessage(serverMsg)
					.then(result => {
						console.log('页面接收服务器返回结果',result);
						// 拿到刚发送的消息赋值
						this.chatDataList[sendmsgIndex].id = result.data.data.id;
						this.chatDataList[sendmsgIndex].sendStatus = 'success';
						// 执行后续操作 
						resolve();
					}).catch(error => {
						console.log('页面接收服务器错误结果',error);
						this.chatDataList[sendmsgIndex].sendStatus = 'fail';
						// 返回错误
						reject(error);
					});
				}else{
					console.log('不发给服务器只在本地预览拿到当前预览数据', msg);
					console.log('不发给服务器只在本地预览拿到当前预览文件的索引', 
					msg.previewFileIndex);
					// 执行后续操作 对预览的文件显示进度
					// 拿到当前预览文件的索引
					resolve(msg.previewFileIndex);
				}
				// 清空发送的内容然后还要滚动到底部
				if(msgType == 'text') this.messageValue = '';
				this.chatContentToBottom();
			});
		},
		//格式化视频时长
		...,
		// 发送图片视频等文件到服务器或者云存储
		async uploadFile(res, filetype = 'image', previewFileIndex = null){
			console.log('上传原始数据',res);
			
			const that = this;
			let files = [];
			if(filetype == 'image'){
				// 处理图片文件
				files = res.tempFiles || 
				(res.tempFilePaths && 
				res.tempFilePaths.map(path => ({ path }))) || [];
			}else if(filetype == 'video'){
				// 视频文件:`tempFile`在H5端是File对象,在小程序端是类似File的对象
				// 为了统一,使用tempFilePath 兼容多端
				if (res.tempFilePath) {
					files = [{ 
						path: res.tempFilePath, 
						size: res.size, 
						duration: res.duration 
					}];
				} else {
					console.error('视频文件路径不存在');
					throw new Error('视频文件路径不存在');
				}
			}
			
			console.log('处理后的文件数组', files);
			
			// 如果是单文件上传,直接处理
			if(files.length === 1){
				const file = files[0];
				// 创建一个上传任务Promise
				const task = new Promise((resolve,reject)=>{
					// 根据配置选择上传方式
					if(uploadfileSaveType.image == 'myserver' || 
					uploadfileSaveType.video == 'myserver'){
						// 自定义文件夹路径
						let imagepath = ``;
						if(uploadfileSaveType.image == 'myserver'){
							imagepath = `chatImgs`;
						}else if(uploadfileSaveType.video == 'myserver'){
							imagepath = `chatVideos`;
						}
						//let imagepath = filetype === 'image' ? `chatImgs` : `chatVideos`;
						// 文件上传到服务器自定义文件夹
						this.uploadFileAjaxServer(0, imagepath, that, 
						                       file, previewFileIndex)
						.then(uploadServer =>{
							console.log('文件上传到服务器的结果', uploadServer);
							resolve(uploadServer);
						}).catch(reject);
						
					}else if(uploadfileSaveType.image == 'AliyunOSS' || 
					uploadfileSaveType.video == 'AliyunOSS'){
						// 文件上传到阿里云OSS
						this.uploadFileAjaxAliyunOSS(0, that, 
						               file, previewFileIndex)
						.then(uploadAliyunOSS => {
							console.log('文件上传到阿里云OSS的结果', uploadAliyunOSS);
							resolve(uploadAliyunOSS);
						}).catch(reject);
					}
				});
				
				try{
					const url = await task;
					return [url];
				}catch(error){
					console.log('文件上传失败:' , error);
					throw error;
				}
			}else{
				// 多文件处理逻辑(如果有需要)
				// ...
			}
			
		},
		async _xxx(){
			// 存储所有上传的任务
			const uploadTasks = [];
			const uploadedUrls = [];
			
			// 循环上传
			for(let i=0; i < files.length; i++ ){
				const file = files[i];
				// 创建一个上传任务Promise
				const task = new Promise((resolve,reject)=>{
					// 上传文件到服务器
					// 自定义文件夹路径
					let imagepath = ``;
					if(uploadfileSaveType.image == 'myserver'){
						imagepath = `chatImgs`;
					}else if(uploadfileSaveType.video == 'myserver'){
						imagepath = `chatVideos`;
					}
					if(uploadfileSaveType.image == 'myserver' ||
					uploadfileSaveType.video == 'myserver'){
						// 文件上传到服务器自定义文件夹
						this.uploadFileAjaxServer(i, imagepath, that, 
						                        file, previewFileIndex)
						.then(uploadServer =>{
							console.log('文件上传到服务器的结果', uploadServer);
							resolve(uploadServer);
						});
					}else if(uploadfileSaveType.image == 'AliyunOSS' ||
					uploadfileSaveType.video == 'AliyunOSS'){
						// 文件上传到阿里云OSS
						this.uploadFileAjaxAliyunOSS(i, that, file, previewFileIndex)
						.then(uploadAliyunOSS => {
							console.log('文件上传到阿里云OSS的结果', uploadAliyunOSS);
							resolve(uploadAliyunOSS);
						});
					}
				});
				uploadTasks.push(task);
			}
			
			try{
				// 等待所有文件上次完成
				const urls = await Promise.all(uploadTasks);
				console.log('等待所有文件上传完成', urls);
				
				// 所有的文件上传成功
				for(let i = 0; i < urls.length; i++){
					uploadedUrls.push(urls[i]);
					
					// 如果是图片,发送图片消息
					// if (filetype === 'image') {
					// 	this.sendMessage('image', {
					// 		path: urls[i],
					// 		localPath: files[i].path, // 保留本地路径用于显示
					// 	});
					// }
					// 视频消息在sendVideoAlbumOrCamera中发送
				}
				
				return uploadedUrls;
				
			}catch(error){
				console.log('文件上传失败:' , error);
				uni.showToast({
					title: '文件上传:' + error.message,
					icon:'none',
					duration:3000
				});
				throw error;
			}
			
		},
		// 文件上传到服务器自定义文件夹
		uploadFileAjaxServer(i, imagepath, that, file, previewFileIndex){
			return new Promise((resolve,reject)=>{
				// 文件上传到服务器
				this.uploadFileAjax(that, file, {
					url: requestUrl.http + 
					     `/api/chat/uploadStreamSingleToServerDiy/${imagepath}` ,
					filePath: file.path,
					name: 'file',
					formData: {
						token : that.me.token
					},
					header:{
						'token': that.me.token
					},
				}, (progress)=>{
					console.log(`${i+1}个文件上传进度`, progress);
				}, previewFileIndex).then(res=>{
					console.log('图片(视频等)上传到服务器的结果',res);
					resolve(res.data.url);
				}).catch(err =>{
					console.log('图片(视频等)上传到服务器失败结果',err);
					reject(err);
				});
			});
		},
		// 文件上传到阿里云OSS
		uploadFileAjaxAliyunOSS(i, that, file, previewFileIndex){
			return new Promise((resolve, reject)=>{
				// 文件上传到阿里云OSS
				this.uploadFileAjax(that, file, {
					url: requestUrl.http + `/api/chat/uploadAliyun` ,
					filePath: file.path,
					name: 'img',
					formData: {
						token : that.me.token,
						imageClassId: 0
					},
					header:{
						'token': that.me.token
					},
				}, (progress)=>{
					console.log(`${i+1}个文件上传进度`, progress);
				}, previewFileIndex).then(ossRes =>{
					console.log('图片(视频等)上传阿里云的结果',ossRes);
					if(ossRes && ossRes.data.length){
						resolve(ossRes.data[0].url);
					}else {
						reject(new Error('上传阿里云返回数据格式错误'));
					}
				}).catch(err =>{
					console.log('图片(视频等)上传阿里云失败结果',err);
					reject(err);
				});
			});
		},
		// 专门上传文件的方法
		uploadFileAjax(that, file, option, 
		               onProgress = false, previewFileIndex = null){
			return new Promise((resolve,reject)=>{
				// 上传
				const uploadTask = uni.uploadFile({
					url: option.url , 
					filePath: option.filePath,
					name: option.name,
					formData: option.formData,
					header: option.header,
					success: (uploadRes) => {
						console.log('上传服务器的结果',uploadRes);
						if(uploadRes.statusCode == 200){
							try{
							  const data = JSON.parse(uploadRes.data);
							  resolve(data);
							}catch(e){
								reject(new Error('解析响应数据失败'));
							}
						}else{
							reject(new Error('上传失败,状态码:' + uploadRes.statusCode));
						}
					},
					fail: (err) => {
						reject(err);
					}
				});
				// 监听上传进度
				uploadTask.onProgressUpdate((res) => {
					console.log('上传进度' + res.progress);
					console.log('已经上传的数据长度' + res.totalBytesSent);
					console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
								
					// 测试条件,取消上传任务。
					// if (res.progress > 50) {
					// 	uploadTask.abort();
					// }
					
					// 上传进度更新到页面
					that.$set(file, 'progress', res.progress);
					
					if(onProgress && typeof onProgress == 'function'){
						onProgress(res.progress);
					}
					
					// 如果有消息索引,更新对应消息的进度
				    if (previewFileIndex !== null && 
					    that.chatDataList[previewFileIndex]) {
					    that.$set(that.chatDataList[previewFileIndex], 
						          'progress', res.progress);
						console.log(`索引${previewFileIndex}的对话上传简单`, 
						           res.progress);
				    }
				});
				
			});
		},
		// 初始时候选择视频处理各个平台视频封面时长等
		async VideoRender(res){
			// 数据处理
			let poster = '';
			let durationFormatted = '';
			let showPoster = false; // 是否显示封面
			// #ifdef H5
			try{
				// h5封面
				poster = await getH5VideoThumbnail(res.tempFilePath);
				durationFormatted = this.formatVideoDuration(res.duration);
				showPoster = true;
			}catch(e){
				console.log('H5获取封面失败',e);
				poster = this.videoPlaceHolderPoster;
				durationFormatted = '0:00';
			}
			// #endif
			// #ifdef MP
			try{
				// 小程序封面
				poster = res.thumbTempFilePath;
				durationFormatted = this.formatVideoDuration(res.duration);
				showPoster = poster ? true : false;
			}catch(e){
				console.log('小程序获取封面失败',e);
				poster = this.videoPlaceHolderPoster;
				durationFormatted = '0:00';
			}
			// #endif
			// #ifdef APP
			try{
				// APP封面
				//引入插件
				// if(uni.getSystemInfoSync().platform === "android"){
				// 	// 安卓手机获取封面
				// 	const plug=uni.requireNativePlugin("Html5app-VideoCover");
				// 	plug.setVideoPath({
				// 		'url':res.tempFilePath,
				// 		'time':1,
				// 	},ret=>{
				// 		console.log('安卓手机获取封面的结果',ret);
				// 	});
				// }
				poster = this.videoPlaceHolderPoster;
				durationFormatted = this.formatVideoDuration(res.duration);
			}catch(e){
				console.log('APP封面获取失败',e);
				poster = this.videoPlaceHolderPoster;
				durationFormatted = '0:00';
			}
			// #endif
			
			return {
				poster,
				durationFormatted,
				showPoster,
			}
		},
		// 如果视频上传到服务器则需要服务器返回视频封面
		async getVideoScreenshot(videoPath, time = 20, 
		                         width = 260, format = 'png'){
			return new Promise((resolve,reject) =>{
				uni.$u.http.post(requestUrl.http + 
				                `/api/chat/getVideoScreenshot`, {
					videoUrl: videoPath, //视频地址
					time: time || 20, //毫秒
					width: width || 260, //视频截图图片宽 px
					format: format || 'png', //输出格式默认png|jpg
				} , {
					header: {
						token: this.me.token,
					},
				}).then(res => {
					console.log('服务器返回视频截图结果', res);
					// return res.data.data.base64;
					resolve(res.data.data.url);
				}).catch(err=>{
					console.log('服务器返回视频截图错误结果', err);
					reject(err);
				});
			});
		},
		// 发websockt消息 
		sendMsgToUsers(poster, doRes, filePath , 
		               type = 'video', previewFileIndex = -1){
			// 根据情况定义otherData的值
			let otherData = {
				poster: poster || doRes.poster,
				duration: type === 'video' ? doRes.durationFormatted : 0,
				showPoster: poster ? true : doRes.showPoster,
				path: filePath,
			};
			if(type === 'video'){
				otherData = {
					...otherData,
					videoData: filePath,
					
				};
			}else if(type === 'image'){
				otherData = {
					...otherData,
					imageData: filePath, 
				};
			}
            // 发websockt消息 
			console.log('-----发websockt消息------索引',
			           previewFileIndex);
			this.sendMessage(type,{
				...otherData,
				// 视频路径是服务器返回的URL
				videoPath: filePath,  
				// 是否跟服务器通讯 将这条消息发给其他人
				isSendToServer: true,
				// 文件进度 上传完成之后重置到0
				progress: 0,
				// 发送消息的状态[准备发websocket]
				sendStatus: 'pending',
				// 加一个path属性
				path: type === 'video' ? (poster || doRes.poster) : filePath,
				// 预览数据替换成发消息的数据传预览数据索引
				previewFileIndex: previewFileIndex,
				// 对应前端渲染属性
				data: type === 'video' ? (poster || doRes.poster) : filePath,  
				dataType: type,
				otherData: otherData,
			});
		},
		// 弹出框隐藏 -- 点击聊天区域一样的效果
		clickPlusHidetooltip(){
			this.KeyboardHeight = 0;
			uni.hideKeyboard();
			this.$refs.tooltipPlus.hide();
			this.sendMessageMode = "text";
		},
	},
}

# 7. 视频上传到自己的服务器获取视频封面的接口说明

接口说明文档内容:三十一、视频上传到服务器获取视频封面

# 三、服务器通讯发语音消息

# 1. 在聊天页 /pages/chat/chat.nvue

mounted() {
	...
	// 全局监听 全局注册一个发送语音的事件,然后全局
	this.regSendMessage(res=>{
		if(!this.isRecorderCancel){
			res.duration = this.recorderDuration * 1000;
			// this.sendMessage('audio',res);
			this.sendWebsocketAudio('audio',res);
		}
	});
}

# 2. 发语音功能实现

在文件 /pages/chat/plusIconAction.js

    //发送消息
	sendMessage(msgType, option = {}){
		return new Promise((resolve,reject) =>{
			...
			switch (msgType){
				...
				case 'audio':
					console.log('audio的数据',option);
					msg.data = option.path;
					msg.dataType = 'audio';
					msg.otherData = {
						duration: Math.round(option.duration / 1000),
					}
					break;
				...
			}
			...
		});

	},
	// 发送图片视频等文件到服务器或者云存储
	async uploadFile(res, filetype = 'image', previewFileIndex = null){
		...
		if(filetype == 'image'){
			...
		}else if(filetype == 'video'){
			...
		}else if(filetype == 'audio'){
			files = [{
				path: res.tempFilePath,
				size: res.fileSize,
				duration: res.duration,
			}];
		}
		
		console.log('处理后的文件数据', files);
		...
	},
	// 发送websocket消息
	sendMsgToUsers(poster, doRes, filePath, type = 'video', previewFileIndex = -1){
		// 根据情况定义otherData
		let otherData = {
			...
			duration: type == 'video' || type == 'audio' ? 
						doRes.durationFormatted : 0, // 时长
			...
		}
		...
	},	
	// 发语音消息
	sendWebsocketAudio(type,res){
		console.log('发语音消息',res);
		const doRes = {
			poster: false,
			durationFormatted: res.duration,
			showPoster: false,
		};
		// 拿到当前聊天页数据最后一条数据的索引
		let lastIndex = this.chatDataList.length - 1;
		// 本地预览发一次消息
		this.sendMessage('audio',{
			poster: doRes.poster,
			duration: doRes.durationFormatted,
			showPoster: doRes.showPoster,
			// 音频路径是本地音频路径
			videoPath: res.tempFilePath,
			// 是否跟服务器通讯 将这个消息发给其他人
			isSendToServer: false,
			// 文件进度 初始进度为0
			progress: 0,
			// 发送消息的状态【准备发文件】
			sendStatus: 'pendingFile',
			// 加一个path属性
			path: res.tempFilePath,
			// 预览数据索引
			previewFileIndex: lastIndex + 1,
			// 标记预览数据
			isPreviewData: true,
		}).then(previewFileIndex=>{
			// 拿到预览文件在聊天页的索引进行后续操作
			console.log('拿到预览文件在聊天页的索引进行后续操作', 
			previewFileIndex);
			// 接下来发送给服务器或者第三方云存储(如:阿里云oss)及后续处理
			// 这个跟服务器和网络有关,网络越差时间越长:秒级
			// 发音频--使用服务器返回的视频URL发送消息
			this.uploadFile(res, 'audio', previewFileIndex).then(uploadedUrls =>{
				console.log('音频上传成功', uploadedUrls);
				// 获取视频封面然后发websocket消息
				// 判断(处理)视频地址
				const audioPath = uploadedUrls[0].startsWith('/') ? 
				requestUrl.http + uploadedUrls[0] : uploadedUrls[0];
				// 发送websocket消息
				this.sendMsgToUsers(false, doRes, audioPath, 'audio', previewFileIndex);
			});
		});
			
	},

# 四、聊天页撤回转发消息等功能实现

内容较多,在新页面打开 聊天页撤回转发消息等功能实现

更新时间: 2025年9月11日星期四晚上8点27分