gateway tomcat去除

This commit is contained in:
2025-12-22 17:03:37 +08:00
parent e09817015e
commit b023bec261
55 changed files with 1926 additions and 260 deletions

View File

@@ -10,17 +10,20 @@
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@stomp/stompjs": "^7.2.1",
"@vueuse/core": "^11.3.0",
"axios": "^1.7.9",
"element-plus": "^2.8.6",
"lucide-vue-next": "^0.561.0",
"pinia": "^2.2.8",
"sockjs-client": "^1.6.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@module-federation/vite": "^1.9.3",
"@types/node": "^22.0.0",
"@types/sockjs-client": "^1.5.4",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"typescript": "^5.7.2",

View File

@@ -8,6 +8,9 @@ dependencies:
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.25)
'@stomp/stompjs':
specifier: ^7.2.1
version: 7.2.1
'@vueuse/core':
specifier: ^11.3.0
version: 11.3.0(vue@3.5.25)
@@ -17,9 +20,15 @@ dependencies:
element-plus:
specifier: ^2.8.6
version: 2.12.0(vue@3.5.25)
lucide-vue-next:
specifier: ^0.561.0
version: 0.561.0(vue@3.5.25)
pinia:
specifier: ^2.2.8
version: 2.3.1(typescript@5.9.3)(vue@3.5.25)
sockjs-client:
specifier: ^1.6.1
version: 1.6.1
vue:
specifier: ^3.5.13
version: 3.5.25(typescript@5.9.3)
@@ -34,6 +43,9 @@ devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.19.1
'@types/sockjs-client':
specifier: ^1.5.4
version: 1.5.4
'@vitejs/plugin-vue':
specifier: ^5.2.1
version: 5.2.4(vite@6.4.1)(vue@3.5.25)
@@ -841,6 +853,10 @@ packages:
dev: true
optional: true
/@stomp/stompjs@7.2.1:
resolution: {integrity: sha512-DLd/WeicnHS5SsWWSk3x6/pcivqchNaEvg9UEGVqAcfYEBVmS9D6980ckXjTtfpXLjdLDsd96M7IuX4w7nzq5g==}
dev: false
/@sxzz/popperjs-es@2.11.7:
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
dev: false
@@ -865,6 +881,10 @@ packages:
undici-types: 6.21.0
dev: true
/@types/sockjs-client@1.5.4:
resolution: {integrity: sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==}
dev: true
/@types/web-bluetooth@0.0.16:
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
dev: false
@@ -1188,6 +1208,17 @@ packages:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
dev: true
/debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.3
dev: false
/debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -1319,6 +1350,18 @@ packages:
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
/eventsource@2.0.2:
resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==}
engines: {node: '>=12.0.0'}
dev: false
/faye-websocket@0.11.4:
resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==}
engines: {node: '>=0.8.0'}
dependencies:
websocket-driver: 0.7.4
dev: false
/fdir@6.5.0(picomatch@4.0.3):
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -1422,6 +1465,14 @@ packages:
hasBin: true
dev: true
/http-parser-js@0.5.10:
resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==}
dev: false
/inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
@@ -1464,6 +1515,14 @@ packages:
yallist: 3.1.1
dev: true
/lucide-vue-next@0.561.0(vue@3.5.25):
resolution: {integrity: sha512-c5HUckO0qHklVSOf/0vaSR3pEb8fYImRDCRDLde56uqS9js0D/e3RAvq0/YFWjkmyOBKCb0/IdskdoHZQEkT5g==}
peerDependencies:
vue: '>=3.0.1'
dependencies:
vue: 3.5.25(typescript@5.9.3)
dev: false
/magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
dependencies:
@@ -1499,7 +1558,6 @@ packages:
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
/muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
@@ -1563,6 +1621,14 @@ packages:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
dev: false
/querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: false
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/rollup@4.53.3:
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -1595,11 +1661,28 @@ packages:
fsevents: 2.3.3
dev: true
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
/semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
dev: true
/sockjs-client@1.6.1:
resolution: {integrity: sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==}
engines: {node: '>=12'}
dependencies:
debug: 3.2.7
eventsource: 2.0.2
faye-websocket: 0.11.4
inherits: 2.0.4
url-parse: 1.5.10
transitivePeerDependencies:
- supports-color
dev: false
/source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -1632,6 +1715,13 @@ packages:
picocolors: 1.1.1
dev: true
/url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
dev: false
/vite@6.4.1(@types/node@22.19.1):
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -1737,6 +1827,20 @@ packages:
'@vue/shared': 3.5.25
typescript: 5.9.3
/websocket-driver@0.7.4:
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
engines: {node: '>=0.8.0'}
dependencies:
http-parser-js: 0.5.10
safe-buffer: 5.2.1
websocket-extensions: 0.1.4
dev: false
/websocket-extensions@0.1.4:
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
engines: {node: '>=0.8.0'}
dev: false
/yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
dev: true

View File

@@ -98,8 +98,10 @@ declare module 'shared/api/ai' {
declare module 'shared/api/workcase' {
export const workcaseAPI: any
export const workcaseChatAPI: any
}
// ============ types模块 ==================
declare module 'shared/types' {
import type { BaseDTO } from '../../../shared/src/types/base'
@@ -143,21 +145,30 @@ declare module 'shared/types' {
TbWorkcaseDeviceDTO,
// 聊天室相关
TbChatRoomDTO,
TbChatMessageDTO,
TbChatRoomMessageDTO,
TbChatRoomMemberDTO,
TbVideoMeetingDTO,
TbMeetingParticipantDTO,
TbMeetingTranscriptionDTO,
ChatRoomVO,
ChatMessageVO,
ChatRoomMessageVO,
ChatMemberVO,
VideoMeetingVO,
MeetingParticipantVO,
SendMessageParam,
CreateMeetingParam,
MarkReadParam,
// 客服相关
TbCustomerServiceDTO,
CustomerServiceVO,
// 词云
TbWordCloudDTO,
// 来客相关
TbGuestDTO,
GuestVO
GuestVO,
CustomerVO,
ConversationVO
} from '../../../shared/src/types/workcase'
// 重新导出 menu

View File

@@ -31,7 +31,7 @@
>
<!-- 头像 -->
<div class="room-avatar">
{{ room.guestName.substring(0, 1) }}
{{ room.guestName?.substring(0, 1) || '?' }}
</div>
<!-- 信息 -->
@@ -114,67 +114,50 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElButton, ElInput, ElDialog } from 'element-plus'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ElButton, ElInput, ElDialog, ElMessage } from 'element-plus'
import { Search, FileText, MessageSquare } from 'lucide-vue-next'
import ChatRoom from 'shared/components/chatRoom/ChatRoom.vue'
import WorkcaseDetail from '@/views/public/workcase/WorkcaseDetail/WorkcaseDetail.vue'
import { workcaseChatAPI } from 'shared/api/workcase'
import { fileAPI } from 'shared/api/file'
import { FILE_DOWNLOAD_URL } from '@/config'
import type { ChatRoomVO, ChatRoomMessageVO, TbChatRoomMessageDTO } from 'shared/types'
import SockJS from 'sockjs-client'
import { Client } from '@stomp/stompjs'
interface ChatRoomVO {
roomId: string
workcaseId: string
roomName: string
guestName: string
lastMessage: string | null
lastMessageTime: string | null
unreadCount: number
// WebSocket配置 (通过网关代理访问workcase服务)
// 原生WebSocket URL (ws://或wss://)
const getWsUrl = () => {
const token = localStorage.getItem('token') || ''
// 直接连接网关跳过Nginx调试
return `ws://localhost:8180/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}`
}
interface ChatMessageVO {
messageId: string
senderId: string
senderName: string
senderAvatar: string
content: string
files: string[]
sendTime: string
}
// STOMP客户端
let stompClient: any = null
let roomSubscription: any = null
let listSubscription: any = null
// 当前用户ID
const userId = ref('CURRENT_USER_ID')
// 当前用户ID(从登录状态获取)
const userId = ref(localStorage.getItem('userId') || '')
// 搜索文本
const searchText = ref('')
// 加载状态
const loading = ref(false)
const messageLoading = ref(false)
// 聊天室列表
const chatRooms = ref<ChatRoomVO[]>([
{
roomId: 'ROOM001',
workcaseId: 'WC001',
roomName: '工单#WC001 - 电源故障',
guestName: '张三',
lastMessage: '好的,谢谢您的帮助',
lastMessageTime: new Date().toISOString(),
unreadCount: 3
},
{
roomId: 'ROOM002',
workcaseId: 'WC002',
roomName: '工单#WC002 - 设备维修',
guestName: '李四',
lastMessage: '请问什么时候能来处理?',
lastMessageTime: new Date(Date.now() - 3600000).toISOString(),
unreadCount: 0
}
])
const chatRooms = ref<ChatRoomVO[]>([])
// 当前选中的聊天室ID
const currentRoomId = ref<string | null>(null)
// 当前聊天室
const currentRoom = computed(() =>
chatRooms.value.find(r => r.roomId === currentRoomId.value)
chatRooms.value.find((r: ChatRoomVO) => r.roomId === currentRoomId.value)
)
// 当前工单ID
@@ -184,15 +167,15 @@ const currentWorkcaseId = computed(() => currentRoom.value?.workcaseId || '')
const filteredRooms = computed(() => {
if (!searchText.value) return chatRooms.value
const keyword = searchText.value.toLowerCase()
return chatRooms.value.filter(room =>
room.roomName.toLowerCase().includes(keyword) ||
room.guestName.toLowerCase().includes(keyword) ||
room.workcaseId.toLowerCase().includes(keyword)
return chatRooms.value.filter((room: ChatRoomVO) =>
room.roomName?.toLowerCase().includes(keyword) ||
room.guestName?.toLowerCase().includes(keyword) ||
room.workcaseId?.toLowerCase().includes(keyword)
)
})
// 消息列表
const messages = ref<ChatMessageVO[]>([])
const messages = ref<ChatRoomMessageVO[]>([])
// 工单详情对话框
const showWorkcaseDetail = ref(false)
@@ -201,66 +184,93 @@ const showWorkcaseDetail = ref(false)
const currentMeetingUrl = ref('')
const showMeetingIframe = ref(false)
// 获取聊天室列表
const fetchChatRooms = async () => {
loading.value = true
try {
const result = await workcaseChatAPI.getChatRoomPage({
filter: { status: 'active' },
pageParam: { page: 1, pageSize: 100, total: 0 }
})
if (result.success && result.pageDomain) {
chatRooms.value = result.pageDomain.dataList || []
}
} catch (error) {
console.error('获取聊天室列表失败:', error)
ElMessage.error('获取聊天室列表失败')
} finally {
loading.value = false
}
}
// 选择聊天室
const selectRoom = (roomId: string) => {
currentRoomId.value = roomId
// TODO: 加载该聊天室的消息
loadMessages(roomId)
}
// 加载消息
const loadMessages = async (roomId: string) => {
// TODO: 调用API加载消息
messages.value = [
{
messageId: 'MSG001',
senderId: 'OTHER_USER',
senderName: '张三',
senderAvatar: 'avatar.jpg',
content: '你好,我的设备出现故障了',
files: [],
sendTime: new Date().toISOString()
},
{
messageId: 'MSG002',
senderId: userId.value,
senderName: '客服',
senderAvatar: 'avatar.jpg',
content: '您好,请问是什么故障?',
files: [],
sendTime: new Date().toISOString()
messageLoading.value = true
try {
const result = await workcaseChatAPI.getChatMessagePage({
filter: { roomId },
pageParam: { page: 1, pageSize: 100, total: 0 }
})
if (result.success && result.pageDomain) {
messages.value = result.pageDomain.dataList || []
}
]
scrollToBottom()
scrollToBottom()
} catch (error) {
console.error('加载消息失败:', error)
ElMessage.error('加载消息失败')
} finally {
messageLoading.value = false
}
}
// 处理发送消息从ChatRoom组件触发
const handleSendMessage = async (content: string, files: File[]) => {
if (!currentRoomId.value) return
// TODO: 上传文件获取fileIds
const fileIds: string[] = []
const newMessage: ChatMessageVO = {
messageId: 'MSG' + Date.now(),
senderId: userId.value,
senderName: '客服',
senderAvatar: 'avatar.jpg',
content,
files: fileIds,
sendTime: new Date().toISOString()
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,
senderId: userId.value,
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('发送消息失败')
}
messages.value.push(newMessage)
// TODO: 通过WebSocket发送到服务器
console.log('发送消息:', { content, files })
}
// 下载文件
const downloadFile = (fileId: string) => {
// TODO: 下载文件
console.log('下载文件:', fileId)
window.open(`${FILE_DOWNLOAD_URL}/${fileId}`, '_blank')
}
// 发起会议
@@ -268,14 +278,9 @@ const startMeeting = async () => {
if (!currentRoomId.value) return
// TODO: 调用后端API创建Jitsi会议
// const meeting = await createMeeting(currentRoomId.value)
// 模拟会议URL
const meetingId = 'meeting-' + Date.now()
const meetingId = 'meeting-' + currentRoomId.value + '-' + Date.now()
currentMeetingUrl.value = `https://meet.jit.si/${meetingId}`
showMeetingIframe.value = true
console.log('发起会议:', currentMeetingUrl.value)
}
// 滚动到底部
@@ -284,7 +289,7 @@ const scrollToBottom = () => {
}
// 格式化时间(用于聊天室列表)
const formatTime = (time: string | null) => {
const formatTime = (time: string | null | undefined) => {
if (!time) return ''
const date = new Date(time)
const now = new Date()
@@ -296,6 +301,122 @@ const formatTime = (time: string | null) => {
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
}
// ==================== WebSocket连接管理 ====================
// 初始化WebSocket连接使用原生WebSocket不降级
const initWebSocket = () => {
const token = localStorage.getItem('token') || ''
const wsUrl = getWsUrl()
console.log('WebSocket连接URL:', wsUrl)
// 创建STOMP客户端使用原生WebSocket
stompClient = new Client({
brokerURL: wsUrl, // 使用原生WebSocket URL
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()
}
// 订阅聊天室列表更新 (用于更新列表中的lastMessage)
const subscribeToListUpdate = () => {
if (!stompClient || !stompClient.connected) return
listSubscription = stompClient.subscribe('/topic/chat/list-update', (message: any) => {
const chatMessage = JSON.parse(message.body)
// 更新对应聊天室的lastMessage和lastMessageTime
const roomIndex = chatRooms.value.findIndex((r: ChatRoomVO) => r.roomId === chatMessage.roomId)
if (roomIndex !== -1) {
chatRooms.value[roomIndex] = {
...chatRooms.value[roomIndex],
lastMessage: chatMessage.content,
lastMessageTime: chatMessage.sendTime
}
}
})
}
// 订阅指定聊天室消息 (用于实时接收消息)
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
// 避免重复添加自己发送的消息
if (chatMessage.senderId !== userId.value) {
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()
})
</script>
<style scoped lang="scss">
@import url("./ChatRoomView.scss");

View File

@@ -41,7 +41,8 @@ export default defineConfig(({ mode }) => ({
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: true,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true,
global: 'globalThis'
},
resolve: {
@@ -64,6 +65,7 @@ export default defineConfig(({ mode }) => ({
'/api': {
target: 'http://localhost:8180',
changeOrigin: true,
ws: true, // 启用 WebSocket 代理
rewrite: (path: string) => path.replace(/^\/api/, '')
},
// 代理共享模块请求到 shared 服务