比这之前优化了以下功能
上线通知
群聊里适时显示在线人数
约请好友 通过好友通过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 , 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_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' ) { let newid = q. id. replace ( 'g_' , '' ) 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 ) ; } ; 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' ) { 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 ( ) ; } ) ; this . socket. on ( 'userList' , ( users ) => { this . userList = users; 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 ) { 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_' , '' ) 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) ; 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 ( ) { 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} ` } , success : ( uploadFileRes ) => { const response = JSON . parse ( uploadFileRes. data) ; if ( response. code == 0 ) { const avatarUrl = response. data; this . sendMessage ( avatarUrl, 'audio' ) ; } } , fail : ( 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' } ) ; } } , withdraw ( item ) { let _ = this ; const currentTime = Date. now ( ) ; const messageTime = parseInt ( item. time) ; const oneMinute = config. minute; 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; if ( currentTime > ( messageTime + oneMinute) ) { return false ; } else { return true ; } } , getCurrentTimeToMinute ( ) { const now = new Date ( ) ; 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 = [ ] ; 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 } , 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, 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 } 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 ( ) { const isPC = / Windows|Mac|Linux / . test ( navigator. userAgent) ; if ( isPC) { 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 = ( ) => { 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. toBlob ( ( blob ) => { stream. getTracks ( ) . forEach ( track => track. stop ( ) ) ; document. body. removeChild ( container) ; const imageUrl = URL . createObjectURL ( blob) ; this . selectedFilePath = imageUrl; this . uploadAvatar ( 'image' ) ; 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 { 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] } ) ; } , 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, event, ( ) => { uni. showToast ( { title : '已复制到剪切板' , } ) ; } , ( ) => { uni. showToast ( { title : '复制失败' , } ) ; } ) ; } , formatMessage ( content ) { 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 ) { const codeKeywords = [ 'function' , 'const' , 'let' , 'var' , 'if' , 'else' , '{' , '}' , '=' , '=>' ] ; return codeKeywords. some ( keyword => content. includes ( keyword) ) || / [<>&] / . test ( content) ; } , escapeHtml ( content ) { return content. replace ( / & / g , "&" ) . replace ( / < / g , "<" ) . replace ( / > / g , ">" ) ; } } } ;
< / 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; } . 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 % ; display : flex; flex- direction: column; justify- content: center; align- items: center; margin : 5rpx; } . popup- image { width : 80 % ; height : auto; object- fit: cover; } . username { font- size: 20rpx; color : #666 ; margin- top: 5px; text- align: center; }
< / style>