2025-12-20 18:52:33 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="chat-room-container">
|
2025-12-23 19:15:00 +08:00
|
|
|
|
<!-- 折叠状态的侧边栏 -->
|
|
|
|
|
|
<div v-if="!isSidebarOpen" class="sidebar-collapsed">
|
|
|
|
|
|
<button class="sidebar-toggle-btn" @click="toggleSidebar" title="展开聊天室列表">
|
|
|
|
|
|
<MessageCircle :size="20" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="sidebar-icons">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="room in filteredRooms.slice(0, 8)"
|
|
|
|
|
|
:key="room.roomId"
|
|
|
|
|
|
@click="selectRoom(room.roomId!); toggleSidebar()"
|
|
|
|
|
|
class="sidebar-icon-btn"
|
|
|
|
|
|
:class="{ active: currentRoomId === room.roomId }"
|
|
|
|
|
|
:title="room.roomName"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ room.guestName?.substring(0, 1) || '?' }}
|
|
|
|
|
|
</button>
|
2025-12-20 18:52:33 +08:00
|
|
|
|
</div>
|
2025-12-23 19:15:00 +08:00
|
|
|
|
<button class="expand-btn" @click="toggleSidebar" title="展开聊天室列表">
|
|
|
|
|
|
<ChevronRight :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-20 18:52:33 +08:00
|
|
|
|
|
2025-12-23 19:15:00 +08:00
|
|
|
|
<!-- 展开时的关闭按钮 -->
|
|
|
|
|
|
<button v-if="isSidebarOpen" class="sidebar-close-btn" @click="toggleSidebar">
|
|
|
|
|
|
<ChevronLeft :size="18" />
|
|
|
|
|
|
</button>
|
2025-12-20 18:52:33 +08:00
|
|
|
|
|
2025-12-23 19:15:00 +08:00
|
|
|
|
<!-- 聊天室列表侧边栏 -->
|
|
|
|
|
|
<aside class="room-list-sidebar" :class="{ open: isSidebarOpen }">
|
|
|
|
|
|
<div class="sidebar-inner">
|
|
|
|
|
|
<!-- 头部 -->
|
|
|
|
|
|
<div class="sidebar-header">
|
|
|
|
|
|
<span class="title">聊天室</span>
|
2025-12-20 18:52:33 +08:00
|
|
|
|
</div>
|
2025-12-23 19:15:00 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 搜索框 -->
|
|
|
|
|
|
<div class="search-box">
|
|
|
|
|
|
<ElInput
|
|
|
|
|
|
v-model="searchText"
|
|
|
|
|
|
placeholder="搜索工单号、来客姓名、电话..."
|
|
|
|
|
|
:prefix-icon="Search"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- chatRoom列表 -->
|
|
|
|
|
|
<div class="room-list-container">
|
|
|
|
|
|
<div v-if="filteredRooms.length === 0" class="empty-tip">
|
|
|
|
|
|
暂无聊天室
|
2025-12-20 18:52:33 +08:00
|
|
|
|
</div>
|
2025-12-23 19:15:00 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="room in filteredRooms"
|
|
|
|
|
|
:key="room.roomId"
|
|
|
|
|
|
class="room-item"
|
|
|
|
|
|
:class="{ active: currentRoomId === room.roomId }"
|
|
|
|
|
|
@click="selectRoom(room.roomId!)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 头像 -->
|
|
|
|
|
|
<div class="room-avatar">
|
|
|
|
|
|
{{ room.guestName?.substring(0, 1) || '?' }}
|
|
|
|
|
|
</div>
|
2025-12-20 18:52:33 +08:00
|
|
|
|
|
2025-12-23 19:15:00 +08:00
|
|
|
|
<!-- 信息 -->
|
|
|
|
|
|
<div class="room-info">
|
|
|
|
|
|
<div class="room-header">
|
|
|
|
|
|
<div class="room-name">{{ room.roomName }}</div>
|
|
|
|
|
|
<div class="room-time">{{ formatTime(room.lastMessageTime) }}</div>
|
|
|
|
|
|
</div>
|
2025-12-24 15:02:23 +08:00
|
|
|
|
<div class="last-message-row">
|
|
|
|
|
|
<div class="last-message">{{ room.lastMessage || '暂无消息' }}</div>
|
|
|
|
|
|
<!-- 未读红点 -->
|
|
|
|
|
|
<div v-if="(room.unreadCount ?? 0) > 0" class="unread-badge">
|
|
|
|
|
|
{{ (room.unreadCount ?? 0) > 99 ? '99+' : room.unreadCount }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-20 18:52:33 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 主聊天区域 -->
|
2025-12-23 19:15:00 +08:00
|
|
|
|
<main class="chat-main" :class="{ 'sidebar-open': isSidebarOpen }">
|
2025-12-20 18:52:33 +08:00
|
|
|
|
<template v-if="currentRoomId">
|
|
|
|
|
|
<ChatRoom
|
2025-12-23 19:15:00 +08:00
|
|
|
|
ref="chatRoomRef"
|
2025-12-20 18:52:33 +08:00
|
|
|
|
:messages="messages"
|
2025-12-24 15:02:23 +08:00
|
|
|
|
:current-user-id="loginDomain.user.userId"
|
2025-12-20 18:52:33 +08:00
|
|
|
|
:room-name="currentRoom?.roomName"
|
|
|
|
|
|
:meeting-url="currentMeetingUrl"
|
|
|
|
|
|
:show-meeting="showMeetingIframe"
|
|
|
|
|
|
:file-download-url="FILE_DOWNLOAD_URL"
|
2025-12-23 19:15:00 +08:00
|
|
|
|
:has-more="hasMore"
|
|
|
|
|
|
:loading-more="loadingMore"
|
2025-12-20 18:52:33 +08:00
|
|
|
|
@send-message="handleSendMessage"
|
|
|
|
|
|
@start-meeting="startMeeting"
|
|
|
|
|
|
@download-file="downloadFile"
|
2025-12-23 19:15:00 +08:00
|
|
|
|
@load-more="loadMoreMessages"
|
2025-12-20 18:52:33 +08:00
|
|
|
|
>
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="chat-room-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<div class="room-avatar-small">
|
|
|
|
|
|
{{ currentRoom?.guestName?.substring(0, 1) }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="room-title-group">
|
|
|
|
|
|
<div class="room-name-text">{{ currentRoom?.roomName }}</div>
|
|
|
|
|
|
<div class="room-subtitle">
|
|
|
|
|
|
工单 #{{ currentRoom?.workcaseId }} · {{ currentRoom?.guestName }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #action-area>
|
|
|
|
|
|
<ElButton type="primary" @click="showWorkcaseDetail = true">
|
|
|
|
|
|
<FileText :size="16" />
|
|
|
|
|
|
查看工单
|
|
|
|
|
|
</ElButton>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ChatRoom>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 空状态 -->
|
|
|
|
|
|
<div v-else class="empty-state">
|
|
|
|
|
|
<div class="empty-content">
|
|
|
|
|
|
<div class="empty-icon">
|
|
|
|
|
|
<MessageSquare :size="40" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h3 class="empty-title">选择一个聊天室开始对话</h3>
|
|
|
|
|
|
<p class="empty-desc">从左侧列表中选择一个聊天室查看消息</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 工单详情对话框 -->
|
|
|
|
|
|
<ElDialog
|
|
|
|
|
|
v-model="showWorkcaseDetail"
|
|
|
|
|
|
title="工单详情"
|
|
|
|
|
|
width="800px"
|
|
|
|
|
|
class="workcase-dialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
<WorkcaseDetail :workcase-id="currentWorkcaseId" />
|
|
|
|
|
|
</ElDialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
2025-12-23 19:15:00 +08:00
|
|
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
2025-12-22 17:03:37 +08:00
|
|
|
|
import { ElButton, ElInput, ElDialog, ElMessage } from 'element-plus'
|
2025-12-23 19:15:00 +08:00
|
|
|
|
import { Search, FileText, MessageSquare, MessageCircle, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
2025-12-23 16:16:47 +08:00
|
|
|
|
import { ChatRoom } from '@/components/chatRoom'
|
2025-12-20 18:52:33 +08:00
|
|
|
|
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
|
2025-12-23 16:16:47 +08:00
|
|
|
|
import { workcaseChatAPI } from '@/api/workcase'
|
2025-12-22 17:03:37 +08:00
|
|
|
|
import { fileAPI } from 'shared/api/file'
|
2025-12-20 18:52:33 +08:00
|
|
|
|
import { FILE_DOWNLOAD_URL } from '@/config'
|
2025-12-24 15:02:23 +08:00
|
|
|
|
import type { ChatRoomVO, ChatRoomMessageVO, TbChatRoomMessageDTO, TbChatRoomMemberDTO } from '@/types/workcase'
|
2025-12-22 17:03:37 +08:00
|
|
|
|
import SockJS from 'sockjs-client'
|
|
|
|
|
|
import { Client } from '@stomp/stompjs'
|
2025-12-20 18:52:33 +08:00
|
|
|
|
|
2025-12-22 17:08:49 +08:00
|
|
|
|
// WebSocket配置 (通过Nginx代理访问网关,再到workcase服务)
|
|
|
|
|
|
// SockJS URL (http://)
|
2025-12-22 17:03:37 +08:00
|
|
|
|
const getWsUrl = () => {
|
|
|
|
|
|
const token = localStorage.getItem('token') || ''
|
2025-12-22 17:08:49 +08:00
|
|
|
|
const protocol = window.location.protocol
|
|
|
|
|
|
const host = window.location.host
|
|
|
|
|
|
return `${protocol}//${host}/api/urban-lifeline/workcase/ws/chat-sockjs?token=${encodeURIComponent(token)}`
|
2025-12-20 18:52:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 17:03:37 +08:00
|
|
|
|
// STOMP客户端
|
|
|
|
|
|
let stompClient: any = null
|
|
|
|
|
|
let roomSubscription: any = null
|
|
|
|
|
|
let listSubscription: any = null
|
2025-12-20 18:52:33 +08:00
|
|
|
|
|
2025-12-22 17:03:37 +08:00
|
|
|
|
// 当前用户ID(从登录状态获取)
|
2025-12-24 15:02:23 +08:00
|
|
|
|
const loginDomain = JSON.parse(localStorage.getItem('loginDomain')!)
|
2025-12-20 18:52:33 +08:00
|
|
|
|
|
2025-12-23 19:15:00 +08:00
|
|
|
|
// 侧边栏展开状态
|
|
|
|
|
|
const isSidebarOpen = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 切换侧边栏
|
|
|
|
|
|
const toggleSidebar = () => {
|
|
|
|
|
|
isSidebarOpen.value = !isSidebarOpen.value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 18:52:33 +08:00
|
|
|
|
// 搜索文本
|
|
|
|
|
|
const searchText = ref('')
|
2025-12-23 16:56:22 +08:00
|
|
|
|
const userType = true //web端固定这个
|
2025-12-22 17:03:37 +08:00
|
|
|
|
// 加载状态
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const messageLoading = ref(false)
|
2025-12-23 19:15:00 +08:00
|
|
|
|
const loadingMore = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 分页状态
|
|
|
|
|
|
const PAGE_SIZE = 5
|
|
|
|
|
|
const currentPage = ref(1)
|
|
|
|
|
|
const hasMore = ref(true)
|
2025-12-22 17:03:37 +08:00
|
|
|
|
|
2025-12-20 18:52:33 +08:00
|
|
|
|
// 聊天室列表
|
2025-12-22 17:03:37 +08:00
|
|
|
|
const chatRooms = ref<ChatRoomVO[]>([])
|
2025-12-20 18:52:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 当前选中的聊天室ID
|
|
|
|
|
|
const currentRoomId = ref<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
// 当前聊天室
|
|
|
|
|
|
const currentRoom = computed(() =>
|
2025-12-22 17:03:37 +08:00
|
|
|
|
chatRooms.value.find((r: ChatRoomVO) => r.roomId === currentRoomId.value)
|
2025-12-20 18:52:33 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 当前工单ID
|
|
|
|
|
|
const currentWorkcaseId = computed(() => currentRoom.value?.workcaseId || '')
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤后的聊天室列表
|
|
|
|
|
|
const filteredRooms = computed(() => {
|
|
|
|
|
|
if (!searchText.value) return chatRooms.value
|
|
|
|
|
|
const keyword = searchText.value.toLowerCase()
|
2025-12-22 17:03:37 +08:00
|
|
|
|
return chatRooms.value.filter((room: ChatRoomVO) =>
|
|
|
|
|
|
room.roomName?.toLowerCase().includes(keyword) ||
|
|
|
|
|
|
room.guestName?.toLowerCase().includes(keyword) ||
|
|
|
|
|
|
room.workcaseId?.toLowerCase().includes(keyword)
|
2025-12-20 18:52:33 +08:00
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 消息列表
|
2025-12-22 17:03:37 +08:00
|
|
|
|
const messages = ref<ChatRoomMessageVO[]>([])
|
2025-12-20 18:52:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 工单详情对话框
|
|
|
|
|
|
const showWorkcaseDetail = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// Jitsi Meet会议相关
|
|
|
|
|
|
const currentMeetingUrl = ref('')
|
|
|
|
|
|
const showMeetingIframe = ref(false)
|
|
|
|
|
|
|
2025-12-23 19:15:00 +08:00
|
|
|
|
// ChatRoom组件引用
|
|
|
|
|
|
const chatRoomRef = ref<InstanceType<typeof ChatRoom> | null>(null)
|
|
|
|
|
|
|
2025-12-22 17:03:37 +08:00
|
|
|
|
// 获取聊天室列表
|
|
|
|
|
|
const fetchChatRooms = async () => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await workcaseChatAPI.getChatRoomPage({
|
|
|
|
|
|
filter: { status: 'active' },
|
|
|
|
|
|
pageParam: { page: 1, pageSize: 100, total: 0 }
|
2025-12-24 15:02:23 +08:00
|
|
|
|
}, loginDomain.user.userId)
|
2025-12-22 17:03:37 +08:00
|
|
|
|
if (result.success && result.pageDomain) {
|
|
|
|
|
|
chatRooms.value = result.pageDomain.dataList || []
|
|
|
|
|
|
}
|
2025-12-23 19:15:00 +08:00
|
|
|
|
|
2025-12-22 17:03:37 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取聊天室列表失败:', error)
|
|
|
|
|
|
ElMessage.error('获取聊天室列表失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 18:52:33 +08:00
|
|
|
|
// 选择聊天室
|
2025-12-24 15:02:23 +08:00
|
|
|
|
const selectRoom = async (roomId: string) => {
|
2025-12-20 18:52:33 +08:00
|
|
|
|
currentRoomId.value = roomId
|
2025-12-24 15:02:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 自动加入聊天室成员表(如果不存在)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const memberData: TbChatRoomMemberDTO = {
|
|
|
|
|
|
roomId: roomId,
|
|
|
|
|
|
userId: loginDomain.user.userId,
|
|
|
|
|
|
userName: loginDomain.userInfo.username,
|
|
|
|
|
|
userType: 'staff'
|
|
|
|
|
|
}
|
|
|
|
|
|
await workcaseChatAPI.addChatRoomMember(memberData)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 已存在成员或其他错误,忽略
|
|
|
|
|
|
console.debug('加入聊天室:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 18:52:33 +08:00
|
|
|
|
loadMessages(roomId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 19:15:00 +08:00
|
|
|
|
// 加载消息(初始加载page1,后端降序返回)
|
2025-12-20 18:52:33 +08:00
|
|
|
|
const loadMessages = async (roomId: string) => {
|
2025-12-22 17:03:37 +08:00
|
|
|
|
messageLoading.value = true
|
2025-12-23 19:15:00 +08:00
|
|
|
|
currentPage.value = 1
|
|
|
|
|
|
hasMore.value = true
|
2025-12-22 17:03:37 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const result = await workcaseChatAPI.getChatMessagePage({
|
|
|
|
|
|
filter: { roomId },
|
2025-12-23 19:15:00 +08:00
|
|
|
|
pageParam: { page: 1, pageSize: PAGE_SIZE }
|
2025-12-22 17:03:37 +08:00
|
|
|
|
})
|
|
|
|
|
|
if (result.success && result.pageDomain) {
|
2025-12-23 19:15:00 +08:00
|
|
|
|
const pageInfo = result.pageDomain.pageParam
|
|
|
|
|
|
const actualTotalPages = pageInfo?.totalPages || 1
|
|
|
|
|
|
hasMore.value = actualTotalPages > currentPage.value
|
|
|
|
|
|
|
|
|
|
|
|
// 后端降序返回,需要反转后显示(早的在上,新的在下)
|
|
|
|
|
|
const dataList = result.pageDomain.dataList || []
|
|
|
|
|
|
messages.value = [...dataList].reverse()
|
2025-12-20 18:52:33 +08:00
|
|
|
|
}
|
2025-12-23 19:15:00 +08:00
|
|
|
|
// 加载完成后滚动到底部
|
2025-12-22 17:03:37 +08:00
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载消息失败:', error)
|
|
|
|
|
|
ElMessage.error('加载消息失败')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
messageLoading.value = false
|
|
|
|
|
|
}
|
2025-12-20 18:52:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 19:15:00 +08:00
|
|
|
|
// 加载更多历史消息(滚动到顶部触发)
|
|
|
|
|
|
const loadMoreMessages = async () => {
|
|
|
|
|
|
if (!currentRoomId.value || loadingMore.value || !hasMore.value) return
|
|
|
|
|
|
|
|
|
|
|
|
const nextPage = currentPage.value + 1
|
|
|
|
|
|
loadingMore.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await workcaseChatAPI.getChatMessagePage({
|
|
|
|
|
|
filter: { roomId: currentRoomId.value },
|
|
|
|
|
|
pageParam: { page: nextPage, pageSize: PAGE_SIZE }
|
|
|
|
|
|
})
|
|
|
|
|
|
if (result.success && result.pageDomain) {
|
|
|
|
|
|
const pageInfo = result.pageDomain.pageParam
|
|
|
|
|
|
const actualTotalPages = pageInfo?.totalPages || 1
|
|
|
|
|
|
const dataList = result.pageDomain.dataList || []
|
|
|
|
|
|
|
|
|
|
|
|
if (dataList.length > 0) {
|
|
|
|
|
|
currentPage.value = nextPage
|
|
|
|
|
|
hasMore.value = actualTotalPages > currentPage.value
|
|
|
|
|
|
// 后端降序返回,反转后插入到列表前面
|
|
|
|
|
|
const reversedList = [...dataList].reverse()
|
|
|
|
|
|
messages.value.unshift(...reversedList)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
hasMore.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载更多消息失败:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingMore.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-20 18:52:33 +08:00
|
|
|
|
// 处理发送消息(从ChatRoom组件触发)
|
|
|
|
|
|
const handleSendMessage = async (content: string, files: File[]) => {
|
|
|
|
|
|
if (!currentRoomId.value) return
|
|
|
|
|
|
|
2025-12-22 17:03:37 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 上传文件获取fileIds
|
|
|
|
|
|
let fileIds: string[] = []
|
|
|
|
|
|
if (files.length > 0) {
|
|
|
|
|
|
const uploadResult = await fileAPI.batchUpload(files, 'chatroom')
|
|
|
|
|
|
if (uploadResult.success && uploadResult.dataList) {
|
|
|
|
|
|
fileIds = uploadResult.dataList.map((f: any) => f.fileId)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构造消息
|
|
|
|
|
|
const messageData: TbChatRoomMessageDTO = {
|
|
|
|
|
|
roomId: currentRoomId.value,
|
2025-12-24 15:02:23 +08:00
|
|
|
|
senderId: loginDomain.user.userId,
|
|
|
|
|
|
senderName: loginDomain.userInfo.username,
|
2025-12-22 17:03:37 +08:00
|
|
|
|
senderType: 'agent',
|
|
|
|
|
|
content,
|
|
|
|
|
|
files: fileIds,
|
|
|
|
|
|
messageType: 'text'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送消息
|
|
|
|
|
|
const result = await workcaseChatAPI.sendMessage(messageData)
|
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
|
// 添加到消息列表
|
|
|
|
|
|
messages.value.push(result.data as ChatRoomMessageVO)
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(result.message || '发送失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('发送消息失败:', error)
|
|
|
|
|
|
ElMessage.error('发送消息失败')
|
2025-12-20 18:52:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 下载文件
|
|
|
|
|
|
const downloadFile = (fileId: string) => {
|
2025-12-22 17:03:37 +08:00
|
|
|
|
window.open(`${FILE_DOWNLOAD_URL}/${fileId}`, '_blank')
|
2025-12-20 18:52:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发起会议
|
|
|
|
|
|
const startMeeting = async () => {
|
|
|
|
|
|
if (!currentRoomId.value) return
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: 调用后端API创建Jitsi会议
|
2025-12-22 17:03:37 +08:00
|
|
|
|
const meetingId = 'meeting-' + currentRoomId.value + '-' + Date.now()
|
2025-12-20 18:52:33 +08:00
|
|
|
|
currentMeetingUrl.value = `https://meet.jit.si/${meetingId}`
|
|
|
|
|
|
showMeetingIframe.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 19:15:00 +08:00
|
|
|
|
// 滚动聊天消息到底部
|
2025-12-20 18:52:33 +08:00
|
|
|
|
const scrollToBottom = () => {
|
2025-12-23 19:15:00 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
chatRoomRef.value?.scrollToBottom()
|
|
|
|
|
|
})
|
2025-12-20 18:52:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 格式化时间(用于聊天室列表)
|
2025-12-22 17:03:37 +08:00
|
|
|
|
const formatTime = (time: string | null | undefined) => {
|
2025-12-20 18:52:33 +08:00
|
|
|
|
if (!time) return ''
|
|
|
|
|
|
const date = new Date(time)
|
|
|
|
|
|
const now = new Date()
|
|
|
|
|
|
const diff = now.getTime() - date.getTime()
|
|
|
|
|
|
|
|
|
|
|
|
if (diff < 60000) return '刚刚'
|
|
|
|
|
|
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
|
|
|
|
|
|
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'
|
|
|
|
|
|
|
|
|
|
|
|
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
|
|
|
|
|
|
}
|
2025-12-22 17:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== WebSocket连接管理 ====================
|
|
|
|
|
|
|
2025-12-22 17:08:49 +08:00
|
|
|
|
// 初始化WebSocket连接(支持SockJS降级)
|
2025-12-22 17:03:37 +08:00
|
|
|
|
const initWebSocket = () => {
|
|
|
|
|
|
const token = localStorage.getItem('token') || ''
|
|
|
|
|
|
const wsUrl = getWsUrl()
|
|
|
|
|
|
|
|
|
|
|
|
console.log('WebSocket连接URL:', wsUrl)
|
|
|
|
|
|
|
2025-12-22 17:08:49 +08:00
|
|
|
|
// 创建STOMP客户端,使用SockJS(支持降级)
|
2025-12-22 17:03:37 +08:00
|
|
|
|
stompClient = new Client({
|
2025-12-22 17:08:49 +08:00
|
|
|
|
webSocketFactory: () => new SockJS(wsUrl),
|
2025-12-22 17:03:37 +08:00
|
|
|
|
connectHeaders: {
|
|
|
|
|
|
Authorization: `Bearer ${token}`
|
|
|
|
|
|
},
|
|
|
|
|
|
reconnectDelay: 5000,
|
|
|
|
|
|
heartbeatIncoming: 4000,
|
|
|
|
|
|
heartbeatOutgoing: 4000,
|
|
|
|
|
|
debug: (str: string) => {
|
|
|
|
|
|
console.log('[STOMP]', str)
|
|
|
|
|
|
},
|
|
|
|
|
|
onConnect: () => {
|
|
|
|
|
|
console.log('WebSocket已连接')
|
|
|
|
|
|
// 订阅聊天室列表更新
|
|
|
|
|
|
subscribeToListUpdate()
|
|
|
|
|
|
// 如果当前有选中的聊天室,订阅该聊天室消息
|
|
|
|
|
|
if (currentRoomId.value) {
|
|
|
|
|
|
subscribeToRoom(currentRoomId.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onDisconnect: () => {
|
|
|
|
|
|
console.log('WebSocket已断开')
|
|
|
|
|
|
},
|
|
|
|
|
|
onStompError: (frame: any) => {
|
|
|
|
|
|
console.error('STOMP错误:', frame)
|
|
|
|
|
|
},
|
|
|
|
|
|
onWebSocketError: (event: any) => {
|
|
|
|
|
|
console.error('WebSocket错误:', event)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
stompClient.activate()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 15:02:23 +08:00
|
|
|
|
// 订阅聊天室列表更新 (用于更新列表中的lastMessage和未读数)
|
2025-12-22 17:03:37 +08:00
|
|
|
|
const subscribeToListUpdate = () => {
|
|
|
|
|
|
if (!stompClient || !stompClient.connected) return
|
|
|
|
|
|
|
2025-12-24 15:02:23 +08:00
|
|
|
|
listSubscription = stompClient.subscribe('/topic/chat/list-update', async (message: any) => {
|
2025-12-22 17:03:37 +08:00
|
|
|
|
const chatMessage = JSON.parse(message.body)
|
|
|
|
|
|
// 更新对应聊天室的lastMessage和lastMessageTime
|
|
|
|
|
|
const roomIndex = chatRooms.value.findIndex((r: ChatRoomVO) => r.roomId === chatMessage.roomId)
|
|
|
|
|
|
if (roomIndex !== -1) {
|
2025-12-24 15:02:23 +08:00
|
|
|
|
// 查询当前用户在该聊天室的未读数
|
|
|
|
|
|
let unreadCount = 0
|
|
|
|
|
|
try {
|
|
|
|
|
|
const unreadResult = await workcaseChatAPI.getUnreadCount(
|
|
|
|
|
|
chatMessage.roomId,
|
|
|
|
|
|
loginDomain.user.userId
|
|
|
|
|
|
)
|
|
|
|
|
|
if (unreadResult.success && unreadResult.data !== undefined) {
|
|
|
|
|
|
unreadCount = unreadResult.data
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('查询未读数失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 17:03:37 +08:00
|
|
|
|
chatRooms.value[roomIndex] = {
|
|
|
|
|
|
...chatRooms.value[roomIndex],
|
|
|
|
|
|
lastMessage: chatMessage.content,
|
2025-12-24 15:02:23 +08:00
|
|
|
|
lastMessageTime: chatMessage.sendTime,
|
|
|
|
|
|
unreadCount: unreadCount
|
2025-12-22 17:03:37 +08:00
|
|
|
|
}
|
2025-12-24 15:02:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 将更新的聊天室移到列表顶部
|
|
|
|
|
|
const updatedRoom = chatRooms.value[roomIndex]
|
|
|
|
|
|
chatRooms.value.splice(roomIndex, 1)
|
|
|
|
|
|
chatRooms.value.unshift(updatedRoom)
|
2025-12-22 17:03:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 订阅指定聊天室消息 (用于实时接收消息)
|
|
|
|
|
|
const subscribeToRoom = (roomId: string) => {
|
|
|
|
|
|
if (!stompClient || !stompClient.connected) return
|
|
|
|
|
|
|
|
|
|
|
|
// 先取消之前的订阅
|
|
|
|
|
|
if (roomSubscription) {
|
|
|
|
|
|
roomSubscription.unsubscribe()
|
|
|
|
|
|
roomSubscription = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
roomSubscription = stompClient.subscribe(`/topic/chat/${roomId}`, (message: any) => {
|
|
|
|
|
|
const chatMessage = JSON.parse(message.body) as ChatRoomMessageVO
|
|
|
|
|
|
// 避免重复添加自己发送的消息
|
2025-12-24 15:02:23 +08:00
|
|
|
|
if (chatMessage.senderId !== loginDomain.user.userId) {
|
2025-12-22 17:03:37 +08:00
|
|
|
|
messages.value.push(chatMessage)
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 断开WebSocket连接
|
|
|
|
|
|
const disconnectWebSocket = () => {
|
|
|
|
|
|
if (roomSubscription) {
|
|
|
|
|
|
roomSubscription.unsubscribe()
|
|
|
|
|
|
roomSubscription = null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (listSubscription) {
|
|
|
|
|
|
listSubscription.unsubscribe()
|
|
|
|
|
|
listSubscription = null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (stompClient) {
|
|
|
|
|
|
stompClient.deactivate()
|
|
|
|
|
|
stompClient = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 监听currentRoomId变化,切换聊天室时重新订阅
|
|
|
|
|
|
watch(currentRoomId, (newRoomId) => {
|
|
|
|
|
|
if (newRoomId && stompClient?.connected) {
|
|
|
|
|
|
subscribeToRoom(newRoomId)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchChatRooms()
|
|
|
|
|
|
initWebSocket()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时断开连接
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
disconnectWebSocket()
|
|
|
|
|
|
})
|
2025-12-20 18:52:33 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
@import url("./ChatRoomView.scss");
|
|
|
|
|
|
</style>
|