808 lines
27 KiB
Plaintext
808 lines
27 KiB
Plaintext
<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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
|
||
// 处理粗体(**语法)
|
||
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> |