baodeng_xcx/page_user/pingzhuo/qunliao.vue
2025-06-06 11:14:06 +08:00

771 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<u-navbar title="拼桌群聊" :border-bottom="false" :background="bgc" back-icon-color="#fff" title-color='#fff'
title-size='36' height='44' id="navbar">
</u-navbar>
<view class="tablexx">
<view class="top">
<view class="zhuti">
拼桌主题
</view>
<!-- <button open-type="share"><image src="https://api.ccttiot.com/smartmeter/img/static/uOBU07kcJQjEVSNLyxPC" mode=""></image></button> -->
</view>
<view class="jieshao">
{{pinzhuoobj.topic}}
</view>
<view class="dangqian">
当前桌号{{pinzhuoobj.boothName}}
</view>
<view class="pianhao">
<view class="">
<image style="height:26rpx;" src="https://api.ccttiot.com/smartmeter/img/static/uSFf09L8mtvqhjMOu0RV" mode=""></image>
{{pinzhuoobj.prefer == 1 ? '意向偏好男性' : pinzhuoobj.prefer == 2 ? '意向偏好女性' : '男女不限'}}
</view>
<view class="">
<image src="https://api.ccttiot.com/smartmeter/img/static/uL0SnLLV5kxVrM1dGGsQ" mode=""></image> 已加入{{pinzhuoobj.currentNum}} | 还可以加入{{Number(pinzhuoobj.limitNum == null ? 0 : pinzhuoobj.limitNum) - Number(pinzhuoobj.currentNum == null ? 0 : pinzhuoobj.currentNum)}}
</view>
</view>
</view>
<view class="chat-container" :style="{ transform: `translateY(-${keyboardHeight}px)` }">
<scroll-view
scroll-y="true"
class="message-list"
:scroll-top="scrollTop"
:scroll-with-animation="true"
:scroll-anchoring="false"
:enhanced="true"
:bounces="false"
@scrolltoupper="loadMoreMessages"
refresher-enabled @refresherrefresh="onRefresh" :refresher-triggered="isRefreshing" refresher-default-style="white"
id="messageList">
<view v-for="(messages, index) in messages" :key="index" class="message-item">
<view style="width: 100%;color: #666;text-align: center;font-size: 20rpx;margin-top: 20rpx;margin-bottom: 10rpx;">{{ messages.createTime }}</view>
<view v-if="messages.sendId == userId" class="message-self">
<view class="message-content" v-if="messages.type == 1">
{{ messages.content }}
<view class="message-status" v-if="messages.status">
<image v-if="messages.status === 'sending'" src="https://api.ccttiot.com/smartmeter/img/static/loading" class="status-icon sending"></image>
<image v-else-if="messages.status === 'failed'" src="https://api.ccttiot.com/smartmeter/img/static/failed" class="status-icon failed" @click="resendMessage(messages)"></image>
</view>
</view>
<image class="message-image" v-if="messages.type == 2" :src="messages.content" mode="widthFix" @click="previewImage(messages.content)"></image>
<video class="message-video" v-if="messages.type == 3" :src="messages.content" object-fit="contain" :id="'video-' + index"></video>
<image class="avatar" :src="messages.senderAvatar" v-if="messages.type != 4" mode=""></image>
</view>
<view v-else class="message-other">
<image class="avatar" :src="messages.senderAvatar" v-if="messages.type != 4" mode=""></image>
<view v-if="messages.type == 4" style="width: 100%;color: #666;text-align: center;">{{ messages.content }}</view>
<view class="message-content" v-if="messages.type == 1">{{ messages.content }}</view>
<image class="message-image" v-if="messages.type == 2" :src="messages.content" mode="widthFix" @click="previewImage(messages.content)"></image>
<video class="message-video" v-if="messages.type == 3" :src="messages.content" object-fit="contain" :id="'video-' + index"></video>
</view>
</view>
</scroll-view>
<view class="bottom-area">
<view class="input-container">
<input
type="text"
v-model="inputContent"
placeholder="说点什么..."
class="input-box"
:adjust-position="false"
:cursor-spacing="20"
:hold-keyboard="true"
@confirm="sendMessage(1)"
@focus="onInputFocus"
@blur="onInputBlur"
/>
<button @click="sendMessage(1)" class="send-button">发送</button>
</view>
<view class="function-icons">
<view class="icon-item" @click="showMediaActionSheet">
<image src="https://api.ccttiot.com/smartmeter/img/static/uGcE4EknnaOMxnqivQgL" mode="aspectFit"></image>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
bgc: {
backgroundColor: "#010000",
},
messages: [
{ content: '倡导聊天过程文明善意~ 谨慎判断,切勿私下交易,谨防网络诈骗', isSelf: false },
],
inputContent: '',
scrollTop: 0,
keyboardHeight: 0,
isKeyboardShow: false,
pinzhuoobj:{},
teamId:'',
total:'',
pageNum:1,
isRefreshing: false,
userId:'',
oldScrollTop: 0,
socketTask: null,
isConnected: false,
sendingMessages: new Set(),
reconnectTimer: null,
reconnectCount: 0,
maxReconnectAttempts: 5,
token:''
}
},
onLoad(option) {
this.userId = uni.getStorageSync('user').userId //获取登录用户id
if(option.teamId){
this.teamId = option.teamId
this.getxq()
}
this.getlist()
this.connectWebSocket()
this.gettoken()
// 监听键盘高度变化
uni.onKeyboardHeightChange(res => {
this.keyboardHeight = res.height;
this.isKeyboardShow = res.height > 0;
if (this.isKeyboardShow) {
this.$nextTick(() => {
this.scrollToBottom();
});
}
});
},
onUnload() {
// 页面卸载时断开WebSocket连接
this.disconnectWebSocket();
uni.offKeyboardHeightChange();
},
methods: {
// 下拉刷新更多历史消息
onRefresh() {
if(this.messages.length < this.total){
this.isRefreshing = true
// 保存当前滚动位置
const query = uni.createSelectorQuery().in(this);
query.select('#messageList').boundingClientRect(data => {
this.oldScrollTop = data.top;
}).exec();
this.pageNum++
this.getlist()
setTimeout(() => {
this.isRefreshing = false
}, 1000)
}else{
this.isRefreshing = true
setTimeout(() => {
this.isRefreshing = false
}, 1000)
}
},
// 查询队伍聊天信息
getlist(){
this.$u.get(`/app/chat/receiveList?pageNum=${this.pageNum}&pageSize=20&teamId=${this.teamId}`).then(res =>{
if(res.code == 200){
this.total = res.total
if(this.pageNum == 1){
this.messages = res.rows.reverse()
this.$nextTick(() => {
this.scrollToBottom()
})
}else{
const oldLength = this.messages.length
this.messages = [...res.rows.reverse(), ...this.messages] //将获取的信息进行倒转,最新的信息显示最下面
this.$nextTick(() => {
// 计算新增内容的高度并设置滚动位置
const query = uni.createSelectorQuery().in(this);
query.selectAll('.message-item').boundingClientRect(items => {
if (items && items.length > oldLength) {
let newHeight = 0;
for (let i = 0; i < items.length - oldLength; i++) {
newHeight += items[i].height;
}
this.scrollTop = newHeight;
}
}).exec();
});
}
}
})
},
// 查询拼桌信息
getxq(){
this.$u.get(`/app/team/getTeamInfo?id=${this.teamId}`).then(res =>{
if(res.code == 200){
this.pinzhuoobj = res.data
}
})
},
// 进行WebSocket连接
connectWebSocket() {
if (this.socketTask) {
this.disconnectWebSocket()
}
const token = uni.getStorageSync('token')
const wsUrl = `ws://${this.$store.state.wsUrl}/ws/user?token=${token}`
this.socketTask = uni.connectSocket({
url: wsUrl,
success: () => {
console.log('WebSocket连接成功')
}
})
this.socketTask.onOpen(() => {
console.log('WebSocket连接已打开')
this.isConnected = true
this.reconnectCount = 0 // 重置重连次数
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
})
this.socketTask.onMessage((res) => {
const message = JSON.parse(res.data)
console.log(message,'asdasdasdas')
this.messages.push(message)
this.$nextTick(() => {
this.scrollToBottom()
})
})
this.socketTask.onClose(() => {
console.log('WebSocket连接已关闭')
this.isConnected = false;
this.handleReconnect()
})
this.socketTask.onError((err) => {
console.error('WebSocket错误:', err)
this.isConnected = false
this.handleReconnect()
})
},
disconnectWebSocket() {
if (this.socketTask) {
this.socketTask.close()
this.socketTask = null;
this.isConnected = false;
}
},
handleReconnect() {
if (this.reconnectCount >= this.maxReconnectAttempts) {
uni.showToast({
title: '网络连接失败,请检查网络后重试',
icon: 'none',
duration: 2000
})
return
}
if (!this.reconnectTimer) {
this.reconnectTimer = setTimeout(() => {
this.reconnectCount++
console.log(`尝试第${this.reconnectCount}次重连`);
this.connectWebSocket()
}, 3000) // 3秒后重试
}
},
// 点击发送信息
sendMessage(type) {
if (this.inputContent.trim() !== '') {
const tempId = Date.now().toString();
const tempMessage = {
sendId: this.userId,
content: this.inputContent,
type: type,
status: 'sending',
tempId: tempId,
senderAvatar: uni.getStorageSync('user').avatar || 'https://api.ccttiot.com/smartmeter/img/static/default-avatar'
}
this.sendingMessages.add(tempId);
// this.messages.push(tempMessage);
this.$nextTick(() => {
this.scrollToBottom()
})
let data = {
teamId: this.teamId,
content: this.inputContent,
type: type
}
this.$u.post(`/app/chat/sendMessage`, data).then(res => {
if (res.code == 200) {
const index = this.messages.findIndex(msg => msg.tempId === tempId);
if (index !== -1) {
this.messages[index].status = 'sent';
}
this.sendingMessages.delete(tempId);
} else {
const index = this.messages.findIndex(msg => msg.tempId === tempId);
if (index !== -1) {
this.messages[index].status = 'failed';
}
this.sendingMessages.delete(tempId);
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
});
}
}).catch(err => {
const index = this.messages.findIndex(msg => msg.tempId === tempId);
if (index !== -1) {
this.messages[index].status = 'failed';
}
this.sendingMessages.delete(tempId);
uni.showToast({
title: '发送失败,请重试',
icon: 'none',
duration: 2000
});
});
this.inputContent = '';
}
},
scrollToBottom() {
const query = uni.createSelectorQuery().in(this);
query.select('#messageList').boundingClientRect(data => {
if (data) {
query.selectAll('.message-item').boundingClientRect(items => {
let totalHeight = 0;
items.forEach(item => {
totalHeight += item.height;
});
this.scrollTop = totalHeight;
}).exec();
}
}).exec();
},
loadMoreMessages() {
// 加载更多历史消息的逻辑
// console.log('加载更多消息');
},
onInputFocus() {
this.scrollToBottom()
},
onInputBlur() {
this.isKeyboardShow = false;
this.keyboardHeight = 0;
},
// 预览图片
previewImage(url) {
uni.previewImage({
urls: [url],
current: url,
indicator: 'number',
loop: false
});
},
// 播放视频
playVideo(index) {
const videoContext = uni.createVideoContext('video-' + index, this);
videoContext.requestFullScreen();
videoContext.play();
},
// 重发消息
resendMessage(message) {
const index = this.messages.findIndex(msg => msg.tempId === message.tempId);
if (index !== -1) {
this.messages[index].status = 'sending';
this.sendingMessages.add(message.tempId);
let data = {
teamId: this.teamId,
content: message.content,
type: message.type
};
this.$u.post(`/app/chat/sendMessage`, data).then(res => {
if (res.code == 200) {
this.messages[index].status = 'sent';
this.sendingMessages.delete(message.tempId);
} else {
this.messages[index].status = 'failed';
this.sendingMessages.delete(message.tempId);
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
});
}
}).catch(err => {
this.messages[index].status = 'failed';
this.sendingMessages.delete(message.tempId);
uni.showToast({
title: '发送失败,请重试',
icon: 'none',
duration: 2000
});
});
}
},
// 获取七牛云token
gettoken(){
this.$u.get(`/common/qiniuToken`).then(res => {
if (res.code == 200) {
this.token = res.data
}
})
},
// 显示媒体选择菜单
showMediaActionSheet() {
uni.showActionSheet({
itemList: ['从相册选择', '拍摄视频'],
success: (res) => {
switch (res.tapIndex) {
case 0: // 从相册选择
this.chooseImage();
break;
case 1: // 拍摄视频
this.takeVideo();
break;
}
}
});
},
// 选择图片
chooseImage() {
let _this = this
let math = 'static/' + _this.$u.guid(20)
uni.chooseImage({
count: 9,
type: 'all',
success(res) {
const tempFilePaths = res.tempFiles
wx.uploadFile({
url: 'https://up-z2.qiniup.com',
name: 'file',
filePath: tempFilePaths[0].path,
formData: {
token: _this.token, //后端返回的token
key: 'smartmeter/img/' + math
},
success: function(res) {
console.log(res, 'resres');
let str = JSON.parse(res.data)
_this.inputContent = 'https://api.ccttiot.com/' + str.key
console.log(_this.inputContent)
_this.sendMessage(2)
}
})
}
})
},
// 拍摄视频
takeVideo() {
let _this = this;
let fileKey = 'static/video/' + _this.$u.guid(20) + '.mp4'; // 视频文件后缀
uni.chooseVideo({
sourceType: ['camera'], // 只允许拍摄
maxDuration: 60, // 最长60秒
camera: 'back', // 默认后置摄像头
compressed: true, // 压缩视频
success(res) {
const tempFilePath = res.tempFilePath;
// 显示上传loading
uni.showLoading({
title: '视频上传中...',
mask: true
});
wx.uploadFile({
url: 'https://up-z2.qiniup.com',
name: 'file',
filePath: tempFilePath,
formData: {
token: _this.token, // 后端返回的token
key: 'smartmeter/video/' + fileKey // 修改存储路径为video目录
},
success: function(uploadRes) {
uni.hideLoading();
let responseData = JSON.parse(uploadRes.data);
_this.inputContent = 'https://api.ccttiot.com/' + responseData.key;
// 视频特殊处理:保存缩略图
if(res.thumbTempFilePath){
_this.uploadThumb(res.thumbTempFilePath);
}
_this.sendMessage(3); // 类型改为3表示视频
},
fail(err) {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('视频上传失败:', err);
}
});
},
fail(err) {
console.log('视频选择失败:', err);
}
});
},
// 上传视频缩略图
uploadThumb(thumbPath) {
let _this = this;
let thumbKey = 'static/thumb/' + _this.$u.guid(20) + '.jpg';
wx.uploadFile({
url: 'https://up-z2.qiniup.com',
name: 'file',
filePath: thumbPath,
formData: {
token: _this.token,
key: thumbKey
},
success(res) {
console.log('缩略图上传成功');
}
});
}
}
}
</script>
<style lang="scss">
page {
background: #010000;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.page {
width: 100%;
height: 100%;
}
.chat-container {
display: flex;
flex-direction: column;
height: calc(100vh - 350rpx - 70rpx);
background-color: #010000;
width: 750rpx;
position: relative;
top: 240rpx;
transition: transform 0.3s;
.message-list {
flex: 1;
padding: 20rpx;
width: 100%;
height: calc(100% - 120rpx);
overflow: hidden;
box-sizing: border-box;
.message-item {
margin-bottom: 20rpx;
.avatar {
width: 60rpx;
height: 60rpx;
background-color: #ccc;
border-radius: 50%;
margin-left: 15rpx;
}
.message-self {
display: flex;
justify-content: flex-end;
.message-content {
background-color: #4D3F53;
color: white;
padding: 15rpx 20rpx;
border-radius: 10rpx;
max-width: 70%;
position: relative;
.message-status {
position: absolute;
right: -40rpx;
bottom: 0;
display: flex;
align-items: center;
.status-icon {
width: 32rpx;
height: 32rpx;
&.sending {
animation: rotate 1s linear infinite;
}
&.failed {
opacity: 0.7;
}
}
}
}
.message-image {
max-width: 400rpx;
border-radius: 10rpx;
}
.message-video {
max-width: 400rpx;
border-radius: 10rpx;
}
}
.message-other {
display: flex;
align-items: flex-start;
.avatar {
width: 60rpx;
height: 60rpx;
background-color: #ccc;
border-radius: 50%;
margin-right: 15rpx;
}
.message-content {
background-color: #333;
color: white;
padding: 15rpx 20rpx;
border-radius: 10rpx;
max-width: 70%;
}
.message-image {
max-width: 400rpx;
border-radius: 10rpx;
}
.message-video {
max-width: 400rpx;
border-radius: 10rpx;
}
}
}
}
.bottom-area {
background-color: #010000;
border-top: 1rpx solid #333;
.input-container {
display: flex;
padding: 20rpx;
.input-box {
flex: 1;
height: 80rpx;
background-color: #333;
color: white;
padding: 0 20rpx;
border-radius: 40rpx;
font-size: 28rpx;
}
.send-button {
margin-left: 20rpx;
background-color: #FF8998;
color: white;
border: none;
border-radius: 40rpx;
padding: 0 30rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
}
}
}
}
.tablexx{
width: 730rpx;
height: 266rpx;
background: #010000;
border-radius: 20rpx 20rpx 20rpx 20rpx;
border-image: linear-gradient(226deg, rgba(255, 137.00000703334808, 152.0000061392784, 0.1899999976158142), rgba(255, 137.00000703334808, 152.0000061392784, 0)) 2 2;
padding: 32rpx 36rpx;
box-sizing: border-box;
position: fixed;
top: 150rpx;
left: 50%;
transform: translateX(-50%);
z-index: 99;
.jieshao{
font-size: 24rpx;
color: #FFFFFF;
margin-top: 16rpx;
}
.dangqian{
font-weight: 600;
font-size: 28rpx;
color: #FFFFFF;
margin-top: 26rpx;
}
.pianhao{
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
view{
margin-top: 20rpx;
font-size: 24rpx;
color: #6E7191;
display: flex;
align-items: center;
image{
height: 32rpx;
width: 32rpx;
margin-right: 16rpx;
}
}
}
.top{
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 32rpx;
color: #FFFFFF;
button{
background-color: transparent;
margin: 0 !important;
width: 32rpx;
height: 32rpx;
line-height: 32rpx;
position: relative;
image{
width: 32rpx;
height: 32rpx;
position: absolute;
right: 10rpx;
}
}
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.function-icons {
display: flex;
padding: 20rpx;
border-top: 1rpx solid #333;
.icon-item {
width: 60rpx;
height: 60rpx;
image {
width: 100%;
height: 100%;
}
}
}
</style>