Sprinkler-app/page_user/xiaoxi/siliao.vue
2025-12-26 16:53:58 +08:00

1230 lines
41 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="#333" title-color='#333'
title-size='36' height='44' id="navbar">
</u-navbar>
<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 v-if="messages.sendId == userId" class="message-self">
<view class="message-content" v-if="messages.type == 1">
<view class="message-header">
<view class="message-status" v-if="messages.status">
<image v-if="messages.status === 'sending'" src="https://api.ccttiot.com/smartmeter/img/static/uO9UFjzbzS9YlabzL4HV" class="status-icon sending"></image>
<image v-else-if="messages.status === 'failed'" src="https://api.ccttiot.com/smartmeter/img/static/ualrWMTB5pPYR8UvkCv6" class="status-icon failed" @click="resendMessage(messages)"></image>
<text v-else-if="messages.status === 'sent'" class="status-text">已发送</text>
</view>
<text class="message-time">{{ messages.createTime }}</text>
</view>
<text class="message-text">{{ messages.content }}</text>
</view>
<view class="message-content" v-if="messages.type == 2">
<view class="message-header">
<view class="message-status" v-if="messages.status">
<image v-if="messages.status === 'sending'" src="https://api.ccttiot.com/smartmeter/img/static/uO9UFjzbzS9YlabzL4HV" class="status-icon sending"></image>
<image v-else-if="messages.status === 'failed'" src="https://api.ccttiot.com/smartmeter/img/static/ualrWMTB5pPYR8UvkCv6" class="status-icon failed" @click="resendMessage(messages)"></image>
<text v-else-if="messages.status === 'sent'" class="status-text">已发送</text>
</view>
<text class="message-time">{{ messages.createTime }}</text>
</view>
<image class="message-image" :src="messages.content" mode="widthFix" @click="previewImage(messages.content)"></image>
</view>
<view class="message-content" v-if="messages.type == 3">
<view class="message-header">
<view class="message-status" v-if="messages.status">
<image v-if="messages.status === 'sending'" src="https://api.ccttiot.com/smartmeter/img/static/uO9UFjzbzS9YlabzL4HV" class="status-icon sending"></image>
<image v-else-if="messages.status === 'failed'" src="https://api.ccttiot.com/smartmeter/img/static/ualrWMTB5pPYR8UvkCv6" class="status-icon failed" @click="resendMessage(messages)"></image>
<text v-else-if="messages.status === 'sent'" class="status-text">已发送</text>
</view>
<text class="message-time">{{ messages.createTime }}</text>
</view>
<video class="message-video" :src="messages.content" object-fit="contain" :id="'video-' + index"></video>
</view>
<!-- 礼物消息 type==5 -->
<view class="message-content" v-if="messages.type == 5">
<view class="message-header">
<view class="message-status" v-if="messages.status">
<image v-if="messages.status === 'sending'" src="https://api.ccttiot.com/smartmeter/img/static/uO9UFjzbzS9YlabzL4HV" class="status-icon sending"></image>
<image v-else-if="messages.status === 'failed'" src="https://api.ccttiot.com/smartmeter/img/static/ualrWMTB5pPYR8UvkCv6" class="status-icon failed" @click="resendMessage(messages)"></image>
<text v-else-if="messages.status === 'sent'" class="status-text">已发送</text>
</view>
<text class="message-time">{{ messages.createTime }}</text>
</view>
<view class="gift-bubble">
<image class="gift-image" :src="parseGift(messages.content).image" mode="aspectFill"></image>
<view class="gift-info">
<view class="gift-title">🎁 {{ parseGift(messages.content).goodsName }}</view>
<view class="gift-desc">{{ parseGift(messages.content).description }}</view>
<view class="gift-fromto">
<text>送给</text>
<text class="gift-donee">{{ getUserNameById(parseGift(messages.content).doneeName) }}</text>
</view>
</view>
</view>
</view>
<view v-if="messages.type != 4 && messages.type != 5" style="display: flex; flex-direction: column; align-items: flex-end;">
<image class="avatar" :src="messages.senderAvatar" mode="aspectFill"></image>
<view class="avatar-name">{{ messages.senderName }}</view>
</view>
<view v-if="messages.type == 5" style="display: flex; flex-direction: column; align-items: flex-end;">
<image class="avatar" :src="messages.senderAvatar" mode="aspectFill"></image>
<view class="avatar-name">{{ messages.senderName }}</view>
</view>
</view>
<!-- 别人发的消息 -->
<view v-else class="message-other">
<view v-if="messages.type != 4 && messages.type != 5" style="display: flex; flex-direction: column; align-items: flex-start;">
<image class="avatar" :src="messages.senderAvatar" mode="aspectFill"></image>
<view class="avatar-name">{{ messages.senderName }}</view>
</view>
<view v-if="messages.type == 5" style="display: flex; flex-direction: column; align-items: flex-start;">
<image class="avatar" :src="messages.senderAvatar" mode="aspectFill"></image>
<view class="avatar-name">{{ messages.senderName }}</view>
</view>
<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">
<view class="message-header">
<text class="message-time">{{ messages.createTime }}</text>
</view>
<text class="message-text">{{ messages.content }}</text>
</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>
<!-- 礼物消息 type==5 -->
<view class="message-content" v-if="messages.type == 5">
<view class="message-header">
<text class="message-time">{{ messages.createTime }}</text>
</view>
<view class="gift-bubble">
<image class="gift-image" :src="parseGift(messages.content).image" mode="aspectFill"></image>
<view class="gift-info">
<view class="gift-title">🎁 {{ parseGift(messages.content).goodsName }}</view>
<view class="gift-desc">{{ parseGift(messages.content).description }}</view>
<view class="gift-fromto">
<text>送给</text>
<text class="gift-donee">{{ getUserNameById(parseGift(messages.content).doneeName) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="bottom-area">
<view class="input-container">
<view class="icon-item" @click="showMediaActionSheet">
<image src="https://api.ccttiot.com/smartmeter/img/static/uWOhKZl3r8uWMDgySuGc" mode="aspectFit"></image>
</view>
<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>
</view>
</view>
</template>
<script>
export default {
data() {
return {
bgc: {
backgroundColor: "#fff",
},
messages: [
{ content: '', type: 4 },
],
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: 50,
token:'',
id:''
}
},
onLoad(option) {
this.id = option.id
this.getqkwd()
// if(option.teamId){
// this.teamId = option.teamId
// this.getxq()
// }
this.userId = uni.getStorageSync('userId') //获取登录用户id
// 监听键盘高度变化
uni.onKeyboardHeightChange(res => {
this.keyboardHeight = res.height
this.isKeyboardShow = res.height > 0
if (this.isKeyboardShow) {
this.$nextTick(() => {
this.scrollToBottom()
})
}
})
},
onShow() {
setTimeout(()=>{
this.getlist()
this.connectWebSocket()
this.gettoken()
},1)
},
onUnload() {
// 页面卸载时断开WebSocket连接
this.disconnectWebSocket();
uni.offKeyboardHeightChange();
},
methods: {
// 清空未读消息
getqkwd(){
this.$u.post(`/app/chatMsg/read?sessionId=${this.id}`).then(res => {})
},
// 下拉刷新更多历史消息
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.getlists()
setTimeout(() => {
this.isRefreshing = false
}, 1000)
}else{
this.isRefreshing = true
setTimeout(() => {
this.isRefreshing = false
}, 1000)
}
},
getlists(){
this.$u.get(`/app/chat/list?sessionId=${this.id}&pageNum=${this.pageNum}&pageSize=20`).then(res =>{
if(res.code == 200){
this.total = res.total
this.messages = [...res.rows.reverse(), ...this.messages] //将获取的信息进行倒转,最新的信息显示最下面
}
})
},
// 查询队伍聊天信息
getlist(){
this.$u.get(`/app/chat/list?sessionId=${this.id}&pageNum=${this.pageNum}&pageSize=20`).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 = `${this.$store.state.wsUrl}?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('收到WebSocket消息:', message)
console.log('当前用户ID:', this.userId)
// 检查消息格式确保有chat对象
if (!message.chat) {
console.log('消息格式错误缺少chat对象')
return
}
const chatData = message.chat
const messageSendId = chatData.sendId
console.log('消息发送者ID:', messageSendId)
// 判断是否是自己发送的消息
const isOwnMessage = messageSendId == this.userId
console.log('是否为自己的消息:', isOwnMessage)
if (isOwnMessage) {
// 如果是自己的消息,查找并更新状态为已发送
// 通过内容和类型匹配因为sessionId可能不同
const index = this.messages.findIndex(msg =>
msg.sendId == this.userId &&
msg.content == chatData.content &&
msg.type == chatData.type &&
(msg.status === 'sending' || !msg.status)
)
console.log('找到匹配的消息索引:', index)
if (index !== -1) {
console.log('更新消息状态为已发送')
// 更新消息状态和相关信息
this.messages[index].status = 'sent'
if (this.messages[index].sessionId) {
this.sendingMessages.delete(this.messages[index].sessionId)
}
// 更新服务器返回的完整信息
if (chatData.createTime) {
this.messages[index].createTime = chatData.createTime
}
if (chatData.sessionId) {
this.messages[index].sessionId = chatData.sessionId
}
} else {
console.log('未找到匹配的发送中消息,可能是重复消息,不添加')
}
} else {
// 如果是对方的消息,检查是否已存在(避免重复添加)
const isDuplicate = this.messages.some(msg =>
msg.sendId == messageSendId &&
msg.content == chatData.content &&
msg.type == chatData.type &&
msg.createTime == chatData.createTime
)
if (!isDuplicate) {
this.$nextTick(() => {
// 格式化消息对象,确保结构一致
const formattedMessage = {
sendId: chatData.sendId,
content: chatData.content,
type: chatData.type,
createTime: chatData.createTime || this.$u.timeFormat(new Date(), 'yyyy-mm-dd hh:MM:ss'),
senderAvatar: chatData.senderAvatar || 'https://api.ccttiot.com/smartmeter/img/static/default-avatar',
senderName: chatData.senderName || '用户',
sessionId: chatData.sessionId || Date.now().toString(),
status: 'sent'
}
this.messages.push(formattedMessage)
console.log('添加对方新消息:', formattedMessage)
this.$nextTick(() => {
this.scrollToBottom()
})
})
} else {
console.log('消息已存在,跳过添加')
}
}
})
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.isConnected == false){
uni.showModal({
title: '提示',
content: '当前聊天未连接,请退出重试',
showCancel: false,
success: function(res) {
if (res.confirm) {
} else if (res.cancel) {
}
}
})
return
}
if (this.inputContent.trim() !== '') {
const sessionId = Date.now().toString()
const tempMessage = {
sendId: this.userId,
content: this.inputContent,
type: type,
status: 'sending',
sessionId: sessionId,
senderAvatar: uni.getStorageSync('user').avatar || 'https://api.ccttiot.com/smartmeter/img/static/default-avatar',
createTime: this.$u.timeFormat(new Date(), 'yyyy-mm-dd hh:MM:ss')
}
// 立即将消息添加到聊天列表中
this.messages.push(tempMessage)
this.sendingMessages.add(sessionId)
this.$nextTick(() => {
this.scrollToBottom()
})
let data = {
sessionId: this.id,
content: this.inputContent,
type: type
}
this.$u.post(`/app/chat/sendMessage`, data).then(res => {
if (res.code == 200) {
// 状态更新由WebSocket处理这里不需要重复更新
console.log('消息发送成功')
// 备用机制如果3秒后WebSocket没有更新状态则手动更新
setTimeout(() => {
const index = this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1 && this.messages[index].status === 'sending') {
console.log('WebSocket未响应手动更新状态为已发送')
this.messages[index].status = 'sent'
this.sendingMessages.delete(sessionId)
}
}, 3000)
} else {
const index = this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
this.messages[index].status = 'failed'
}
this.sendingMessages.delete(sessionId)
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(err => {
const index = this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
this.messages[index].status = 'failed'
}
this.sendingMessages.delete(sessionId)
uni.showModal({
title: '提示',
content: '发送失败,请重试',
showCancel: false,
success: function(res) {
if (res.confirm) {
} else if (res.cancel) {
}
}
})
})
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() {
},
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.sessionId === message.sessionId)
if (index !== -1) {
this.messages[index].status = 'sending'
this.sendingMessages.add(message.sessionId)
// 根据消息类型处理重发
if (message.type === 2 || message.type === 3) {
// 图片或视频消息,需要重新上传
this.reuploadMedia(message, index)
} else {
// 文本消息,直接发送
let data = {
sessionId: this.id,
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.sessionId)
} else {
this.messages[index].status = 'failed'
this.sendingMessages.delete(message.sessionId)
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(err => {
this.messages[index].status = 'failed'
this.sendingMessages.delete(message.sessionId)
uni.showToast({
title: '发送失败,请重试',
icon: 'none',
duration: 2000
})
})
}
}
},
// 重新上传媒体文件
reuploadMedia(message, index) {
// 这里需要根据实际情况处理,可能需要用户重新选择文件
uni.showModal({
title: '提示',
content: '媒体文件重发需要重新选择文件,是否重新选择?',
success: (res) => {
if (res.confirm) {
if (message.type === 2) {
this.chooseImage()
} else if (message.type === 3) {
this.takeVideo()
}
// 移除失败的消息
this.messages.splice(index, 1)
this.sendingMessages.delete(message.sessionId)
} else {
// 取消重发,恢复失败状态
this.messages[index].status = 'failed'
this.sendingMessages.delete(message.sessionId)
}
}
})
},
// 获取七牛云token
gettoken(){
this.$u.get(`/common/qiniu/uploadInfo`).then(res => {
if (res.code == 200) {
this.token = res.token
this.picdomain = res.domain
}
})
},
// 显示媒体选择菜单
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
// 立即添加一个发送中的消息
const sessionId = Date.now().toString()
const tempMessage = {
sendId: _this.userId,
content: tempFilePaths[0].path, // 临时路径
type: 2,
status: 'sending',
sessionId: sessionId,
senderAvatar: uni.getStorageSync('user').avatar || 'https://api.ccttiot.com/smartmeter/img/static/default-avatar',
createTime: _this.$u.timeFormat(new Date(), 'yyyy-mm-dd hh:MM:ss')
}
_this.messages.push(tempMessage)
_this.sendingMessages.add(sessionId)
_this.$nextTick(() => {
_this.scrollToBottom()
})
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)
const imageUrl = 'https://api.ccttiot.com/' + str.key
// 更新消息内容为真实URL
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
_this.messages[index].content = imageUrl
}
// 发送消息到服务器
let data = {
sessionId: _this.id,
content: imageUrl,
type: 2
}
_this.$u.post(`/app/chat/sendMessage`, data).then(res => {
if (res.code == 200) {
console.log('图片消息发送成功')
// 备用机制如果3秒后WebSocket没有更新状态则手动更新
setTimeout(() => {
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1 && _this.messages[index].status === 'sending') {
console.log('WebSocket未响应手动更新图片状态为已发送')
_this.messages[index].status = 'sent'
_this.sendingMessages.delete(sessionId)
}
}, 3000)
} else {
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
_this.messages[index].status = 'failed'
}
_this.sendingMessages.delete(sessionId)
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(err => {
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
_this.messages[index].status = 'failed'
}
_this.sendingMessages.delete(sessionId)
uni.showToast({
title: '发送失败,请重试',
icon: 'none',
duration: 2000
})
})
},
fail(err) {
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
_this.messages[index].status = 'failed'
}
_this.sendingMessages.delete(sessionId)
uni.showToast({
title: '上传失败,请重试',
icon: 'none',
duration: 2000
})
}
})
}
})
},
// 拍摄视频
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
// 立即添加一个发送中的消息
const sessionId = Date.now().toString()
const tempMessage = {
sendId: _this.userId,
content: tempFilePath, // 临时路径
type: 3,
status: 'sending',
sessionId: sessionId,
senderAvatar: uni.getStorageSync('user').avatar || 'https://api.ccttiot.com/smartmeter/img/static/default-avatar',
createTime: _this.$u.timeFormat(new Date(), 'yyyy-mm-dd hh:MM:ss')
}
_this.messages.push(tempMessage)
_this.sendingMessages.add(sessionId)
_this.$nextTick(() => {
_this.scrollToBottom()
})
// 显示上传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);
const videoUrl = 'https://api.ccttiot.com/' + responseData.key
// 更新消息内容为真实URL
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
_this.messages[index].content = videoUrl
}
// 视频特殊处理:保存缩略图
if(res.thumbTempFilePath){
_this.uploadThumb(res.thumbTempFilePath)
}
// 发送消息到服务器
let data = {
sessionId: _this.id,
content: videoUrl,
type: 3
}
_this.$u.post(`/app/chat/sendMessage`, data).then(res => {
if (res.code == 200) {
console.log('视频消息发送成功')
// 备用机制如果3秒后WebSocket没有更新状态则手动更新
setTimeout(() => {
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1 && _this.messages[index].status === 'sending') {
console.log('WebSocket未响应手动更新视频状态为已发送')
_this.messages[index].status = 'sent'
_this.sendingMessages.delete(sessionId)
}
}, 3000)
} else {
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
_this.messages[index].status = 'failed'
}
_this.sendingMessages.delete(sessionId)
uni.showToast({
title: res.msg,
icon: 'none',
duration: 2000
})
}
}).catch(err => {
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
_this.messages[index].status = 'failed'
}
_this.sendingMessages.delete(sessionId)
uni.showToast({
title: '发送失败,请重试',
icon: 'none',
duration: 2000
})
})
},
fail(err) {
uni.hideLoading()
const index = _this.messages.findIndex(msg => msg.sessionId === sessionId)
if (index !== -1) {
_this.messages[index].status = 'failed'
}
_this.sendingMessages.delete(sessionId)
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('缩略图上传成功')
}
})
},
parseGift(content) {
try {
return JSON.parse(content)
} catch (e) {
return {}
}
},
getUserNameById(id) {
if (id == this.userId) return '我'
// 这里可以查找群成员列表,返回名字
return id
}
}
}
</script>
<style lang="scss">
page {
background: #fff;
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 - 130rpx - 70rpx);
background-color: #fff;
width: 750rpx;
position: relative;
top: 0;
transition: transform 0.3s;
.message-list {
flex: 1;
padding: 20rpx;
width: 100%;
height: calc(100% - 120rpx);
overflow: hidden;
box-sizing: border-box;
border-top: 1rpx solid rgba(216,216,216,0.8);
.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: #65c578;
color: white;
padding: 15rpx 20rpx;
border-radius: 10rpx;
max-width: 70%;
display: flex;
flex-direction: column;
gap: 8rpx;
.message-header {
display: flex;
align-items: center;
gap: 10rpx;
flex-shrink: 0;
.message-status {
display: flex;
align-items: center;
.status-icon {
width: 24rpx;
height: 24rpx;
&.sending {
animation: rotate 1s linear infinite;
}
&.failed {
opacity: 0.7;
}
}
.status-text {
font-size: 18rpx;
color: #fff;
white-space: nowrap;
}
}
.message-time {
font-size: 18rpx;
color: #fff;
white-space: nowrap;
}
}
.message-text {
word-break: break-all;
line-height: 1.4;
margin-top: 4rpx;
}
}
.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%;
display: flex;
flex-direction: column;
gap: 8rpx;
.message-header {
display: flex;
align-items: center;
gap: 10rpx;
flex-shrink: 0;
.message-time {
font-size: 18rpx;
color: #999;
white-space: nowrap;
}
}
.message-text {
word-break: break-all;
line-height: 1.4;
margin-top: 4rpx;
}
}
.message-image {
max-width: 400rpx;
border-radius: 10rpx;
}
.message-video {
max-width: 400rpx;
border-radius: 10rpx;
}
}
}
}
.bottom-area {
background-color: #fff;
border-top: 1rpx solid #d8d8d8;
.input-container {
display: flex;
padding: 20rpx;
.icon-item {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
image {
width: 100%;
height: 100%;
}
}
.input-box {
flex: 1;
height: 80rpx;
background-color: #F4F4F4;
color: #333;
padding: 0 20rpx;
border-radius: 40rpx;
font-size: 28rpx;
}
.send-button {
margin-left: 20rpx;
background-color: #48893B;
color: white;
border: none;
border-radius: 40rpx;
padding: 0 30rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
}
}
}
}
.tablexx{
width: 730rpx;
height: 266rpx;
background: #fff;
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 #d8d8d8;
}
.avatar-name {
text-align: center;
font-size: 20rpx;
color: #bbb;
margin-top: 6rpx;
width: 60rpx;
word-break: break-all;
margin-left: 15rpx;
}
.gift-bubble {
display: flex;
align-items: center;
background: linear-gradient(90deg, #ffe0e6 0%, #fff7f0 100%);
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(255,137,152,0.10);
padding: 16rpx 20rpx;
margin: 8rpx 0;
max-width: 70vw;
.gift-image {
width: 60rpx;
height: 90rpx;
border-radius: 12rpx;
margin-right: 16rpx;
border: 2rpx solid #ff8998;
background: #fff;
}
.gift-info {
display: flex;
flex-direction: column;
.gift-title {
font-size: 24rpx;
font-weight: bold;
color: #ff8998;
margin-bottom: 4rpx;
}
.gift-desc {
font-size: 20rpx;
color: #666;
margin-bottom: 4rpx;
}
.gift-fromto {
font-size: 18rpx;
color: #999;
.gift-donee {
color: #4d3f53;
font-weight: bold;
}
}
}
}
</style>