比这之前优化了以下功能
上线通知
群聊里适时显示在线人数
约请好友 通过好友通过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, //群主id
fid: '',
to_id: 0,
recordingPath: '', // 用于存储录音文件的路径
isRecording: false,
mediaRecorder: null,
audioChunks: []
};
},
computed: {
...mapState(['user']),
windowObj() {
let obj;
uni.getSystemInfo({
success: (res) => {
obj = res;
}
});
return obj;
}
},
watch: {
isPopupOpen(newValue) {
if (!newValue) {
this.$refs.popup.close();
}
},
isPopupAudioOpen(newValue) {
if (!newValue) {
this.$refs.popupAudio.close();
}
}
},
async onLoad(q) {
let _ = this;
try {
if (q && q.id != undefined) {
this.groupName = q.id;
this.tid = q.tid;
this.to_id = q.to_id
this.receiver_type = q.type;
this.type = this.receiver_type
uni.setNavigationBarTitle({
title: q.type == 'group' ? '[群聊] '+q.to_name: '[私聊] '+q.to_name
});
if (q.type == 'group') {
//将q.id的前面g_去掉
let newid = q.id.replace('g_', '')
//获到了当前群的群主id
let re = await _.getGroupOwner(newid)
this.group_owner_id = re.data.data.owner_id
}
let re = await _.checkFriend(q.id);
if (re == true) {
_.joinGroup(this.groupName);
} else {
uni.navigateTo({
url: '/pages/index/friends'
});
}
} else {
uni.navigateTo({
url: '/pages/index/friends'
});
}
} catch (e) {
uni.navigateTo({
url: '/pages/index/friends'
});
}
},
onUnload() {
this.socket.close();
},
onShow() {
this.fetchUser();
},
mounted() {
this.initChatLog();
this.socket = io(config.apiBaseUrl);
this.socket.on('connect', () => {
console.log('Socket connected:', this.socket.id);
});
this.socket.on('disconnect', () => {
console.log('Socket disconnected');
});
let heartbeatInterval;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;
const startHeartbeat = () => {
heartbeatInterval = setInterval(() => {
if (this.socket.connected) {
this.socket.emit('heartbeat');
console.log('heartbeat')
} else {
reconnectSocket();
}
}, 120000); // 1分钟
};
const reconnectSocket = () => {
if (reconnectAttempts < maxReconnectAttempts) {
this.socket.connect();
reconnectAttempts++;
} else {
clearInterval(heartbeatInterval);
uni.showModal({
title: '连接失败',
content: '无法连接到服务器,是否手动重新连接?',
confirmText: '重新连接',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
reconnectAttempts = 0;
this.socket.connect();
startHeartbeat();
}
}
});
}
};
startHeartbeat();
this.socket.on('reconnect', () => {
console.log('Socket重新连接成功');
reconnectAttempts = 0;
});
this.socket.on('message', (msg) => {
if (msg.type == 'broadcast') {
return;
}
if (msg.type == 'widthdraw') {
//查出 msg.sn 将此记录信息改为撤回
//console.log(msg);
this.list.forEach((item, index) => {
if (item.sn == msg.content) {
this.list[index].content = '[消息已撤回]';
this.list[index].type = 'text';
this.list[index].withdraw = 1;
this.widthdrawRow(item.sn)
}
});
return;
}
let msgs = {
sn: msg.sn,
name: msg.user_name,
avatar: msg.avatar,
isMe: msg.fid == this.user.id ? true : false,
content: msg.content,
type: msg.type,
sn: msg.sn,
createat: Math.floor(Date.now() / 1000),
time: Date.now(),
withdraw: 0,
toid: msg.fid
};
this.list.push(msgs);
this.setScrollTop();
});
// 监听 'userList' 事件
this.socket.on('userList', (users) => {
this.userList = users; // 更新 userList 变量
console.log('- 当前群用户 -')
console.log(this.userList)
});
},
methods: {
...mapActions(['fetchUser', 'logout', 'fetchGroups']),
formatDate() {
return getCurrentDateTime();
},
kickopen(item) {
this.name = item.name
this.toid = item.toid
if (!item.isMe) {
this.$refs.popupkick.open()
}
},
getGroupOwner(id) {
//接口 group 提交id 获取到群的信息
const token = uni.getStorageSync('token');
return new Promise((resolve, reject) => {
uni.request({
url: `${config.apiBaseUrl}/group`,
method: 'GET',
header: {
Authorization: `Bearer ${token}`
},
data: {
id: id
},
success: (res) => {
resolve(res)
},
fail: (err) => {
reject(err)
}
});
})
},
async widthdrawRow(sn) {
const token = uni.getStorageSync('token');
if (!token) return;
try {
const [error, response] = await uni.request({
url: `${config.apiBaseUrl}/withdraw`,
method: 'GET',
header: {
Authorization: `Bearer ${token}`
},
data: {
sn: sn
}
});
if (error) {
throw new Error(`Request failed with error: ${error}`);
}
if (response.data.code === 0) {
return true;
} else {
return false;
}
} catch (error) {
return false;
}
},
adduserTogroup() {
this.isPopupOpen=false
uni.navigateTo({
url: '/pages/index/addfriend?groupId=' + this.tid
});
},
kick(type) {
//将用户踢出去
if (this.group_owner_id != this.fid) {
//这样才能踢
if (type == 'kick') {
this.kickUser(this.name)
} else {
//拉黑
this.kickUser(this.name, 'black')
//再拉黑
}
} else {
uni.showToast({
title: '不能对自己操作'
})
}
},
detail() {
uni.navigateTo({
url: '/pages/index/about?id=' + this.to_id
});
},
groupdetail() {
let groupid = this.groupName.replace('g_', '')
if (this.type == 'group') {
uni.navigateTo({
url: '/pages/index/groupdetail?id=' + groupid
});
} else {
uni.navigateTo({
url: '/pages/index/about?id=' + this.to_id
});
}
},
async quitgroup() {
console.log(this.group_owner_id)
console.log(this.user.id)
if (this.group_owner_id == this.user.id) {
//主人不能退群
uni.showToast({
title: '主人不能退群'
})
return
}
let groupid = this.groupName.replace('g_', '')
//调用接口退出 接口名为leavgroup
const token = uni.getStorageSync('token');
if (!token) return;
try {
const [error, response] = await uni.request({
url: `${config.apiBaseUrl}/leavgroup`,
method: 'GET',
header: {
Authorization: `Bearer ${token}`
},
data: {
groupid
}
});
if (error) {
throw new Error(`Request failed with error: ${error}`);
}
console.log(response)
if (response.data.code === 0) {
uni.navigateTo({
url: '/pages/index/friends'
})
return true;
} else {
return false;
}
} catch (error) {
return false;
}
},
onPopupChange() {
if (this.isPopupOpen == true) {
this.isPopupOpen = false;
}
},
playVoice(url) {
// 创建音频对象
const audio = new Audio(url);
// 播放音频
audio.play().then(() => {
console.log('音频开始播放');
}).catch((error) => {
console.error('音频播放失败:', error);
});
// 监听音频播放结束事件
audio.onended = () => {
console.log('音频播放结束');
};
},
onPopupAudioChange() {
if (this.isPopupOpen == true) {
this.isPopupOpen = false;
}
this.recordingPath = '';
},
audio() {
this.$refs.popup.close();
this.$refs.popupAudio.open();
this.isPopupOpen = true;
},
exitchat() {
this.$refs.popupAudio.close();
},
async startRecording() {
try {
if(this.isRecording){
uni.showToast({
title: '正在录音中',
icon: 'none',
duration: 2000
});
return;
}
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream);
//console.log(this.mediaRecorder);
this.mediaRecorder.ondataavailable = (event) => {
this.audioChunks.push(event.data);
};
this.mediaRecorder.onstop = async () => {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
const url = URL.createObjectURL(audioBlob);
this.selectedFilePath = url;
// 创建一个提示框
const confirmResult = await new Promise((resolve) => {
uni.showModal({
title: '录音完成',
content: '是否上传录音?',
confirmText: '上传',
cancelText: '取消',
success: (res) => {
resolve(true);
}
});
});
// 如果用户选择取消,则不继续处理
if (!confirmResult) {
this.audioChunks = [];
this.isRecording = false;
return;
}else{
this.uploadAvatar('audio');
}
this.isPopupOpen=false;
this.isRecording=false;
// 清理本地声音
stream.getTracks().forEach(track => track.stop());
URL.revokeObjectURL(url);
};
this.mediaRecorder.start();
this.isRecording = true;
} catch (error) {
console.error('获取麦克风权限失败:', error);
}
},
async stopRecording() {
//console.log('停止录音')
if (this.mediaRecorder) {
this.mediaRecorder.stop();
this.isRecording = false;
this.popupAudio=false;
}else{
uni.showToast({
title: '没有录音',
icon: 'none'
});
}
},
uploadAudio(audioBlob) {
const formData = new FormData();
formData.append('audio', audioBlob, 'recorded_audio.wav');
console.log(URL.createObjectURL(audioBlob))
const token = uni.getStorageSync('token');
uni.uploadFile({
url: `${config.apiBaseUrl}/upload`,
filePath: URL.createObjectURL(audioBlob),
name: 'avatar',
header: {
Authorization: `Bearer ${token}`
},
// formData: formData,
success: (uploadFileRes) => {
const response = JSON.parse(uploadFileRes.data);
if (response.code == 0) {
const avatarUrl = response.data;
this.sendMessage(avatarUrl, 'audio');
}
},
fail: (err) => {
//console.error('上传失败:', err);
console.error('Failed to upload avatar:', error);
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
});
},
playRecording() {
if (this.recordingPath) {
const innerAudioContext = uni.createInnerAudioContext();
innerAudioContext.src = this.recordingPath;
innerAudioContext.onPlay(() => {
console.log('开始播放录音');
});
innerAudioContext.onError((res) => {
console.error('播放录音失败:', res);
});
innerAudioContext.play();
} else {
uni.showToast({
title: '没有可播放的录音',
icon: 'none'
});
}
},
upsong() {
const token = uni.getStorageSync('token');
uni.uploadFile({
url: `${config.apiBaseUrl}/upload`,
filePath: this.selectedFilePath,
name: 'avatar',
header: {
Authorization: `Bearer ${token}`
},
success: async (uploadFileRes) => {
const response = JSON.parse(uploadFileRes.data);
if (response.code == 0) {
const avatarUrl = response.data;
this.sendMessage(avatarUrl, type);
}
},
fail: (error) => {
console.error('Failed to upload avatar:', error);
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
});
},
more() {
this.$refs.popup.open();
this.isPopupOpen = true;
},
openCamera() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then((stream) => {
// 创建视频元素
const video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
// 创建容器
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100%';
container.style.height = '100%';
container.style.backgroundColor = 'rgba(0,0,0,0.8)';
container.style.zIndex = '9999';
container.appendChild(video);
document.body.appendChild(container);
// 创建录制器
const mediaRecorder = new MediaRecorder(stream);
let chunks = [];
mediaRecorder.ondataavailable = (e) => {
chunks.push(e.data);
};
mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
chunks = [];
const videoUrl = URL.createObjectURL(blob);
this.selectedFilePath = videoUrl;
this.uploadAvatar('video');
};
// 开始录制
mediaRecorder.start();
// 添加上传按钮
const uploadButton = document.createElement('button');
uploadButton.textContent = '停止录制并上传';
uploadButton.style.position = 'absolute';
uploadButton.style.bottom = '10px';
uploadButton.style.left = '50%';
uploadButton.style.transform = 'translateX(-50%)';
uploadButton.onclick = () => {
mediaRecorder.stop();
stream.getTracks().forEach(track => track.stop());
document.body.removeChild(container);
};
container.appendChild(uploadButton);
})
.catch((error) => {
console.error('无法访问摄像头:', error);
uni.showToast({
title: '无法访问摄像头',
icon: 'none'
});
});
} else {
uni.showToast({
title: '您的设备不支持摄像头',
icon: 'none'
});
}
// if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
// navigator.mediaDevices.getUserMedia({ video: true })
// .then((stream) => {
// // 创建一个 video 元素来显示摄像头画面
// const video = document.createElement('video');
// video.srcObject = stream;
// video.autoplay = true;
// // 创建一个容器来放置 video 元素
// const container = document.createElement('div');
// container.style.position = 'fixed';
// container.style.top = '0';
// container.style.left = '0';
// container.style.width = '100%';
// container.style.height = '100%';
// container.style.backgroundColor = 'rgba(0,0,0,0.8)';
// container.style.zIndex = '9999';
// container.appendChild(video);
// document.body.appendChild(container);
// // 创建一个 canvas 元素用于捕获视频帧
// const canvas = document.createElement('canvas');
// // 添加一个按钮来关闭摄像头并上传图片
// const closeButton = document.createElement('button');
// closeButton.textContent = '拍照并上传';
// closeButton.style.position = 'absolute';
// closeButton.style.bottom = '10px';
// closeButton.style.left = '50%';
// closeButton.style.transform = 'translateX(-50%)';
// closeButton.onclick = () => {
// // 捕获当前视频帧
// canvas.width = video.videoWidth;
// canvas.height = video.videoHeight;
// canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
// // 将 canvas 转换为 Blob
// canvas.toBlob((blob) => {
// // 停止所有视频轨道
// stream.getTracks().forEach(track => track.stop());
// // 移除容器
// document.body.removeChild(container);
// // 创建一个临时的 URL
// const imageUrl = URL.createObjectURL(blob);
// // 将Blob转换为File对象,并设置.mp4后缀
// //const file = new File([blob], 'captured_video.mp4', { type: 'video/mp4' });
// // 创建新的临时URL
// // 设置 selectedFilePath 并调用 uploadAvatar
// this.selectedFilePath = imageUrl;
// this.uploadAvatar('video');
// // 清理临时 URL
// URL.revokeObjectURL(imageUrl);
// }, 'image/jpeg');
// };
// container.appendChild(closeButton);
// })
// .catch((error) => {
// console.error('无法访问摄像头:', error);
// uni.showToast({
// title: '无法访问摄像头',
// icon: 'none',
// duration: 2000
// });
// });
// } else {
// uni.showToast({
// title: '您的设备不支持摄像头访问',
// icon: 'none',
// duration: 2000
// });
// }
},
withdraw(item) {
let _ = this;
const currentTime = Date.now();
const messageTime = parseInt(item.time);
const oneMinute = config.minute; // 60 * 1000 milliseconds
if (currentTime < (messageTime + oneMinute)) {
uni.showModal({
title: '提示',
content: '确认删除该条信息吗?',
success: function(res) {
if (res.confirm) {
// 执行确认后的操作
if (_.canwithdraw(item)) {
const messageData = {
sn: uuidv4(),
group_name: _.groupName,
avatar: _.user.avatar_url,
content: item.sn,
user_name: _.user.username,
type: 'widthdraw',
fid: _.user.id,
tid: _.tid,
created_at: _.getCurrentTimeToMinute(),
receiver_type: _.receiver_type
};
_.socket.emit('sendMessage', messageData);
} else {
uni.showToast({
title: '超过一分钟不能撤回',
icon: 'none'
});
}
} else {
// 执行取消后的操作
}
}
});
}
},
canwithdraw(item) {
const currentTime = Date.now();
const messageTime = parseInt(item.time);
const oneMinute = config.minute; // 60 * 1000 milliseconds
if (currentTime > (messageTime + oneMinute)) {
return false;
} else {
return true;
}
},
getCurrentTimeToMinute() {
const now = new Date();
// 使用 Intl.DateTimeFormat 格式化日期和时间
const dateFormatter = new Intl.DateTimeFormat('default', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
// 格式化日期时间并返回
return dateFormatter.format(now).replace(',', '');
},
async checkFriend(id) {
const token = uni.getStorageSync('token');
if (!token) return;
let data = {
id
};
try {
const [error, response] = await uni.request({
url: `${config.apiBaseUrl}/checkFriend`,
method: 'GET',
header: {
Authorization: `Bearer ${token}`
},
data: {
Id: id
}
});
if (error) {
throw new Error(`Request failed with error: ${error}`);
}
if (response.data.code === 0) {
return true;
} else {
return false;
}
} catch (error) {
return false;
}
},
joinGroup() {
this.socket.emit('joinGroup', {
groupName: this.groupName,
userName: this.user.username,
userId: this.user.id
});
},
tapTo(state) {
let message = this.inputValue;
if (message == '') {
uni.showToast({
title: '请输入聊天内容',
icon: 'error'
});
return;
}
this.sendMessage(message);
},
getInput(e) {
this.inputValue = e.detail.value;
},
initChatLog() {
console.log('-initChatLog-')
let _ = this;
this.list = [];
//接口 group 提交id 获取到群的信息
const token = uni.getStorageSync('token');
return new Promise((resolve, reject) => {
uni.request({
url: `${config.apiBaseUrl}/getMessages`,
method: 'GET',
header: {
Authorization: `Bearer ${token}`
},
data: {
receiver_type: _.receiver_type,
tid: _.to_id // 修复Bug, 原来这里写的是 _.tid
},
success: (res) => {
resolve(res)
console.log('-getMessages-')
console.log(res.data.data.messages)
this.list = res.data.data.messages
this.list.forEach((item, index) => {
this.list[index].isMe = item.fid == this.user.id ? true :
false;
this.list[index].toid = item.fid
});
},
fail: (err) => {
reject(err)
}
});
})
},
async sendMessage(message, type = 'text') {
this.$refs.popup.close();
const messageData = {
sn: uuidv4(),
group_name: this.groupName,
avatar: this.user.avatar_url,
content: message,
user_name: this.user.username,
type: type,
fid: this.user.id,
tid: this.to_id, // 原来this.tid写错了
created_at: this.getCurrentTimeToMinute(),
receiver_type: this.receiver_type
};
this.socket.emit('sendMessage', messageData);
this.inputValue = '';
if (type == 'image' || type == 'audio' || type == 'video' || type == 'text') {
const token = uni.getStorageSync('token');
try {
const [error, response] = await uni.request({
url: `${config.apiBaseUrl}/addmessage`,
method: 'POST',
header: {
Authorization: `Bearer ${token}`
},
data: messageData
});
if (error) {
throw new Error(`Request failed with error: ${error}`);
}
} catch (error) {}
}
this.$nextTick(() => {
this.setScrollTop();
});
},
async kickUser(name, type = 'kick') {
console.log("groupname", this.groupName)
console.log("name", name)
console.log("type", type)
if (type == 'kick') {
this.socket.emit('kickUser', {
groupName: this.type == 'group' ? this.groupName : this.groupName.replace('g_', ''),
userName: name
});
} else {
this.socket.emit('kickUser', {
groupName: this.type == 'group' ? this.groupName : this.groupName.replace('g_', ''),
userName: name
});
//拉黑
let group_id = this.groupName.replace('g_', '')
if (this.type != 'group') {
group_id = 0
}
//调用black接口进行拉黑,拦黑完成让界面跳到friends
const token = uni.getStorageSync('token');
try {
const [error, response] = await uni.request({
url: `${config.apiBaseUrl}/black`,
method: 'POST',
header: {
Authorization: `Bearer ${token}`
},
data: {
name,
group_id
}
});
if (error) {
throw new Error(`Request failed with error: ${error}`);
}
if (response.data.data.code == 0) {
if (this.type == 'user') {
uni.navigateTo({
url: '/pages/index/friends'
})
}
}
} catch (error) {}
}
},
setScrollTop() {
this.$nextTick(() => {
let query = uni.createSelectorQuery().in(this);
query
.select('.scroll-view')
.boundingClientRect((rect) => {
if (rect) {
this.scrollHeight = rect.height;
}
})
.exec();
});
},
chooseFile() {
// 检查是否为PC端
const isPC = /Windows|Mac|Linux/.test(navigator.userAgent);
if (isPC) {
// PC端,调用摄像头拍照
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: true })
.then((stream) => {
// 创建视频元素
const video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
// 创建容器
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100%';
container.style.height = '100%';
container.style.backgroundColor = 'rgba(0,0,0,0.8)';
container.style.zIndex = '9999';
container.appendChild(video);
document.body.appendChild(container);
// 添加拍照按钮
const captureButton = document.createElement('button');
captureButton.textContent = '拍照';
captureButton.style.position = 'absolute';
captureButton.style.bottom = '10px';
captureButton.style.left = '30%';
captureButton.style.transform = 'translateX(-50%)';
captureButton.onclick = () => {
// 创建canvas并捕获当前视频帧
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
// 将canvas转换为Blob
canvas.toBlob((blob) => {
// 停止所有视频轨道
stream.getTracks().forEach(track => track.stop());
// 移除容器
document.body.removeChild(container);
// 创建临时URL并上传
const imageUrl = URL.createObjectURL(blob);
this.selectedFilePath = imageUrl;
this.uploadAvatar('image');
// 清理临时URL
URL.revokeObjectURL(imageUrl);
}, 'image/jpeg');
};
container.appendChild(captureButton);
// 添加取消按钮
const cancelButton = document.createElement('button');
cancelButton.textContent = '取消';
cancelButton.style.position = 'absolute';
cancelButton.style.bottom = '10px';
cancelButton.style.left = '70%';
cancelButton.style.transform = 'translateX(-50%)';
cancelButton.onclick = () => {
// 停止所有视频轨道
stream.getTracks().forEach(track => track.stop());
// 移除容器
document.body.removeChild(container);
// 继续执行选择文件的逻辑
this.showFileChooseOptions();
};
container.appendChild(cancelButton);
})
.catch((error) => {
console.error('无法访问摄像头:', error);
uni.showToast({
title: '无法访问摄像头',
icon: 'none'
});
// 如果无法访问摄像头,继续执行选择文件的逻辑
this.showFileChooseOptions();
});
} else {
uni.showToast({
title: '您的设备不支持摄像头',
icon: 'none'
});
// 如果设备不支持摄像头,继续执行选择文件的逻辑
this.showFileChooseOptions();
}
} else {
// 非PC端,直接执行选择文件的逻辑
this.showFileChooseOptions();
}
},
showFileChooseOptions(){
uni.showActionSheet({
itemList: ['拍照', '从相册选择'],
success: (res) => {
if (res.tapIndex === 0) {
this.takePhoto();
} else if (res.tapIndex === 1) {
this.selectImage();
}
},
fail: (error) => {
console.error('Failed to show action sheet:', error);
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
});},
takePhoto() {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: async (res) => {
this.selectedFilePath = res.tempFilePaths[0];
await this.uploadAvatar('image');
},
fail: (error) => {
console.error('Failed to take photo:', error);
uni.showToast({
title: '拍照失败',
icon: 'none'
});
}
});
},
selectImage() {
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: async (res) => {
this.selectedFilePath = res.tempFilePaths[0];
await this.uploadAvatar('image');
},
fail: (error) => {
console.error('Failed to select image:', error);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
previewImage(url) {
uni.previewImage({
urls: [url] // 需要预览的图片http链接列表
});
},
async uploadAvatar(type) {
if (!this.selectedFilePath) {
uni.showToast({
title: '请选择文件',
icon: 'none'
});
return;
}
const token = uni.getStorageSync('token');
uni.uploadFile({
url: `${config.apiBaseUrl}/upload`,
filePath: this.selectedFilePath,
name: 'avatar',
header: {
Authorization: `Bearer ${token}`
},
success: async (uploadFileRes) => {
const response = JSON.parse(uploadFileRes.data);
if (response.code == 0) {
const avatarUrl = response.data;
this.sendMessage(avatarUrl, type);
}
},
fail: (error) => {
console.error('Failed to upload avatar:', error);
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
});
},
copyBtnClick(data) {
handleClipboard( // 这是 实现向剪切板 写入内容的代码, data 就是传入的要写入剪切板的内容
// 写入剪切板
data,
event,
() => {
uni.showToast({
title: '已复制到剪切板',
});
},
() => {
uni.showToast({
title: '复制失败',
});
}
);
},
formatMessage(content) {
// Detect URLs and format them as links
const urlRegex = /(https?:\/\/[^\s]+)/g;
content = content.replace(urlRegex, '<a href="$1" target="_blank" style="color:blue;">$1</a>');
return content.replace(/\n/g, '<br>');
},
detectCode(content) {
// Basic check to see if the content is likely code (this can be improved)
const codeKeywords = ['function', 'const', 'let', 'var', 'if', 'else', '{', '}', '=', '=>'];
return codeKeywords.some(keyword => content.includes(keyword)) || /[<>&]/.test(content);
},
escapeHtml(content) {
// Escape HTML to prevent it from being rendered as actual HTML
return content
.replace(/&/g, "&")
.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;
/* 调整为 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>