比这之前优化了以下功能
上线通知
群聊里适时显示在线人数
约请好友 通过好友通过socket 相应端自动变化
PC端可以拉取摄象头拍照
PC端可以录音发送
拉起摄象头发送录象

用uniapp 及socket.io做一个简单聊天 升级 9-LMLPHP

<template>
	<view class="">
		<scroll-view scroll-y="true" class="scroll-box"
			:style="{ height: `${windowObj.windowHeight - windowObj.statusBarHeight - 94}px` }"
			:scroll-top="scrollHeight" @scrolltoupper="loadMores">
			<view class="group-box">
				在线{{userList.length}}人:
				<text class="group-member" v-for="(item, index) in userList" :key="index">
					{{item}} 
				</text>

			</view>
			<view class="scroll-view">
				<view class="news-box" v-for="(item, index) in list" :key="index">
					<view class="message-type" v-if="['left', 'join', 'kick'].includes(item.type)">
						{{ item.content }} {{(formatDate(Date()))}}
					</view>
					<image class="avatar" :class="[item.isMe ? 'is-me' : 'avatar-right']" :src="item.avatar"
						mode="aspectFill" v-if="!['kick', 'join', 'left'].includes(item.type)" @tap="kickopen(item)">
					</image>
					<view class="message-box" :class="{ 'is-me': item.isMe }"
						v-if="!['kick', 'join', 'left'].includes(item.type)">
						<text class="message" v-if="item.type === 'text'">
							<image src="../../static/withdraw.png"
								style="width: 40rpx; height: 40rpx;position:relative;right:16rpx;bottom:1rpx;"
								mode="aspectFill" v-if="item.isMe && canwithdraw(item) && item.withdraw === 0"
								@tap="withdraw(item)"></image>
							<text :selectable="true" @tap="copyBtnClick(item.content)" > {{formatMessage(item.content || '')}}</text>
							
						</text>
						<text class="message_img" v-if="['image', 'video', 'audio'].includes(item.type)">
							<template v-if="item.type === 'image'">
								<image class="message-image" :src="item.content" mode="aspectFill"
									@click="previewImage(item.content)" />
							</template>
							<template v-if="item.type === 'video'">
								<video v-if="item.content" :src="item.content" controls></video>
							</template>
							<template v-if="item.type === 'audio'">
								<audio v-if="item.content" :src="item.content" controls ></audio>
							</template>
							<image src="../../static/withdraw.png" style="width: 50rpx; height: 50rpx" mode="aspectFill"
								v-if="item.isMe && canwithdraw(item) && item.withdraw === 0" @click="withdraw(item)">
							</image>
						</text>
					</view>
				</view>
			</view>
		</scroll-view>
		<view class="base-btn" :class="{ 'base-btn-popup-open': isPopupOpen || isPopupAudioOpen }">
			<view class="base-con unify-flex">
				<view @tap="more">
					<image src="../../static/chat/more.png" style="width: 50rpx; height: 50rpx"></image>
				</view>
				<input class="input-text" type="text" :value="inputValue" placeholder="说些什么吧" @input="getInput"
					@confirm="tapTo(2)" />
				<view @click="tapTo(2)">
					<image src="../../static/chat/chat.png" style="width: 50rpx; height: 50rpx"></image>
				</view>
			</view>
		</view>
		<uni-popup ref="popup" type="bottom" :style="{ height: '200rpx' }" @change="onPopupChange">
			<view class="popup-content"
				:style="{ width: '100%', backgroundColor: '#fff', height: '200rpx', overflowY: 'scroll' }">
				<view class="popup-items">
					<view class="popup-item" v-if="type === 'group'" @tap="adduserTogroup">
						<image src="../../static/chat/add.png" style="width: 50rpx; height: 50rpx"></image>
						<text>添加</text>
					</view>
					<view class="popup-item" @click="chooseFile">
						<image src="../../static/chat/pic.png" style="width: 50rpx; height: 50rpx"></image>
						<text>图片</text>
					</view>
					<view class="popup-item" @tap="audio">
						<image src="../../static/chat/audio.png" style="width: 50rpx; height: 50rpx"></image>
						<text>音频</text>
					</view>
					<view class="popup-item" @tap="openCamera">
						<image src="../../static/chat/video.png" style="width: 50rpx; height: 50rpx"></image>
						<text>视频</text>
					</view>
					<view class="popup-item" @tap="groupdetail">
						<image src="../../static/chat/detail.png" style="width: 50rpx; height: 50rpx"></image>
						<text>详情</text>
					</view>
					<view class="popup-item" v-if="type === 'group'" @tap="quitgroup">
						<image src="../../static/chat/exit-group.png" style="width: 50rpx; height: 50rpx"></image>
						<text>退群</text>
					</view>
				</view>
			</view>
		</uni-popup>
		<uni-popup ref="popupAudio" type="bottom" :style="{ height: '200rpx' }" @change="onPopupAudioChange">
			<view class="popup-content"
				:style="{ width: '100%', backgroundColor: '#fff', height: '200rpx', overflowY: 'scroll' }">
				<view class="popup-item" @click="startRecording">
					<image src="../../static/chat/beginaudio.png" style="width: 50rpx; height: 50rpx"></image>
					<text>录音</text>
				</view>
				<view class="popup-item" @click="stopRecording">
					<image src="../../static/chat/send.png" style="width: 50rpx; height: 50rpx"></image>
					<text>发送录音</text>
				</view>
		<!-- 		<view class="popup-item" @tap="playRecording">
					<image src="../../static/chat/play.png" style="width: 50rpx; height: 50rpx"></image>
					<text>播放</text>
				</view> -->
			<!-- 	<view class="popup-item" @tap="upsong">
					<image src="../../static/chat/send.png" style="width: 50rpx; height: 50rpx"></image>
					<text>发送</text>
				</view> -->
				<view class="popup-item" @tap="exitchat">
					<image src="../../static/chat/exit.png" style="width: 50rpx; height: 50rpx"></image>
					<text>退出</text>
				</view>
			</view>
		</uni-popup>
		<uni-popup ref="popupkick" type="bottom" :style="{ height: '200rpx' }" @change="onPopupAudioChange">
			<view class="popup-content"
				:style="{ width: '100%', backgroundColor: '#fff', height: '200rpx', overflowY: 'scroll' }">
				<view class="popup-item" @click="kick('kick')">
					<image src="../../static/chat/kickp.png" style="width: 50rpx; height: 50rpx"></image>
					<text>踢人</text>
				</view>
				<view class="popup-item" @click="kick('black')">
					<image src="../../static/chat/black.png" style="width: 50rpx; height: 50rpx"></image>
					<text>拉黑</text>
				</view>
				<view class="popup-item" @tap="detail">
					<image src="../../static/chat/detail.png" style="width: 50rpx; height: 50rpx"></image>
					<text>详情</text>
				</view>

			</view>
		</uni-popup>
	</view>
</template>
<script>
	import io from 'socket.io-client';
	import config from '@/config/config.js';
	import {
		mapState,
		mapActions
	} from 'vuex';
	import {
		v4 as uuidv4
	} from 'uuid';
	import {
		getCurrentDateTime
	} from '@/common/dateFormatter.js'
	import { handleClipboard } from '@/common/clipboardone.js';


	export default {
		data() {
			return {
				name: '',
				inputValue: '',
				list: [],
				image: '',
				scrollHeight: 0,
				userList: '',
				type: '',
				socket: null,
				messages: [],
				groupName: '',
				tid: '',
				toid: 0,
				receiver_type: '',
				isPopupOpen: false,
				isPopupAudioOpen: false,
				selectedFilePath: '',
				group_owner_id: 0, //群主id
				fid: '',
				to_id: 0,
				recordingPath: '', // 用于存储录音文件的路径
		    	  isRecording: false,
			      mediaRecorder: null,
			      audioChunks: []
			};
		},
		computed: {
			...mapState(['user']),
			windowObj() {
				let obj;
				uni.getSystemInfo({
					success: (res) => {
						obj = res;
					}
				});
				return obj;
			}
		},
		watch: {
			isPopupOpen(newValue) {
				if (!newValue) {
					this.$refs.popup.close();
				}
			},

			isPopupAudioOpen(newValue) {
				if (!newValue) {
					this.$refs.popupAudio.close();
				}
			}
		},
		async onLoad(q) {
	
			let _ = this;
			try {
				if (q && q.id != undefined) {
					this.groupName = q.id;
					this.tid = q.tid;
					this.to_id = q.to_id
					this.receiver_type = q.type;
					this.type = this.receiver_type
					uni.setNavigationBarTitle({
						title: q.type == 'group' ? '[群聊] '+q.to_name: '[私聊] '+q.to_name
					});

					if (q.type == 'group') {
						//将q.id的前面g_去掉
						let newid = q.id.replace('g_', '')
						//获到了当前群的群主id
						let re = await _.getGroupOwner(newid)
						this.group_owner_id = re.data.data.owner_id
					}
					let re = await _.checkFriend(q.id);
					if (re == true) {
						_.joinGroup(this.groupName);
					} else {
						uni.navigateTo({
							url: '/pages/index/friends'
						});
					}
				} else {
					uni.navigateTo({
						url: '/pages/index/friends'
					});
				}
			} catch (e) {
				uni.navigateTo({
					url: '/pages/index/friends'
				});
			}

		},
		onUnload() {
			this.socket.close();
		},

		onShow() {
			this.fetchUser();
		},

		mounted() {

			this.initChatLog();
			this.socket = io(config.apiBaseUrl);
			this.socket.on('connect', () => {
				console.log('Socket connected:', this.socket.id);
			});
			this.socket.on('disconnect', () => {
				console.log('Socket disconnected');
			});

			let heartbeatInterval;
			let reconnectAttempts = 0;
			const maxReconnectAttempts = 10;

			const startHeartbeat = () => {
				heartbeatInterval = setInterval(() => {
					if (this.socket.connected) {
						this.socket.emit('heartbeat');
						console.log('heartbeat')
					} else {
						reconnectSocket();
					}
				}, 120000); // 1分钟
			};

			const reconnectSocket = () => {
				if (reconnectAttempts < maxReconnectAttempts) {
					this.socket.connect();
					reconnectAttempts++;
				} else {
					clearInterval(heartbeatInterval);
					uni.showModal({
						title: '连接失败',
						content: '无法连接到服务器,是否手动重新连接?',
						confirmText: '重新连接',
						cancelText: '取消',
						success: (res) => {
							if (res.confirm) {
								reconnectAttempts = 0;
								this.socket.connect();
								startHeartbeat();
							}
						}
					});
				}
			};

			startHeartbeat();

			this.socket.on('reconnect', () => {
				console.log('Socket重新连接成功');
				reconnectAttempts = 0;
			});





			
			this.socket.on('message', (msg) => {

				if (msg.type == 'broadcast') {
					return;
				}
				if (msg.type == 'widthdraw') {

					//查出 msg.sn 将此记录信息改为撤回
					//console.log(msg);
					this.list.forEach((item, index) => {
						if (item.sn == msg.content) {
							this.list[index].content = '[消息已撤回]';
							this.list[index].type = 'text';
							this.list[index].withdraw = 1;
							this.widthdrawRow(item.sn)
						}
					});
					return;
				}
				let msgs = {
					sn: msg.sn,
					name: msg.user_name,
					avatar: msg.avatar,
					isMe: msg.fid == this.user.id ? true : false,
					content: msg.content,
					type: msg.type,
					sn: msg.sn,
					createat: Math.floor(Date.now() / 1000),
					time: Date.now(),
					withdraw: 0,
					toid: msg.fid
				};

				this.list.push(msgs);
				this.setScrollTop();
			});

			// 监听 'userList' 事件
			this.socket.on('userList', (users) => {
				this.userList = users; // 更新 userList 变量
				console.log('- 当前群用户 -')
				console.log(this.userList)

			});

		},
		methods: {
			...mapActions(['fetchUser', 'logout', 'fetchGroups']),
			formatDate() {
				return getCurrentDateTime();
			},
			kickopen(item) {
				this.name = item.name
				this.toid = item.toid
				if (!item.isMe) {
					this.$refs.popupkick.open()
				}
			},
			getGroupOwner(id) {
				//接口 group 提交id 获取到群的信息
				const token = uni.getStorageSync('token');
				return new Promise((resolve, reject) => {
					uni.request({
						url: `${config.apiBaseUrl}/group`,
						method: 'GET',
						header: {
							Authorization: `Bearer ${token}`
						},
						data: {
							id: id
						},
						success: (res) => {
							resolve(res)

						},
						fail: (err) => {
							reject(err)
						}
					});
				})
			},

			async widthdrawRow(sn) {
				const token = uni.getStorageSync('token');
				if (!token) return;
				try {
					const [error, response] = await uni.request({
						url: `${config.apiBaseUrl}/withdraw`,
						method: 'GET',
						header: {
							Authorization: `Bearer ${token}`
						},
						data: {
							sn: sn
						}
					});
					if (error) {
						throw new Error(`Request failed with error: ${error}`);
					}
					if (response.data.code === 0) {
						return true;
					} else {
						return false;
					}
				} catch (error) {
					return false;
				}

			},
			adduserTogroup() {
				this.isPopupOpen=false
				uni.navigateTo({
					url: '/pages/index/addfriend?groupId=' + this.tid
				});
			},

			kick(type) {
				//将用户踢出去

				if (this.group_owner_id != this.fid) {
					//这样才能踢	
					if (type == 'kick') {
						this.kickUser(this.name)
					} else {
						//拉黑
						this.kickUser(this.name, 'black')
						//再拉黑
					}
				} else {
					uni.showToast({
						title: '不能对自己操作'
					})
				}
			},

			detail() {
				uni.navigateTo({
					url: '/pages/index/about?id=' + this.to_id
				});
			},
			groupdetail() {

				let groupid = this.groupName.replace('g_', '')
				if (this.type == 'group') {
					uni.navigateTo({
						url: '/pages/index/groupdetail?id=' + groupid
					});
				} else {

					uni.navigateTo({
						url: '/pages/index/about?id=' + this.to_id
					});

				}


			},
			async quitgroup() {
				console.log(this.group_owner_id)
				console.log(this.user.id)
				if (this.group_owner_id == this.user.id) {
					//主人不能退群
					uni.showToast({
						title: '主人不能退群'
					})
					return
				}

				let groupid = this.groupName.replace('g_', '')

				//调用接口退出 接口名为leavgroup 	
				const token = uni.getStorageSync('token');
				if (!token) return;

				try {
					const [error, response] = await uni.request({
						url: `${config.apiBaseUrl}/leavgroup`,
						method: 'GET',
						header: {
							Authorization: `Bearer ${token}`
						},
						data: {
							groupid
						}
					});
					if (error) {
						throw new Error(`Request failed with error: ${error}`);
					}
					console.log(response)
					if (response.data.code === 0) {
						uni.navigateTo({
							url: '/pages/index/friends'
						})
						return true;

					} else {
						return false;
					}
				} catch (error) {
					return false;
				}
			},
			onPopupChange() {
				if (this.isPopupOpen == true) {
					this.isPopupOpen = false;
				}
			},
			playVoice(url) {
			// 创建音频对象
						const audio = new Audio(url);
						// 播放音频
						audio.play().then(() => {
							console.log('音频开始播放');
						}).catch((error) => {
							console.error('音频播放失败:', error);
						});

						// 监听音频播放结束事件
						audio.onended = () => {
							console.log('音频播放结束');
						};
			},
			onPopupAudioChange() {
				if (this.isPopupOpen == true) {
					this.isPopupOpen = false;
				}
				this.recordingPath = '';
			},
			audio() {
				this.$refs.popup.close();
				this.$refs.popupAudio.open();
				this.isPopupOpen = true;
			},
			exitchat() {
				this.$refs.popupAudio.close();
			},
		async 	startRecording() {
			
				    try {


						if(this.isRecording){
							uni.showToast({
								title: '正在录音中',
								icon: 'none',
								duration: 2000
							});
							return;
						}

						
				        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
				        this.mediaRecorder = new MediaRecorder(stream);
						
						//console.log(this.mediaRecorder);
				        this.mediaRecorder.ondataavailable = (event) => {
				          this.audioChunks.push(event.data);
				        };
				        this.mediaRecorder.onstop  = async () => {


							const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
								const url = URL.createObjectURL(audioBlob);
								this.selectedFilePath = url;
				          // 创建一个提示框
				          const confirmResult = await new Promise((resolve) => {
				            uni.showModal({
				              title: '录音完成',
				              content: '是否上传录音?',
				              confirmText: '上传',
				              cancelText: '取消',
				              success: (res) => {
							
							
								resolve(true);
				              }
				            });
				          });

				          // 如果用户选择取消,则不继续处理
				          if (!confirmResult) {
				            this.audioChunks = [];
				            this.isRecording = false;
				            return;
				          }else{
							this.uploadAvatar('audio');
							
						  }
						  this.isPopupOpen=false;
				   
						  this.isRecording=false;

				          // 清理本地声音
				          stream.getTracks().forEach(track => track.stop());
				          URL.revokeObjectURL(url);


				        };
				        this.mediaRecorder.start();
				        this.isRecording = true;
				
						
				      } catch (error) {
				        console.error('获取麦克风权限失败:', error);
				      }
		
			},
		     async stopRecording() {

				//console.log('停止录音')


				 if (this.mediaRecorder) {
				        this.mediaRecorder.stop();
				        this.isRecording = false;
						this.popupAudio=false;
						
				 }else{
					uni.showToast({
						title: '没有录音',
						icon: 'none'
					});
				 }
			     
				  
			},


			
			
			 uploadAudio(audioBlob) {
				 
				 
			      const formData = new FormData();
			      formData.append('audio', audioBlob, 'recorded_audio.wav');
			      console.log(URL.createObjectURL(audioBlob))
				  const token = uni.getStorageSync('token');
			      uni.uploadFile({
			      	url: `${config.apiBaseUrl}/upload`,
			        filePath: URL.createObjectURL(audioBlob),
			        name: 'avatar',
					header: {
						Authorization: `Bearer ${token}`
					},
			       // formData: formData,
			        success: (uploadFileRes) => {
					 const response = JSON.parse(uploadFileRes.data);
					 if (response.code == 0) {
					 	const avatarUrl = response.data;
					 	this.sendMessage(avatarUrl, 'audio');
					 }
					 
					 
			        },
			        fail: (err) => {
			          //console.error('上传失败:', err);
					  console.error('Failed to upload avatar:', error);
					  uni.showToast({
					  	title: '上传失败',
					  	icon: 'none'
					  });
			        }
			      });
			    },
			
			
			playRecording() {
				if (this.recordingPath) {
					const innerAudioContext = uni.createInnerAudioContext();
					innerAudioContext.src = this.recordingPath;

					innerAudioContext.onPlay(() => {
						console.log('开始播放录音');
					});

					innerAudioContext.onError((res) => {
						console.error('播放录音失败:', res);
					});

					innerAudioContext.play();
				} else {
					uni.showToast({
						title: '没有可播放的录音',
						icon: 'none'
					});
				}
			},
			upsong() {
	
				const token = uni.getStorageSync('token');
				uni.uploadFile({
					url: `${config.apiBaseUrl}/upload`,
					filePath: this.selectedFilePath,
					name: 'avatar',
					header: {
						Authorization: `Bearer ${token}`
					},
					success: async (uploadFileRes) => {
						const response = JSON.parse(uploadFileRes.data);
						if (response.code == 0) {
							const avatarUrl = response.data;
							this.sendMessage(avatarUrl, type);
						}
					},
					fail: (error) => {
						console.error('Failed to upload avatar:', error);
						uni.showToast({
							title: '上传失败',
							icon: 'none'
						});
					}
				});  
				  
				  
			},

			more() {
				this.$refs.popup.open();
				this.isPopupOpen = true;
			},

			openCamera() {

				if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
					navigator.mediaDevices.getUserMedia({ video: true, audio: true })
						.then((stream) => {
							// 创建视频元素
							const video = document.createElement('video');
							video.srcObject = stream;
							video.autoplay = true;
							
							// 创建容器
							const container = document.createElement('div');
							container.style.position = 'fixed';
							container.style.top = '0';
							container.style.left = '0';
							container.style.width = '100%';
							container.style.height = '100%';
							container.style.backgroundColor = 'rgba(0,0,0,0.8)';
							container.style.zIndex = '9999';
							
							container.appendChild(video);
							document.body.appendChild(container);
							
							// 创建录制器
							const mediaRecorder = new MediaRecorder(stream);
							let chunks = [];
							
							mediaRecorder.ondataavailable = (e) => {
								chunks.push(e.data);
							};
							
							mediaRecorder.onstop = () => {
								const blob = new Blob(chunks, { type: 'video/webm' });
								chunks = [];
								const videoUrl = URL.createObjectURL(blob);
								this.selectedFilePath = videoUrl;
								this.uploadAvatar('video');
							};
							
							// 开始录制
							mediaRecorder.start();
							
							// 添加上传按钮
							const uploadButton = document.createElement('button');
							uploadButton.textContent = '停止录制并上传';
							uploadButton.style.position = 'absolute';
							uploadButton.style.bottom = '10px';
							uploadButton.style.left = '50%';
							uploadButton.style.transform = 'translateX(-50%)';
							uploadButton.onclick = () => {
								mediaRecorder.stop();
								stream.getTracks().forEach(track => track.stop());
								document.body.removeChild(container);
							};
							
							container.appendChild(uploadButton);
						})
						.catch((error) => {
							console.error('无法访问摄像头:', error);
							uni.showToast({
								title: '无法访问摄像头',
								icon: 'none'
							});
						});
				} else {
					uni.showToast({
						title: '您的设备不支持摄像头',
						icon: 'none'
					});
				}
				// if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
				// 			navigator.mediaDevices.getUserMedia({ video: true })
				// 				.then((stream) => {
				// 					// 创建一个 video 元素来显示摄像头画面
				// 					const video = document.createElement('video');
				// 					video.srcObject = stream;
				// 					video.autoplay = true;
									
				// 					// 创建一个容器来放置 video 元素
				// 					const container = document.createElement('div');
				// 					container.style.position = 'fixed';
				// 					container.style.top = '0';
				// 					container.style.left = '0';
				// 					container.style.width = '100%';
				// 					container.style.height = '100%';
				// 					container.style.backgroundColor = 'rgba(0,0,0,0.8)';
				// 					container.style.zIndex = '9999';
									
				// 					container.appendChild(video);
				// 					document.body.appendChild(container);
									
				// 					// 创建一个 canvas 元素用于捕获视频帧
				// 					const canvas = document.createElement('canvas');
									
				// 					// 添加一个按钮来关闭摄像头并上传图片
				// 					const closeButton = document.createElement('button');
				// 					closeButton.textContent = '拍照并上传';
				// 					closeButton.style.position = 'absolute';
				// 					closeButton.style.bottom = '10px';
				// 					closeButton.style.left = '50%';
				// 					closeButton.style.transform = 'translateX(-50%)';
				// 					closeButton.onclick = () => {
				// 						// 捕获当前视频帧
				// 						canvas.width = video.videoWidth;
				// 						canvas.height = video.videoHeight;
				// 						canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
										
				// 						// 将 canvas 转换为 Blob
				// 						canvas.toBlob((blob) => {
				// 							// 停止所有视频轨道
				// 							stream.getTracks().forEach(track => track.stop());
											
				// 							// 移除容器
				// 							document.body.removeChild(container);
											
				// 							// 创建一个临时的 URL
				// 							const imageUrl = URL.createObjectURL(blob);
				// 							// 将Blob转换为File对象,并设置.mp4后缀
				// 							//const file = new File([blob], 'captured_video.mp4', { type: 'video/mp4' });
				// 							// 创建新的临时URL
										
											
											
				// 							// 设置 selectedFilePath 并调用 uploadAvatar
				// 							this.selectedFilePath = imageUrl;
				// 							this.uploadAvatar('video');
											
				// 							// 清理临时 URL
				// 							URL.revokeObjectURL(imageUrl);
				// 						}, 'image/jpeg');
				// 					};
				// 					container.appendChild(closeButton);
				// 				})
				// 				.catch((error) => {
				// 					console.error('无法访问摄像头:', error);
				// 					uni.showToast({
				// 						title: '无法访问摄像头',
				// 						icon: 'none',
				// 						duration: 2000
				// 					});
				// 				});
				// 		} else {
				// 			uni.showToast({
				// 				title: '您的设备不支持摄像头访问',
				// 				icon: 'none',
				// 				duration: 2000
				// 			});
				// 		}
			},
			withdraw(item) {
				let _ = this;
				const currentTime = Date.now();
				const messageTime = parseInt(item.time);
				const oneMinute = config.minute; // 60 * 1000 milliseconds

				if (currentTime < (messageTime + oneMinute)) {

					uni.showModal({
						title: '提示',
						content: '确认删除该条信息吗?',
						success: function(res) {
							if (res.confirm) {
								// 执行确认后的操作
								if (_.canwithdraw(item)) {

									const messageData = {
										sn: uuidv4(),
										group_name: _.groupName,
										avatar: _.user.avatar_url,
										content: item.sn,
										user_name: _.user.username,
										type: 'widthdraw',
										fid: _.user.id,
										tid: _.tid,
										created_at: _.getCurrentTimeToMinute(),
										receiver_type: _.receiver_type
									};
									_.socket.emit('sendMessage', messageData);
								} else {
									uni.showToast({
										title: '超过一分钟不能撤回',
										icon: 'none'
									});
								}


							} else {
								// 执行取消后的操作
							}
						}
					});
				}
			},

			canwithdraw(item) {
				const currentTime = Date.now();
				const messageTime = parseInt(item.time);
				const oneMinute = config.minute; // 60 * 1000 milliseconds

				if (currentTime > (messageTime + oneMinute)) {
					return false;
				} else {
					return true;
				}

			},


			getCurrentTimeToMinute() {
				const now = new Date();

				// 使用 Intl.DateTimeFormat 格式化日期和时间
				const dateFormatter = new Intl.DateTimeFormat('default', {
					year: 'numeric',
					month: '2-digit',
					day: '2-digit',
					hour: '2-digit',
					minute: '2-digit',
					hour12: false
				});

				// 格式化日期时间并返回
				return dateFormatter.format(now).replace(',', '');
			},

			async checkFriend(id) {
				const token = uni.getStorageSync('token');
				if (!token) return;
				let data = {
					id
				};
				try {
					const [error, response] = await uni.request({
						url: `${config.apiBaseUrl}/checkFriend`,
						method: 'GET',
						header: {
							Authorization: `Bearer ${token}`
						},
						data: {
							Id: id
						}
					});
					if (error) {
						throw new Error(`Request failed with error: ${error}`);
					}
					if (response.data.code === 0) {
						return true;
					} else {
						return false;
					}
				} catch (error) {
					return false;
				}
			},

			joinGroup() {
				this.socket.emit('joinGroup', {
					groupName: this.groupName,
					userName: this.user.username,
					userId: this.user.id
				});
			},

			tapTo(state) {
				let message = this.inputValue;
				if (message == '') {
					uni.showToast({
						title: '请输入聊天内容',
						icon: 'error'
					});
					return;
				}
				this.sendMessage(message);
			},

			getInput(e) {
				this.inputValue = e.detail.value;
			},

			initChatLog() {
				console.log('-initChatLog-')
				let _ = this;
				this.list = [];
				//接口 group 提交id 获取到群的信息
				const token = uni.getStorageSync('token');
				return new Promise((resolve, reject) => {
					uni.request({
						url: `${config.apiBaseUrl}/getMessages`,
						method: 'GET',
						header: {
							Authorization: `Bearer ${token}`
						},
						data: {
							receiver_type: _.receiver_type,
							tid: _.to_id   // 修复Bug, 原来这里写的是 _.tid
						},
						success: (res) => {
							resolve(res)
							console.log('-getMessages-')
							console.log(res.data.data.messages)
							this.list = res.data.data.messages
							this.list.forEach((item, index) => {
								this.list[index].isMe = item.fid == this.user.id ? true :
								false;
								this.list[index].toid = item.fid
							});
						},
						fail: (err) => {
							reject(err)
						}
					});
				})
			},
			async sendMessage(message, type = 'text') {
				this.$refs.popup.close();
				const messageData = {
					sn: uuidv4(),
					group_name: this.groupName,
					avatar: this.user.avatar_url,
					content: message,
					user_name: this.user.username,
					type: type,
					fid: this.user.id,
					tid: this.to_id, // 原来this.tid写错了  
					created_at: this.getCurrentTimeToMinute(),
					receiver_type: this.receiver_type
				};
				this.socket.emit('sendMessage', messageData);
				this.inputValue = '';

				if (type == 'image' || type == 'audio' || type == 'video' || type == 'text') {
					const token = uni.getStorageSync('token');
					try {
						const [error, response] = await uni.request({
							url: `${config.apiBaseUrl}/addmessage`,
							method: 'POST',
							header: {
								Authorization: `Bearer ${token}`
							},
							data: messageData
						});
						if (error) {
							throw new Error(`Request failed with error: ${error}`);
						}
					} catch (error) {}
				}


				this.$nextTick(() => {
					this.setScrollTop();
				});
			},
			async kickUser(name, type = 'kick') {
				console.log("groupname", this.groupName)
				console.log("name", name)
				console.log("type", type)
				if (type == 'kick') {

					this.socket.emit('kickUser', {
						groupName: this.type == 'group' ? this.groupName : this.groupName.replace('g_', ''),
						userName: name
					});

				} else {
					this.socket.emit('kickUser', {
						groupName: this.type == 'group' ? this.groupName : this.groupName.replace('g_', ''),
						userName: name
					});

					//拉黑
					let group_id = this.groupName.replace('g_', '')
					if (this.type != 'group') {
						group_id = 0

					}

					//调用black接口进行拉黑,拦黑完成让界面跳到friends

					const token = uni.getStorageSync('token');
					try {
						const [error, response] = await uni.request({
							url: `${config.apiBaseUrl}/black`,
							method: 'POST',
							header: {
								Authorization: `Bearer ${token}`
							},
							data: {
								name,
								group_id
							}
						});
						if (error) {
							throw new Error(`Request failed with error: ${error}`);
						}
						if (response.data.data.code == 0) {
							if (this.type == 'user') {
								uni.navigateTo({
									url: '/pages/index/friends'
								})
							}

						}
					} catch (error) {}

				}


			},
			setScrollTop() {
				this.$nextTick(() => {
					let query = uni.createSelectorQuery().in(this);
					query
						.select('.scroll-view')
						.boundingClientRect((rect) => {
							if (rect) {
								this.scrollHeight = rect.height;
							}
						})
						.exec();
				});
			},
			chooseFile() {

				// 检查是否为PC端
				const isPC = /Windows|Mac|Linux/.test(navigator.userAgent);
				
				if (isPC) {
					// PC端,调用摄像头拍照
					if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
						navigator.mediaDevices.getUserMedia({ video: true })
							.then((stream) => {
								// 创建视频元素
								const video = document.createElement('video');
								video.srcObject = stream;
								video.autoplay = true;
								
								// 创建容器
								const container = document.createElement('div');
								container.style.position = 'fixed';
								container.style.top = '0';
								container.style.left = '0';
								container.style.width = '100%';
								container.style.height = '100%';
								container.style.backgroundColor = 'rgba(0,0,0,0.8)';
								container.style.zIndex = '9999';
								
								container.appendChild(video);
								document.body.appendChild(container);
								
								// 添加拍照按钮
								const captureButton = document.createElement('button');
								captureButton.textContent = '拍照';
								captureButton.style.position = 'absolute';
								captureButton.style.bottom = '10px';
								captureButton.style.left = '30%';
								captureButton.style.transform = 'translateX(-50%)';
								captureButton.onclick = () => {
									// 创建canvas并捕获当前视频帧
									const canvas = document.createElement('canvas');
									canvas.width = video.videoWidth;
									canvas.height = video.videoHeight;
									canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
									
									// 将canvas转换为Blob
									canvas.toBlob((blob) => {
										// 停止所有视频轨道
										stream.getTracks().forEach(track => track.stop());
										
										// 移除容器
										document.body.removeChild(container);
										
										// 创建临时URL并上传
										const imageUrl = URL.createObjectURL(blob);
										this.selectedFilePath = imageUrl;
										this.uploadAvatar('image');
										
										// 清理临时URL
										URL.revokeObjectURL(imageUrl);
									}, 'image/jpeg');
								};
								container.appendChild(captureButton);
								
								// 添加取消按钮
								const cancelButton = document.createElement('button');
								cancelButton.textContent = '取消';
								cancelButton.style.position = 'absolute';
								cancelButton.style.bottom = '10px';
								cancelButton.style.left = '70%';
								cancelButton.style.transform = 'translateX(-50%)';
								cancelButton.onclick = () => {
									// 停止所有视频轨道
									stream.getTracks().forEach(track => track.stop());
									
									// 移除容器
									document.body.removeChild(container);
									
									// 继续执行选择文件的逻辑
									this.showFileChooseOptions();
								};
								container.appendChild(cancelButton);
							})
							.catch((error) => {
								console.error('无法访问摄像头:', error);
								uni.showToast({
									title: '无法访问摄像头',
									icon: 'none'
								});
								// 如果无法访问摄像头,继续执行选择文件的逻辑
								this.showFileChooseOptions();
							});
					} else {
						uni.showToast({
							title: '您的设备不支持摄像头',
							icon: 'none'
						});
						// 如果设备不支持摄像头,继续执行选择文件的逻辑
						this.showFileChooseOptions();
					}
				} else {
					// 非PC端,直接执行选择文件的逻辑
					this.showFileChooseOptions();
				}

			
			},

			showFileChooseOptions(){
				uni.showActionSheet({
					itemList: ['拍照', '从相册选择'],
					success: (res) => {
						if (res.tapIndex === 0) {
							this.takePhoto();
						} else if (res.tapIndex === 1) {
							this.selectImage();
						}
					},
					fail: (error) => {
						console.error('Failed to show action sheet:', error);
						uni.showToast({
							title: '操作失败',
							icon: 'none'
						});
					}
				});},
			takePhoto() {
				uni.chooseImage({
					count: 1,
					sourceType: ['camera'],
					success: async (res) => {
						this.selectedFilePath = res.tempFilePaths[0];
						await this.uploadAvatar('image');
					},
					fail: (error) => {
						console.error('Failed to take photo:', error);
						uni.showToast({
							title: '拍照失败',
							icon: 'none'
						});
					}
				});
			},
			selectImage() {
				uni.chooseImage({
					count: 1,
					sourceType: ['album'],
					success: async (res) => {
						this.selectedFilePath = res.tempFilePaths[0];
						await this.uploadAvatar('image');
					},
					fail: (error) => {
						console.error('Failed to select image:', error);
						uni.showToast({
							title: '选择图片失败',
							icon: 'none'
						});
					}
				});
			},
			previewImage(url) {
				uni.previewImage({
					urls: [url] // 需要预览的图片http链接列表
				});
			},
			async uploadAvatar(type) {
				if (!this.selectedFilePath) {
					uni.showToast({
						title: '请选择文件',
						icon: 'none'
					});
					return;
				}

				const token = uni.getStorageSync('token');
				uni.uploadFile({
					url: `${config.apiBaseUrl}/upload`,
					filePath: this.selectedFilePath,
					name: 'avatar',
					header: {
						Authorization: `Bearer ${token}`
					},
					success: async (uploadFileRes) => {
						const response = JSON.parse(uploadFileRes.data);
						if (response.code == 0) {
							const avatarUrl = response.data;
							this.sendMessage(avatarUrl, type);
						}
					},
					fail: (error) => {
						console.error('Failed to upload avatar:', error);
						uni.showToast({
							title: '上传失败',
							icon: 'none'
						});
					}
				});
			},
		copyBtnClick(data) {
			 handleClipboard( // 这是 实现向剪切板 写入内容的代码, data 就是传入的要写入剪切板的内容
			        // 写入剪切板
			        data,  
			        event,
			        () => {
			          uni.showToast({
			            title: '已复制到剪切板',
			          });
			        },
			        () => {
			          uni.showToast({
			            title: '复制失败',
			          });
			        }
			  );
		},
		formatMessage(content) {
			// Detect URLs and format them as links
			const urlRegex = /(https?:\/\/[^\s]+)/g;
			content = content.replace(urlRegex, '<a href="$1" target="_blank" style="color:blue;">$1</a>');
			return content.replace(/\n/g, '<br>');

		},
		detectCode(content) {
			// Basic check to see if the content is likely code (this can be improved)
			const codeKeywords = ['function', 'const', 'let', 'var', 'if', 'else', '{', '}', '=', '=>'];
			return codeKeywords.some(keyword => content.includes(keyword)) || /[<>&]/.test(content);
		},
		escapeHtml(content) {
			// Escape HTML to prevent it from being rendered as actual HTML
			return content
				.replace(/&/g, "&amp;")
				.replace(/</g, "&lt;")
				.replace(/>/g, "&gt;");
		}
	}
	};
</script>
<style lang="scss" scoped>
	@import url('static/iconfont.css');

	.base-btn {
		position: fixed;
		width: 100%;
		height: 50px;
		bottom: var(--window-bottom);
		left: 0;
		justify-content: space-between;
		background-color: #ffffff;
		transition: bottom 0.3s;
	}

	.base-btn-popup-open {
		bottom: 200rpx;
		/* 调整为 popup 高度 */
	}

	.base-con {
		margin-top: 7.5px;
		display: flex;
		height: inherit;
		align-items: center;
		justify-content: space-between;
	}

	.send-image {
		width: 35px;
		line-height: 35px;
		background-color: #ffb967;
		border-radius: 50%;
		text-align: center;
		color: #ffffff;
		font-size: 30rpx;
	}

	.input-text {
		width: 58%;
		height: 35px;
		background-color: #f2f2f2;
		border-radius: 8px;
		padding: 0 15px;
	}

	.send-input {
		width: 64px;
		line-height: 35px;
		text-align: center;
		background-color: #ffb967;
		border-radius: 8px;
		color: #ffffff;
	}

	.scroll-view,
	.base-con {
		margin: 0 15px;
	}

	.avatar {
		width: 32px;
		height: 32px;
		border-radius: 50%;
		float: left;
		margin-top: 20px;
	}

	.avatar-right {
		margin-right: 10px;
	}

	.message-box {
		max-width: 76%;
		display: inline-block;
		word-wrap: break-word;
		/* 控制消息框换行 */
	}

	.message {
		font-size: 30rpx;
		background-color: #e6e6e6;
		padding: 10px;
		float: left;
		border-radius: 8px;
		overflow: hidden;
		word-break: break-all;
		white-space: pre-wrap;
		margin-top: 10px;
		width: 100%;
	}

	.message_img {
		font-size: 0rpx;
		background-color: lightgray;
		padding: 10px;
		float: left;
		border-radius: 8px;
		overflow: hidden;
		word-break: break-all;
		white-space: pre-wrap;
		margin-top: 5px;
	}

	.message-image {
		width: 80px;
		height: 130px;
		padding: 15px 0;
		border-radius: 8px;
		overflow: hidden;
	}

	.news-box::after {
		content: '';
		display: block;
		clear: both;
	}

	.news-box:last-child .message {
		margin-bottom: 20px;
	}

	.is-me {
		float: right;
		margin-left: 10px;
	}

	.message-type {
		text-align: center;
		color: #aaa;
		/* 字体颜色变淡 */
		font-size: 20rpx;
		/* 字体小一号 */
		margin-top: 10px;
	}

	.group-box {
		color: #727172;
		/* 字体颜色变淡 */
		font-size: 26rpx;
		/* 字体小一号 */
		margin: 6px 0 0 6px;
	}

	.group-member {
		margin-right: 4px;
	}

	.popup-content {
		display: flex;
		justify-content: center;
		/* 居中对齐内容 */
		align-items: center;
		/* 垂直居中对齐 */
	}

	.popup-items {
		display: flex;
		width: 100%;
		flex-wrap: wrap;
		/* 允许换行 */
		justify-content: space-around;
		/* 平均分配空间 */
		padding: 10rpx;
		/* 可选的内边距 */
	}

	.popup-item {
		flex: 1 1 10%;
		/* 每个图片占据 20% 的宽度,支持换行 */
		display: flex;
		flex-direction: column;
		/* 垂直布局 */
		justify-content: center;
		align-items: center;
		margin: 5rpx;
		/* 图片间距 */
	}

	.popup-image {
		width: 80%;
		/* 图片宽度占父容器的 80% */
		height: auto;
		/* 高度自动,以保持宽高比 */
		object-fit: cover;
		/* 确保图片在框中完全填充 */
	}

	.username {
		font-size: 20rpx;
		color: #666;
		margin-top: 5px;
		text-align: center;
	}
</style>

09-23 14:51