比这之前优化了以下功能
上线通知
群聊里适时显示在线人数
约请好友 通过好友通过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_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' ) {
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. name
this . toid = item. toid
if ( ! 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. 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,
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>