Files
urbanLifeline/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue

808 lines
27 KiB
Plaintext
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>
<!-- #ifdef APP -->
<scroll-view style="flex:1">
<!-- #endif -->
<view class="page">
<!-- 自定义导航栏 -->
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<view class="nav-back" @tap="goBack">
<view class="nav-back-icon"></view>
</view>
<text class="nav-title">{{ roomName }}</text>
</view>
<!-- 聊天室人员和操作 -->
<view class="room-toolbar" :style="{ top: headerPaddingTop + 44 + 'px' }">
<view class="member-count" @tap="showMembers = !showMembers">
<text class="member-count-text">{{ totalMembers.length > 0 ? totalMembers.length + ' 人在线' : '暂无人员' }}</text>
</view>
<view class="toolbar-right">
<button class="toolbar-btn" @tap="handleWorkcaseAction">
<text class="toolbar-btn-text">{{ workcaseId ? '查看工单' : '创建工单' }}</text>
</button>
<button class="toolbar-btn meeting-btn" @tap="startMeeting">
<text class="toolbar-btn-text meeting-text">发起会议</text>
</button>
</view>
</view>
<!-- 弹窗显示人员列表和在线情况 -->
<view v-if="showMembers" class="members-popup-mask" @tap="showMembers = false">
<view class="members-popup" :style="{ top: headerPaddingTop + 88 + 'px' }" @tap.stop>
<view class="members-list">
<view v-if="totalMembers.length === 0" class="members-empty">
<text class="members-empty-text">暂无人员在线</text>
</view>
<view v-else class="member-item" v-for="member in totalMembers" :key="member.oderId">
<view class="member-avatar">
<text class="member-avatar-text">{{ member.userName?.charAt(0) || '客' }}</text>
</view>
<text class="member-name">{{ member.userName || '未知' }}</text>
<view class="member-status" :class="member.isOnline ? 'online' : 'offline'"></view>
</view>
</view>
</view>
</view>
<!-- 聊天消息区域 -->
<scroll-view class="chat-area" scroll-y="true" :scroll-top="scrollTop"
:scroll-with-animation="false"
:style="{ top: (headerPaddingTop + 88) + 'px' }"
@scrolltoupper="loadMoreMessages"
upper-threshold="50">
<!-- 加载更多提示 -->
<view v-if="loadingMore" class="loading-more">
<text class="loading-more-text">加载中...</text>
</view>
<view v-else-if="!hasMore" class="loading-more">
<text class="loading-more-text">没有更多消息了</text>
</view>
<view class="message-list">
<view class="message-item" v-for="msg in messages" :key="msg.messageId">
<!-- 系统消息(居中显示) -->
<view class="message-row system-row" v-if="msg.senderType === 'system'">
<view class="system-message-container">
<!-- 评分消息卡片 -->
<template v-if="msg.messageType === 'comment'">
<CommentMessageCard
:room-id="roomId"
:can-comment="getCanComment()"
:initial-rating="commentLevel"
@submit="handleCommentSubmit"
/>
</template>
<!-- 其他系统消息 -->
<template v-else>
<view class="system-message-text">
<text>{{ msg.content }}</text>
</view>
</template>
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
</view>
</view>
<!-- 普通用户/客服消息 -->
<view v-else :class="msg.senderType === 'guest' ? 'self' : 'other'">
<!-- 对方消息(左侧) -->
<view class="message-row other-row" v-if="msg.senderType !== 'guest'">
<view>
<view class="avatar">
<text class="avatar-text">{{ msg.senderName?.charAt(0) || '客' }}</text>
</view>
<text class="sender-name">{{ msg.senderName || '客服' }}</text>
</view>
<!-- 会议消息卡片 -->
<view class="message-content meeting-card-wrapper" v-if="msg.messageType === 'meet' && msg.contentExtra">
<MeetingCard :meetingId="msg.content" @join="handleJoinMeeting" />
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
</view>
<!-- 普通消息 -->
<view class="message-content" v-else>
<view class="bubble other-bubble">
<rich-text :nodes="renderMarkdown(msg.content || '')" class="message-rich-text"></rich-text>
</view>
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
</view>
</view>
<!-- 自己消息(右侧) -->
<view class="message-row self-row" v-else>
<!-- 会议消息卡片 -->
<view class="message-content meeting-card-wrapper" v-if="msg.messageType === 'meet' && msg.contentExtra">
<MeetingCard :meetingId="msg.content" @join="handleJoinMeeting" />
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
</view>
<!-- 普通消息 -->
<view class="message-content" v-else>
<view class="bubble self-bubble">
<text class="message-text">{{ msg.content }}</text>
</view>
<text class="message-time">{{ formatTime(msg.sendTime) }}</text>
</view>
<view class="avatar self-avatar">
<text class="avatar-text">我</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部输入区 -->
<view class="footer">
<view class="input-row">
<input class="chat-input" v-model="inputText" placeholder="输入消息..."
@confirm="sendMessage" />
<view class="send-btn" @tap="sendMessage">
<text class="send-icon">➤</text>
</view>
</view>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import MeetingCard from '../../meeting/meetingCard/MeetingCard.uvue'
import CommentMessageCard from './CommentMessageCard/CommentMessageCard.uvue'
import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO, VideoMeetingVO } from '@/types/workcase'
import { workcaseChatAPI } from '@/api/workcase'
import { wsClient } from '@/utils/websocket'
import { WS_HOST } from '@/config'
// 响应式数据
const headerPaddingTop = ref<number>(44)
const headerTotalHeight = ref<number>(88)
const roomId = ref<string>('')
const workcaseId = ref<string>('')
const roomName = ref<string>('聊天室')
const guestId = ref<string>('') // 聊天室访客ID
const commentLevel = ref<number>(0) // 已有评分
const inputText = ref<string>('')
const scrollTop = ref<number>(0)
const loading = ref<boolean>(false)
const sending = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
const currentPage = ref<number>(1)
const hasMore = ref<boolean>(true)
// 用户信息从storage获取
const currentUserId = ref<string>('')
const currentUserName = ref<string>('我')
function loadUserInfo() {
try {
const userInfo = uni.getStorageSync('userInfo')
if (userInfo) {
const user = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
currentUserId.value = user.userId || user.id || ''
currentUserName.value = user.username || user.nickName || '我'
}
} catch (e) {
console.error('获取用户信息失败:', e)
}
}
// 消息列表
const messages = reactive<ChatRoomMessageVO[]>([])
// 加载遮罩(已禁用)
// const showLoadingMask = ref<boolean>(true)
// 所有默认客服
const defaultWorkers = reactive<CustomerVO[]>([])
async function loadDefaultWorkers() {
const res = await workcaseChatAPI.getAvailableCustomerServices()
if(res.success && res.dataList) {
defaultWorkers.splice(0, defaultWorkers.length, ...res.dataList)
}
}
// 查询进入聊天室的人员, 包含了访客
const chatMembers = reactive<ChatMemberVO[]>([])
async function loadChatMembers() {
const res = await workcaseChatAPI.getChatRoomMemberList(roomId.value)
if(res.success && res.dataList) {
chatMembers.splice(0, chatMembers.length, ...res.dataList)
}
}
const showMembers = ref(false)
// 计算聊天室人数: 默认客服转成ChatMemberVO + 进入聊天室的人员,去重,进入聊天室的人员是在线状态
interface MemberDisplay {
oderId: string
userId: string
userName: string
isOnline: boolean
}
const totalMembers = computed<MemberDisplay[]>(() => {
const memberMap = new Map<string, MemberDisplay>()
// 先添加默认客服(离线状态)
defaultWorkers.forEach((worker, index) => {
memberMap.set(worker.userId || '', {
oderId: `worker-${index}`,
userId: worker.userId || '',
userName: worker.username || '客服',
isOnline: false
})
})
// 再添加聊天室成员(在线状态),覆盖同一用户
chatMembers.forEach((member, index) => {
memberMap.set(member.userId || '', {
oderId: member.memberId || `member-${index}`,
userId: member.userId || '',
userName: member.userName || '未知',
isOnline: true
})
})
return Array.from(memberMap.values())
})
function getCanComment(): boolean {
return currentUserId.value === guestId.value
}
// 生命周期
onMounted(() => {
const windowInfo = uni.getWindowInfo()
const statusBarHeight = windowInfo.statusBarHeight || 44
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = statusBarHeight
headerTotalHeight.value = statusBarHeight + 44
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = statusBarHeight
headerTotalHeight.value = statusBarHeight + 44
// #endif
// 获取页面参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] as any
if (currentPage && currentPage.options) {
roomId.value = currentPage.options.roomId || ''
workcaseId.value = currentPage.options.workcaseId || ''
}
loadUserInfo()
loadChatRoom()
loadDefaultWorkers()
loadChatMembers()
initWebSocket()
})
// 页面显示时重新查询聊天室信息(从工单页返回时会自动刷新)
onShow(() => {
refreshChatRoomInfo()
})
// 组件卸载时断开WebSocket
onUnmounted(() => {
disconnectWebSocket()
})
// 监听roomId变化切换聊天室时重新订阅
watch(roomId, (newRoomId, oldRoomId) => {
if (oldRoomId && newRoomId !== oldRoomId) {
// 取消旧聊天室订阅
wsClient.unsubscribe(`/topic/chat/${oldRoomId}`)
}
if (newRoomId && wsClient.isConnected()) {
// 订阅新聊天室
wsClient.subscribe(`/topic/chat/${newRoomId}`, handleNewMessage)
}
})
// 加载聊天室
const PAGE_SIZE = 20
const messageTotal = ref<number>(0)
// 刷新聊天室信息(仅更新 workcaseId 等基本信息,不重新加载消息)
async function refreshChatRoomInfo() {
if (!roomId.value) return
try {
const roomRes = await workcaseChatAPI.getChatRoomById(roomId.value)
if (roomRes.success && roomRes.data) {
roomName.value = roomRes.data.roomName || '聊天室'
workcaseId.value = roomRes.data.workcaseId || ''
guestId.value = roomRes.data.guestId || ''
commentLevel.value = roomRes.data.commentLevel || 0
messageTotal.value = roomRes.data.messageCount || 0
}
} catch (e) {
console.error('刷新聊天室信息失败:', e)
}
}
async function loadChatRoom() {
if (!roomId.value) return
loading.value = true
try {
// 自动加入聊天室成员表(如果不存在)
try {
const userId = uni.getStorageSync('userId') || currentUserId.value
const userName = uni.getStorageSync('userName') || currentUserName.value
await workcaseChatAPI.addChatRoomMember({
roomId: roomId.value,
userId: userId,
userName: userName,
userType: 'guest'
})
} catch (error) {
// 已存在成员或其他错误,忽略
console.debug('[chatRoom] 加入聊天室成员表:', error)
}
// 获取聊天室信息
const roomRes = await workcaseChatAPI.getChatRoomById(roomId.value)
if (roomRes.success && roomRes.data) {
roomName.value = roomRes.data.roomName || '聊天室'
workcaseId.value = roomRes.data.workcaseId || ''
guestId.value = roomRes.data.guestId || ''
messageTotal.value = roomRes.data.messageCount || 0
commentLevel.value = roomRes.data.commentLevel!
}
// 后端是降序查询page1是最新消息
currentPage.value = 1
hasMore.value = true // 默认有更多loadMessages中会根据实际情况更新
// 获取消息列表
await loadMessages()
} catch (e) {
console.error('加载聊天室失败:', e)
} finally {
loading.value = false
}
}
// 加载消息列表后端降序page1是最新消息需要反转显示
async function loadMessages() {
console.log('[loadMessages] 开始加载, currentPage:', currentPage.value, 'roomId:', roomId.value)
if (!roomId.value) return
try {
const msgRes = await workcaseChatAPI.getChatMessagePage({
filter: { roomId: roomId.value },
pageParam: { page: currentPage.value, pageSize: PAGE_SIZE }
})
console.log('[loadMessages] 响应:', msgRes)
if (msgRes.success && msgRes.dataList) {
const pageInfo = msgRes.pageDomain?.pageParam
const actualTotalPages = pageInfo?.totalPages || 1
hasMore.value = actualTotalPages > currentPage.value
console.log('[loadMessages] pageInfo:', pageInfo, 'actualTotalPages:', actualTotalPages, 'hasMore:', hasMore.value)
// 后端降序返回,需要反转后显示(早的在上,新的在下)
const reversedList = [...msgRes.dataList].reverse()
messages.splice(0, messages.length, ...reversedList)
console.log('[loadMessages] 加载完成, 消息数:', messages.length)
// 加载完第一页后滚动到底部,需要等待 DOM 完全渲染
if (currentPage.value === 1) {
// 使用 setTimeout 确保 DOM 完全渲染后再滚动
nextTick(() => {
setTimeout(() => {
scrollToBottom()
}, 300)
})
} else {
nextTick(() => scrollToBottom())
}
}
} catch (e) {
console.error('加载消息列表失败:', e)
}
}
// 加载更多历史消息(滚动到顶部触发,加载下一页更早的消息)
async function loadMoreMessages() {
console.log('[loadMoreMessages] 触发, roomId:', roomId.value, 'loadingMore:', loadingMore.value, 'hasMore:', hasMore.value, 'currentPage:', currentPage.value)
if (!roomId.value || loadingMore.value || !hasMore.value) {
console.log('[loadMoreMessages] 跳过加载 - roomId:', !roomId.value, 'loadingMore:', loadingMore.value, '!hasMore:', !hasMore.value)
return
}
// 加载下一页(更早的消息)
const nextPage = currentPage.value + 1
console.log('[loadMoreMessages] 准备加载页:', nextPage)
loadingMore.value = true
try {
const msgRes = await workcaseChatAPI.getChatMessagePage({
filter: { roomId: roomId.value },
pageParam: { page: nextPage, pageSize: PAGE_SIZE }
})
console.log('[loadMoreMessages] 响应:', msgRes)
if (msgRes.success && msgRes.dataList && msgRes.dataList.length > 0) {
const pageInfo = msgRes.pageDomain?.pageParam
const actualTotalPages = pageInfo?.totalPages || 1
hasMore.value = actualTotalPages > currentPage.value
console.log('[loadMoreMessages] pageInfo:', pageInfo, 'actualTotalPages:', actualTotalPages, 'hasMore:', hasMore.value)
currentPage.value = nextPage
// 后端降序返回,反转后插入到列表前面
const reversedList = [...msgRes.dataList].reverse()
messages.unshift(...reversedList)
console.log('[loadMoreMessages] 加载完成, 消息数:', messages.length)
} else {
console.log('[loadMoreMessages] 没有更多数据')
hasMore.value = false
}
} catch (e) {
console.error('加载更多消息失败:', e)
} finally {
loadingMore.value = false
}
}
// 格式化时间(兼容 iOS
function formatTime(time?: string): string {
if (!time) return ''
// iOS 不支持 "yyyy-MM-dd HH:mm:ss" 格式,需要转换为 "yyyy-MM-ddTHH:mm:ss" 或 "yyyy/MM/dd HH:mm:ss"
const iosCompatibleTime = time.replace(' ', 'T')
const date = new Date(iosCompatibleTime)
if (isNaN(date.getTime())) return ''
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 获取会议数据将contentExtra转换为VideoMeetingVO
// 从消息extra中提取meetingId
function getMeetingId(contentExtra: Record<string, any> | undefined): string {
if (!contentExtra || !contentExtra.meetingId) {
console.warn('[chatRoom] contentExtra中没有meetingId:', contentExtra)
return ''
}
return contentExtra.meetingId as string
}
// Markdown渲染函数返回富文本HTML
function renderMarkdown(text: string): string {
if (!text) return ''
// 转义HTML特殊字符
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 处理粗体(**语法)
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>')
// 处理斜体(*语法,但要避免和粗体冲突)
html = html.replace(/(?<!\*)\*([^\*]+)\*(?!\*)/g, '<em>$1</em>')
// 处理行内代码(`语法)
html = html.replace(/`([^`]+)`/g, '<code style="background-color:#f5f5f5;padding:2px 6px;border-radius:3px;font-family:monospace;color:#e53e3e;word-break:break-all;">$1</code>')
// 处理链接([text](url)语法)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#0055AA;text-decoration:underline;word-break:break-all;">$1</a>')
// 处理标题(# ## ###等)
html = html.replace(/^### (.+)$/gm, '<div style="font-size:16px;font-weight:600;margin:8px 0 4px;">$1</div>')
html = html.replace(/^## (.+)$/gm, '<div style="font-size:18px;font-weight:600;margin:10px 0 6px;">$1</div>')
html = html.replace(/^# (.+)$/gm, '<div style="font-size:20px;font-weight:700;margin:12px 0 8px;">$1</div>')
// 处理无序列表(- 或 * 开头)
html = html.replace(/^[*-] (.+)$/gm, '<div style="margin-left:16px;">• $1</div>')
// 处理换行
html = html.replace(/\n/g, '<br/>')
// 包裹在带有换行样式的容器中
return `<div style="word-break:break-all;overflow-wrap:break-word;white-space:pre-wrap;">${html}</div>`
}
// 发送消息
async function sendMessage() {
const text = inputText.value.trim()
if (!text || sending.value) return
sending.value = true
const tempId = Date.now().toString()
// 先添加临时消息到界面
const tempMsg: ChatRoomMessageVO = {
messageId: tempId,
roomId: roomId.value,
senderId: currentUserId.value,
senderType: 'guest',
senderName: currentUserName.value,
content: text,
sendTime: new Date().toISOString(),
status: 'sending'
}
messages.push(tempMsg)
inputText.value = ''
nextTick(() => scrollToBottom())
try {
// 调用API发送消息
const msgDTO: TbChatRoomMessageDTO = {
roomId: roomId.value,
senderId: currentUserId.value,
senderType: 'guest',
senderName: currentUserName.value,
messageType: 'text',
content: text
}
const res = await workcaseChatAPI.sendMessage(msgDTO)
if (res.success && res.data) {
// 更新临时消息为真实消息
const idx = messages.findIndex(m => m.messageId === tempId)
if (idx !== -1) {
messages[idx] = { ...res.data, status: 'sent' }
}
} else {
// 发送失败,标记状态
const idx = messages.findIndex(m => m.messageId === tempId)
if (idx !== -1) {
messages[idx].status = 'failed'
}
uni.showToast({ title: res.message || '发送失败', icon: 'none' })
}
} catch (e) {
console.error('发送消息失败:', e)
const idx = messages.findIndex(m => m.messageId === tempId)
if (idx !== -1) {
messages[idx].status = 'failed'
}
uni.showToast({ title: '发送失败', icon: 'none' })
} finally {
sending.value = false
}
}
// 滚动到底部
function scrollToBottom() {
// uni-app 的 scroll-view 需要 scroll-top 值发生变化才会触发滚动
// 先重置为 0
scrollTop.value = 0
// 使用 setTimeout 确保重置生效
setTimeout(() => {
// 使用一个足够大的值确保滚动到底部
scrollTop.value = 999999
}, 50)
}
// 处理工单操作
function handleWorkcaseAction() {
console.log('[handleWorkcaseAction] 开始执行')
console.log('[handleWorkcaseAction] workcaseId:', workcaseId.value)
console.log('[handleWorkcaseAction] roomId:', roomId.value)
if (workcaseId.value) {
const url = `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId.value}`
console.log('[handleWorkcaseAction] 查看工单跳转URL:', url)
uni.navigateTo({
url: url,
success: () => {
console.log('[handleWorkcaseAction] 跳转成功')
},
fail: (err) => {
console.error('[handleWorkcaseAction] 跳转失败:', err)
}
})
} else {
// 跳转到创建工单页面
const url = `/pages/workcase/workcaseDetail/workcaseDetail?mode=create&roomId=${roomId.value}`
console.log('[handleWorkcaseAction] 创建工单跳转URL:', url)
uni.navigateTo({
url: url,
success: () => {
console.log('[handleWorkcaseAction] 跳转成功')
},
fail: (err) => {
console.error('[handleWorkcaseAction] 跳转失败:', err)
}
})
}
}
// 发起会议 - 跳转到会议创建页面
function startMeeting() {
// 跳转到会议创建页面
const url = `/pages/meeting/meetingCreate/MeetingCreate?roomId=${roomId.value}${workcaseId.value ? '&workcaseId=' + workcaseId.value : ''}`
console.log('[chatRoom] 跳转会议创建页面:', url)
uni.navigateTo({
url: url,
fail: (err) => {
console.error('[chatRoom] 跳转会议创建页面失败:', err)
uni.showToast({
title: '跳转失败',
icon: 'none'
})
}
})
}
// 加入会议从MeetingCard点击加入
async function handleJoinMeeting(meetingId: string) {
console.log('[handleJoinMeeting] 开始加入会议, meetingId:', meetingId)
try {
// 调用加入会议接口获取会议页面URL
const joinRes = await workcaseChatAPI.joinMeeting(meetingId)
console.log('[handleJoinMeeting] API响应:', JSON.stringify(joinRes))
// 兼容两种判断方式success 字段或 code === 200
const isSuccess = joinRes.success || joinRes.code === 200 || joinRes.code === 0
const meetingData = joinRes.data
if (isSuccess && meetingData && meetingData.iframeUrl) {
const meetingPageUrl = meetingData.iframeUrl
const meetingName = meetingData.meetingName || '视频会议'
console.log('[handleJoinMeeting] 获取到会议页面URL:', meetingPageUrl, '会议名称:', meetingName)
// 小程序环境直接使用固定的HTTPS域名
const protocol = 'https:'
const host = 'org.xyzh.yslg'
// 如果meetingPageUrl不包含/workcase需要加上
const fullPath = meetingPageUrl.startsWith('/workcase')
? meetingPageUrl
: '/workcase' + meetingPageUrl
// 附加roomId参数用于离开会议后返回聊天室
const fullMeetingUrl = `${protocol}//${host}${fullPath}&roomId=${roomId.value}`
console.log('[handleJoinMeeting] 完整会议URL:', fullMeetingUrl)
// 小程序环境:显示提示,引导用户复制链接在浏览器打开
uni.showModal({
title: '视频会议',
content: '微信小程序暂不支持视频会议,请复制链接在浏览器中打开',
confirmText: '复制链接',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 复制链接到剪贴板
uni.setClipboardData({
data: fullMeetingUrl,
success: () => {
uni.showToast({
title: '链接已复制,请在浏览器中打开',
icon: 'none',
duration: 3000
})
},
fail: () => {
uni.showToast({
title: '复制失败,请手动复制',
icon: 'none'
})
}
})
}
}
})
} else {
console.error('[handleJoinMeeting] 加入会议失败, isSuccess:', isSuccess, 'data:', meetingData)
uni.showToast({
title: joinRes.message || '加入会议失败:未获取到会议地址',
icon: 'none'
})
}
} catch (error) {
console.error('[handleJoinMeeting] 加入会议异常:', error)
uni.showToast({
title: '加入会议失败',
icon: 'none'
})
}
}
// 处理评分提交
async function handleCommentSubmit(rating: number) {
console.log('[handleCommentSubmit] 提交评分:', rating)
try {
const result = await workcaseChatAPI.submitComment(roomId.value, rating)
if (result.success) {
uni.showToast({
title: '感谢您的评分!',
icon: 'success'
})
} else {
uni.showToast({
title: result.message || '评分提交失败',
icon: 'none'
})
}
} catch (error) {
console.error('[handleCommentSubmit] 评分提交失败:', error)
uni.showToast({
title: '评分提交失败,请稍后重试',
icon: 'none'
})
}
}
// 返回上一页
function goBack() {
uni.navigateBack()
}
// ==================== WebSocket连接管理 ====================
// 初始化WebSocket连接
async function initWebSocket() {
try {
const token = uni.getStorageSync('token') || ''
if (!token) {
console.warn('[chatRoom] 未找到token跳过WebSocket连接')
return
}
// 构建WebSocket URL
// 开发环境ws://localhost:8180 或 ws://192.168.x.x:8180
// 生产环境wss://your-domain.com
const protocol = 'ws:' // 开发环境使用ws生产环境改为wss
// 小程序使用原生WebSocket端点不是SockJS端点
const wsUrl = `${protocol}//${WS_HOST}/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}`
console.log('[chatRoom] 开始连接WebSocket')
await wsClient.connect(wsUrl, token)
// 订阅当前聊天室消息频道
if (roomId.value) {
wsClient.subscribe(`/topic/chat/${roomId.value}`, handleNewMessage)
console.log('[chatRoom] WebSocket连接成功已订阅聊天室:', roomId.value)
}
} catch (error) {
console.error('[chatRoom] WebSocket连接失败:', error)
}
}
// 断开WebSocket连接
function disconnectWebSocket() {
try {
if (roomId.value) {
wsClient.unsubscribe(`/topic/chat/${roomId.value}`)
}
wsClient.disconnect()
console.log('[chatRoom] WebSocket已断开')
} catch (error) {
console.error('[chatRoom] 断开WebSocket失败:', error)
}
}
// 处理接收到的新消息
function handleNewMessage(message: ChatRoomMessageVO) {
console.log('[chatRoom] 收到新消息:', message)
// 避免重复添加自己发送的普通消息自己发送的消息已经通过sendMessage添加到列表
// 但会议消息meet类型始终添加因为它是系统生成的通知
if (message.messageType !== 'meet' && message.senderId === currentUserId.value) {
console.log('[chatRoom] 跳过自己发送的普通消息')
return
}
// 检查消息是否已存在(避免重复)
const exists = messages.some(m => m.messageId === message.messageId)
if (exists) {
console.log('[chatRoom] 消息已存在,跳过')
return
}
// 会议消息延时处理,等待数据库事务提交
if (message.messageType === 'meet') {
console.log('[chatRoom] 收到会议消息延时1秒后刷新')
setTimeout(async () => {
// 重新加载最新消息,确保获取到完整的会议消息数据
await loadMessages()
}, 1000)
return
}
// 添加新消息到列表
messages.push(message)
nextTick(() => scrollToBottom())
// 可以添加消息提示音或震动
// uni.vibrateShort()
}
</script>
<style lang="scss" scoped>
@import "./chatRoom.scss";
</style>