当前位置: 首页 > news >正文

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

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

在这里插入图片描述

<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, //群主idfid: '',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_idthis.receiver_type = q.type;this.type = this.receiver_typeuni.setNavigationBarTitle({title: q.type == 'group' ? '[群聊] '+q.to_name: '[私聊] '+q.to_name});if (q.type == 'group') {//将q.id的前面g_去掉let newid = q.id.replace('g_', '')//获到了当前群的群主idlet 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.namethis.toid = item.toidif (!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=falseuni.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 millisecondsif (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 millisecondsif (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.messagesthis.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接口进行拉黑,拦黑完成让界面跳到friendsconst 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转换为Blobcanvas.toBlob((blob) => {// 停止所有视频轨道stream.getTracks().forEach(track => track.stop());// 移除容器document.body.removeChild(container);// 创建临时URL并上传const imageUrl = URL.createObjectURL(blob);this.selectedFilePath = imageUrl;this.uploadAvatar('image');// 清理临时URLURL.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 linksconst 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 HTMLreturn 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>

相关文章:

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

比这之前优化了以下功能 上线通知 群聊里适时显示在线人数 约请好友 通过好友通过socket 相应端自动变化 PC端可以拉取摄象头拍照 PC端可以录音发送 拉起摄象头发送录象 <template><view class""><scroll-view scroll-y"true" class&…...

【Unity Shader】Special Effects(九)Vortex 旋涡(UI)

源码:[点我获取源码] 索引 Vortex 旋涡思路分析旋涡中心旋涡旋转旋涡强度旋涡动画Vortex 旋涡 旋涡效果可以将一张图像以指定点作为旋涡中心,呈顺时针旋涡动画效果,使用动画播放器: 思路分析 首先,旋涡特效的核心也即是旋转(特别是uv坐标的旋转); 在此基础上,旋涡中…...

01_两数之和

一、题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用两次相同的元素。 你可以按任意顺序返回答案。…...

ChatGLM-6B-部署与使用

✨ Blog’s 主页: 白乐天_ξ( ✿&#xff1e;◡❛) &#x1f308; 个人Motto&#xff1a;他强任他强&#xff0c;清风拂山冈&#xff01; &#x1f4ab; 欢迎来到我的学习笔记&#xff01; 什么是ChatGLM-6B 一、简介 ChatGLM-6B 是由清华大学知识工程实验室&#xff08;KEG&…...

李宏毅结构化学习 03

文章目录 一、Sequence Labeling 问题概述二、Hidden Markov Model(HMM)三、Conditional Random Field(CRF)四、Structured Perceptron/SVM五、Towards Deep Learning 一、Sequence Labeling 问题概述 二、Hidden Markov Model(HMM) 上图 training data 中的黑色字为x&#xff…...

java重点学习-总结

十五 总结 https://kdocs.cn/l/crbMWc8xEZda &#xff08;总结全部的精华&#xff09; 1.面试准备 企业筛选简历规则简历编写注意事项(亮点)项目怎么找&#xff0c;学习到什么程度面试过程(表达结构、什么样的心态去找工作) 2.redis 缓存相关(缓存击穿、穿透、雪崩、缓存过期淘…...

文件操作

文件的由来&#xff1a;在程序中&#xff0c;之前每一个程序都是需要运行然后输入数据&#xff0c;当程序结束时输入的数据也随之消散&#xff0c;为了下一次运行时不再输入数据就有文件的由来&#xff0c;使用文件我们可以将数据直接存放在电脑的硬盘上&#xff0c;做到了数据…...

docker存储

docker分层结构 如图所示&#xff0c;容器是由最上面可读可写的容器层&#xff0c;以及若干个只读镜像层组成&#xff0c;创建容器时&#xff0c;容器中的 数据都来自镜像层。这样的分层机构最大的特点是写时复制&#xff1a; 1、容器中新生成的数据会直接存放在容器层&#xf…...

Ubuntu20.04.6 环境下docker设置proxy

问题背景&#xff1a; 在进行dokcer pull操作的时候&#xff0c;会失败且出现如下提示Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting h…...

如何给文件夹里面的文件批量添加前缀和编号(利用C#写的小工具)

运行结果 将上面的文件编号效果 下载过后启动这个程序即可&#xff08;这个程序灵感来源是上次给美术资源分类和编号的时候给我干吐了&#xff0c;所以写了这个工具&#xff09; 体验链接&#xff1a;laozhupeiqia/批处理 --- laozhupeiqia/批处理 (github.com) 如果对你有帮助…...

使用分布式调度框架时需要考虑的问题——详解

引言 随着企业系统的规模不断扩大&#xff0c;特别是在分布式计算和云计算环境下&#xff0c;如何协调多个节点或服务执行任务成为一个关键问题。分布式调度框架在这种背景下应运而生&#xff0c;它可以调度成千上万的任务&#xff0c;在多个节点上分配、执行和监控任务&#…...

C语言编译四大阶段

目录 一、引言 二、预处理阶段 三、编译阶段 四、汇编阶段 五、链接阶段 六、总结 本文将详细介绍C语言编译的四个阶段&#xff0c;包括预处理、编译、汇编和链接。通过学习这些阶段&#xff0c;读者可以更好地理解C语言程序的编译过程&#xff0c;提高编程效率。 一、引…...

C# 关于“您与该网站的连接不是私密连接...”的问题

目录 问题现象 范例运行环境 WebService 类 类介绍 增加参数 实现 小结 问题现象 最近在访问开发的微信支付功能时遇到了无法访问令牌的错误&#xff0c;这个错误是公司内部应用程序接口返回的访问错误。经过排查是访问 HTTPS 站点遇到的错误&#xff0c;提示证书风险…...

【超详细】基于YOLOv8训练无人机视角Visdrone2019数据集

主要内容如下&#xff1a; 1、Visdrone2019数据集介绍 2、下载、制作YOLO格式训练集 3、模型训练及预测 4、Onnxruntime推理 运行环境&#xff1a;Python3.8&#xff08;要求>3.8&#xff09;&#xff0c;torch1.12.0cu113&#xff08;要求>1.8&#xff09;&#xff0c…...

VUE项目在Linux子系统部署

1、导读 环境&#xff1a;Windows 11、python 3.12.3、Django 4.2.11、 APScheduler 3.10.4 vue 背景&#xff1a;换系统需要重新安装&#xff0c;避免后期忘记&#xff0c;此处记录一下啊 事件&#xff1a;20240922 说明&#xff1a;使用node启动&#xff0c;非nginx&…...

开源 | 如何在产品上扩展大储存?合宙LuatOS外挂SPI Flash库轻松搞定

我们都知道芯片的储存都是寸土寸金的&#xff0c;当你的产品需要存储照片、音频、文档等资源的时候&#xff0c;有没有眉头一紧&#xff1f;内部不够只能外扩&#xff0c;但是外扩要编写各种驱动&#xff0c;还有Flash替换&#xff0c;这都要消耗头发啊&#xff01; 但&#x…...

20 基于STM32的温度、电流、电压检测proteus仿真系统(OLED、DHT11、继电器、电机)

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STM32F103C8T6 采用DHT11读取温度、滑动变阻器模拟读取电流、电压。 通过OLED屏幕显示&#xff0c;设置电流阈值为80&#xff0c;电流小阈值为50&#xff0c;电压阈值为60&#xff0c;温度阈值…...

spring自定义属性编辑器

文章目录 spring自定义属性编辑器步骤 spring自定义属性编辑器 属性编辑器是用来解析bean的配置文件中的属性标签的&#xff0c;spring的BeanWrapperImpl默认会注册CustomCollectionEditor(集合)、CustomMapEditor(Map)、CurrencyEditor(货币)、ByteArrayPropertyEditor等&…...

在VMware16中安装Windows 10:完整教程

在VMware中安装Windows 10&#xff1a;完整教程 1.安装环境准备2.创建虚拟机 1.安装环境准备 1.虚拟机: VMware-workstation-full-16.2.2-19200509 2.系统镜像:win10 2.创建虚拟机 1.自定义 2.下一步 3.稍后安装系统 3.默认下一步 4.虚拟机取名和选择存放路径(按需更改…...

MATLAB系列09:图形句柄

MATLAB系列09&#xff1a;图形句柄 9. 图形句柄9.1 MATLAB图形系统9.2 对象句柄9.3 对象属性的检测和更改9.3.1 在创建对象时改变对象的属性9.3.2 对象创建后改变对象的属性 9.4 用 set 函数列出可能属性值9.5 自定义数据9.6 对象查找9.7 用鼠标选择对象9.8 位置和单位9.8.1 图…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

Go 语言接口详解

Go 语言接口详解 核心概念 接口定义 在 Go 语言中&#xff0c;接口是一种抽象类型&#xff0c;它定义了一组方法的集合&#xff1a; // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的&#xff1a; // 矩形结构体…...

渲染学进阶内容——模型

最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...

页面渲染流程与性能优化

页面渲染流程与性能优化详解&#xff08;完整版&#xff09; 一、现代浏览器渲染流程&#xff08;详细说明&#xff09; 1. 构建DOM树 浏览器接收到HTML文档后&#xff0c;会逐步解析并构建DOM&#xff08;Document Object Model&#xff09;树。具体过程如下&#xff1a; (…...

【单片机期末】单片机系统设计

主要内容&#xff1a;系统状态机&#xff0c;系统时基&#xff0c;系统需求分析&#xff0c;系统构建&#xff0c;系统状态流图 一、题目要求 二、绘制系统状态流图 题目&#xff1a;根据上述描述绘制系统状态流图&#xff0c;注明状态转移条件及方向。 三、利用定时器产生时…...

反射获取方法和属性

Java反射获取方法 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是一种强大的机制&#xff0c;允许程序在运行时访问和操作类的内部属性和方法。通过反射&#xff0c;可以动态地创建对象、调用方法、改变属性值&#xff0c;这在很多Java框架中如Spring和Hiberna…...

Android15默认授权浮窗权限

我们经常有那种需求&#xff0c;客户需要定制的apk集成在ROM中&#xff0c;并且默认授予其【显示在其他应用的上层】权限&#xff0c;也就是我们常说的浮窗权限&#xff0c;那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

华硕a豆14 Air香氛版,美学与科技的馨香融合

在快节奏的现代生活中&#xff0c;我们渴望一个能激发创想、愉悦感官的工作与生活伙伴&#xff0c;它不仅是冰冷的科技工具&#xff0c;更能触动我们内心深处的细腻情感。正是在这样的期许下&#xff0c;华硕a豆14 Air香氛版翩然而至&#xff0c;它以一种前所未有的方式&#x…...

Python ROS2【机器人中间件框架】 简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...