聊天室完成

This commit is contained in:
2025-12-24 16:32:06 +08:00
parent 898da3a2c6
commit ad03f3f2db
16 changed files with 432 additions and 53 deletions

View File

@@ -66,7 +66,7 @@ public interface ChatRoomService {
/** /**
* @description 分页查询聊天室(含当前用户未读数) * @description 分页查询聊天室(含当前用户未读数)
* @param pageRequest 分页请求参数 * @param pageRequest 分页请求参数
* @param userId 当前用户ID * @param userId 当前用户ID(用于查询未读数)
* @author cascade * @author cascade
* @since 2025-12-22 * @since 2025-12-22
*/ */

View File

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -23,6 +24,8 @@ import org.xyzh.api.workcase.vo.ChatMemberVO;
import org.xyzh.api.workcase.vo.ChatRoomMessageVO; import org.xyzh.api.workcase.vo.ChatRoomMessageVO;
import org.xyzh.api.workcase.vo.ChatRoomVO; import org.xyzh.api.workcase.vo.ChatRoomVO;
import org.xyzh.api.workcase.vo.CustomerServiceVO; import org.xyzh.api.workcase.vo.CustomerServiceVO;
import org.xyzh.common.auth.utils.JwtTokenUtil;
import org.xyzh.common.auth.utils.LoginUtil;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest; import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.utils.validation.ValidationResult; import org.xyzh.common.utils.validation.ValidationResult;
@@ -56,6 +59,9 @@ public class WorkcaseChatContorller {
@Autowired @Autowired
private ChatRoomService chatRoomService; private ChatRoomService chatRoomService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
// ========================= ChatRoom聊天室管理实时IM ========================= // ========================= ChatRoom聊天室管理实时IM =========================
@Operation(summary = "创建聊天室(转人工时调用)") @Operation(summary = "创建聊天室(转人工时调用)")
@@ -107,8 +113,7 @@ public class WorkcaseChatContorller {
@PreAuthorize("hasAuthority('workcase:room:view')") @PreAuthorize("hasAuthority('workcase:room:view')")
@PostMapping("/room/page") @PostMapping("/room/page")
public ResultDomain<ChatRoomVO> getChatRoomPage( public ResultDomain<ChatRoomVO> getChatRoomPage(
@RequestBody PageRequest<TbChatRoomDTO> pageRequest, @RequestBody PageRequest<TbChatRoomDTO> pageRequest) {
@RequestParam(value = "userId", required = true) String userId) {
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList( ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null), ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100) ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
@@ -116,6 +121,9 @@ public class WorkcaseChatContorller {
if (!vr.isValid()) { if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors()); return ResultDomain.failure(vr.getAllErrors());
} }
String userId = LoginUtil.getCurrentUserId();
return chatRoomService.getChatRoomPage(pageRequest, userId); return chatRoomService.getChatRoomPage(pageRequest, userId);
} }

View File

@@ -44,7 +44,7 @@ public interface TbChatRoomMapper {
List<ChatRoomVO> selectChatRoomList(@Param("filter") TbChatRoomDTO filter); List<ChatRoomVO> selectChatRoomList(@Param("filter") TbChatRoomDTO filter);
/** /**
* 分页查询聊天室 * 分页查询聊天室(含未读数)
*/ */
List<ChatRoomVO> selectChatRoomPage(@Param("filter") TbChatRoomDTO filter, @Param("pageParam") PageParam pageParam, @Param("userId") String userId); List<ChatRoomVO> selectChatRoomPage(@Param("filter") TbChatRoomDTO filter, @Param("pageParam") PageParam pageParam, @Param("userId") String userId);

View File

@@ -233,7 +233,13 @@ public class ChatRoomServiceImpl implements ChatRoomService {
filter.setUserId(member.getUserId()); filter.setUserId(member.getUserId());
List<ChatMemberVO> existingMembers = chatRoomMemberMapper.selectChatRoomMemberList(filter); List<ChatMemberVO> existingMembers = chatRoomMemberMapper.selectChatRoomMemberList(filter);
if (existingMembers != null && !existingMembers.isEmpty()) { if (existingMembers != null && !existingMembers.isEmpty()) {
return ResultDomain.failure("用户已是聊天室成员"); // 重置未读数量
TbChatRoomMemberDTO updateMember = new TbChatRoomMemberDTO();
updateMember.setMemberId(existingMembers.get(0).getMemberId());
updateMember.setUnreadCount(0);
chatRoomMemberMapper.updateChatRoomMember(updateMember);
return ResultDomain.failure("用户已是聊天室成员,更新未读");
} }
if (member.getMemberId() == null || member.getMemberId().isEmpty()) { if (member.getMemberId() == null || member.getMemberId().isEmpty()) {

View File

@@ -60,11 +60,11 @@ export const workcaseChatAPI = {
/** /**
* 分页查询聊天室(含当前用户未读数) * 分页查询聊天室(含当前用户未读数)
* @param pageRequest - 分页请求filter.guestId用于过滤聊天室guest用
* 注userId从token中自动获取无需传递
*/ */
async getChatRoomPage(pageRequest: PageRequest<TbChatRoomDTO>, userId: string): Promise<ResultDomain<ChatRoomVO>> { async getChatRoomPage(pageRequest: PageRequest<TbChatRoomDTO>): Promise<ResultDomain<ChatRoomVO>> {
const response = await api.post<ChatRoomVO>(`${this.baseUrl}/room/page`, pageRequest, { const response = await api.post<ChatRoomVO>(`${this.baseUrl}/room/page`, pageRequest)
params: { userId }
})
return response.data return response.data
}, },

View File

@@ -64,7 +64,7 @@ $brand-color-hover: #004488;
// ==================== 消息列表 ==================== // ==================== 消息列表 ====================
.messages-list { .messages-list {
max-width: 900px; width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 24px 16px; padding: 24px 16px;

View File

@@ -644,3 +644,46 @@ $brand-color-hover: #004488;
padding: 24px; padding: 24px;
} }
} }
// ==================== 聊天室包装容器 ====================
.chat-room-wrapper {
position: relative;
width: 100%;
height: 100%;
}
// ==================== 自动填充加载遮罩 ====================
.auto-fill-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
gap: 16px;
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #e2e8f0;
border-top-color: $brand-color;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-text {
font-size: 14px;
color: #64748b;
font-weight: 500;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@@ -9,7 +9,7 @@
<button <button
v-for="room in filteredRooms.slice(0, 8)" v-for="room in filteredRooms.slice(0, 8)"
:key="room.roomId" :key="room.roomId"
@click="selectRoom(room.roomId!); toggleSidebar()" @click="selectRoom(room)"
class="sidebar-icon-btn" class="sidebar-icon-btn"
:class="{ active: currentRoomId === room.roomId }" :class="{ active: currentRoomId === room.roomId }"
:title="room.roomName" :title="room.roomName"
@@ -55,7 +55,7 @@
:key="room.roomId" :key="room.roomId"
class="room-item" class="room-item"
:class="{ active: currentRoomId === room.roomId }" :class="{ active: currentRoomId === room.roomId }"
@click="selectRoom(room.roomId!)" @click="selectRoom(room)"
> >
<!-- 头像 --> <!-- 头像 -->
<div class="room-avatar"> <div class="room-avatar">
@@ -85,6 +85,13 @@
<!-- 主聊天区域 --> <!-- 主聊天区域 -->
<main class="chat-main" :class="{ 'sidebar-open': isSidebarOpen }"> <main class="chat-main" :class="{ 'sidebar-open': isSidebarOpen }">
<template v-if="currentRoomId"> <template v-if="currentRoomId">
<div class="chat-room-wrapper">
<!-- 自动填充加载遮罩 -->
<div v-if="autoFilling" class="auto-fill-mask">
<div class="loading-spinner"></div>
<div class="loading-text">正在加载历史消息...</div>
</div>
<ChatRoom <ChatRoom
ref="chatRoomRef" ref="chatRoomRef"
:messages="messages" :messages="messages"
@@ -123,6 +130,7 @@
</ElButton> </ElButton>
</template> </template>
</ChatRoom> </ChatRoom>
</div>
</template> </template>
<!-- 空状态 --> <!-- 空状态 -->
@@ -193,6 +201,8 @@ const userType = true //web端固定这个
const loading = ref(false) const loading = ref(false)
const messageLoading = ref(false) const messageLoading = ref(false)
const loadingMore = ref(false) const loadingMore = ref(false)
// 自动填充加载状态
const autoFilling = ref(false)
// 分页状态 // 分页状态
const PAGE_SIZE = 5 const PAGE_SIZE = 5
@@ -242,9 +252,11 @@ const fetchChatRooms = async () => {
loading.value = true loading.value = true
try { try {
const result = await workcaseChatAPI.getChatRoomPage({ const result = await workcaseChatAPI.getChatRoomPage({
filter: { status: 'active' }, filter: {
status: 'active'
},
pageParam: { page: 1, pageSize: 100, total: 0 } pageParam: { page: 1, pageSize: 100, total: 0 }
}, loginDomain.user.userId) })
if (result.success && result.pageDomain) { if (result.success && result.pageDomain) {
chatRooms.value = result.pageDomain.dataList || [] chatRooms.value = result.pageDomain.dataList || []
} }
@@ -258,24 +270,25 @@ const fetchChatRooms = async () => {
} }
// 选择聊天室 // 选择聊天室
const selectRoom = async (roomId: string) => { const selectRoom = async (room: ChatRoomVO) => {
currentRoomId.value = roomId currentRoomId.value = room.roomId!
// 自动加入聊天室成员表(如果不存在) // 自动加入聊天室成员表(如果不存在)
try { try {
const memberData: TbChatRoomMemberDTO = { const memberData: TbChatRoomMemberDTO = {
roomId: roomId, roomId: room.roomId,
userId: loginDomain.user.userId, userId: loginDomain.user.userId,
userName: loginDomain.userInfo.username, userName: loginDomain.userInfo.username,
userType: 'staff' userType: 'staff'
} }
await workcaseChatAPI.addChatRoomMember(memberData) await workcaseChatAPI.addChatRoomMember(memberData)
room.unreadCount = 0
} catch (error) { } catch (error) {
// 已存在成员或其他错误,忽略 // 已存在成员或其他错误,忽略
console.debug('加入聊天室:', error) console.debug('加入聊天室:', error)
} }
loadMessages(roomId) loadMessages(room.roomId!)
} }
// 加载消息初始加载page1后端降序返回 // 加载消息初始加载page1后端降序返回
@@ -296,6 +309,9 @@ const loadMessages = async (roomId: string) => {
// 后端降序返回,需要反转后显示(早的在上,新的在下) // 后端降序返回,需要反转后显示(早的在上,新的在下)
const dataList = result.pageDomain.dataList || [] const dataList = result.pageDomain.dataList || []
messages.value = [...dataList].reverse() messages.value = [...dataList].reverse()
// 首次加载后自动填充消息直到出现滚动条
await autoFillMessages(roomId)
} }
// 加载完成后滚动到底部 // 加载完成后滚动到底部
scrollToBottom() scrollToBottom()
@@ -307,6 +323,85 @@ const loadMessages = async (roomId: string) => {
} }
} }
// 自动填充消息直到出现滚动条
const autoFillMessages = async (roomId: string) => {
autoFilling.value = true
console.log('[autoFill] 开始检查消息高度, hasMore:', hasMore.value, 'messages:', messages.value.length)
// 等待DOM渲染
await nextTick()
await new Promise(resolve => setTimeout(resolve, 300))
let attempts = 0
const maxAttempts = 20
while (hasMore.value && attempts < maxAttempts) {
attempts++
const container = chatRoomRef.value?.$el?.querySelector?.('.messages-container')
const messagesList = chatRoomRef.value?.$el?.querySelector?.('.messages-list')
if (!container || !messagesList) {
console.warn('[autoFill] 找不到容器或消息列表')
break
}
// 容器高度(可视区域)
const containerHeight = container.clientHeight
// 消息列表实际高度(内容)
const listHeight = messagesList.offsetHeight
const fillPercent = containerHeight > 0 ? Math.round(listHeight / containerHeight * 100) : 0
console.log(`[autoFill] 第${attempts}次检查 - 容器高度: ${containerHeight}px, 列表高度: ${listHeight}px, 填充率: ${fillPercent}%, 消息数: ${messages.value.length}`)
// 判断是否已经溢出(列表高度 >= 容器高度)
if (containerHeight > 0 && listHeight >= containerHeight) {
console.log(`[autoFill] ✓ 列表已溢出(${fillPercent}%),可以滚动!停止加载`)
break
}
// 内容不足,继续加载下一页
console.log('[autoFill] → 内容不足,继续加载历史消息...')
const nextPage = currentPage.value + 1
const result = await workcaseChatAPI.getChatMessagePage({
filter: { roomId },
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)
console.log(`[autoFill] ✓ 加载第${nextPage}页完成, 新增${dataList.length}条, 总消息数: ${messages.value.length}`)
await nextTick()
await new Promise(resolve => setTimeout(resolve, 200))
} else {
hasMore.value = false
break
}
} else {
break
}
}
if (attempts >= maxAttempts) {
console.warn('[autoFill] ⚠ 达到最大尝试次数')
}
console.log(`[autoFill] 自动填充结束 - 共尝试${attempts}次, 最终消息数: ${messages.value.length}, hasMore: ${hasMore.value}`)
autoFilling.value = false
}
// 加载更多历史消息(滚动到顶部触发) // 加载更多历史消息(滚动到顶部触发)
const loadMoreMessages = async () => { const loadMoreMessages = async () => {
if (!currentRoomId.value || loadingMore.value || !hasMore.value) return if (!currentRoomId.value || loadingMore.value || !hasMore.value) return

View File

@@ -10,9 +10,9 @@ declare const uni: {
} }
import type { ResultDomain } from '../types' import type { ResultDomain } from '../types'
import { BASE_URL as CONFIG_BASE_URL } from '../config'
// API 基础配置 // API 基础配置
const BASE_URL = 'http://localhost:8180' const BASE_URL = CONFIG_BASE_URL
// 通用请求方法 // 通用请求方法
export function request<T>(options: { export function request<T>(options: {

View File

@@ -105,6 +105,13 @@ export const workcaseChatAPI = {
return request<ChatMemberVO>({ url: `${this.baseUrl}/room/${roomId}/members`, method: 'GET' }) return request<ChatMemberVO>({ url: `${this.baseUrl}/room/${roomId}/members`, method: 'GET' })
}, },
/**
* 获取当前用户在指定聊天室的未读消息数
*/
getUnreadCount(roomId: string, userId: string): Promise<ResultDomain<number>> {
return request<number>({ url: `${this.baseUrl}/room/${roomId}/unread?userId=${userId}`, method: 'GET' })
},
// ====================== ChatRoom消息管理 ====================== // ====================== ChatRoom消息管理 ======================
/** /**

View File

@@ -1 +1,3 @@
export const AGENT_ID = '17664699513920001' export const AGENT_ID = '17664699513920001'
export const BASE_URL = 'http://localhost:8180'
export const WS_HOST = 'localhost:8180' // WebSocket host不包含协议

View File

@@ -260,6 +260,31 @@
color: #999; color: #999;
} }
.loading-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #f4f5f7;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
.loading-more { .loading-more {
padding: 20rpx; padding: 20rpx;
display: flex; display: flex;

View File

@@ -46,6 +46,12 @@
:style="{ top: (headerPaddingTop + 88) + 'px' }" :style="{ top: (headerPaddingTop + 88) + 'px' }"
@scrolltoupper="loadMoreMessages" @scrolltoupper="loadMoreMessages"
upper-threshold="50"> upper-threshold="50">
<!-- 加载遮罩层 -->
<view v-if="showLoadingMask" class="loading-mask">
<view class="loading-content">
<text class="loading-text">加载中...</text>
</view>
</view>
<!-- 加载更多提示 --> <!-- 加载更多提示 -->
<view v-if="loadingMore" class="loading-more"> <view v-if="loadingMore" class="loading-more">
<text class="loading-more-text">加载中...</text> <text class="loading-more-text">加载中...</text>
@@ -113,7 +119,7 @@ import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO } from '@/types/workcase' import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO } from '@/types/workcase'
import { workcaseChatAPI } from '@/api/workcase' import { workcaseChatAPI } from '@/api/workcase'
import { wsClient } from '@/utils/websocket' import { wsClient } from '@/utils/websocket'
import { WS_HOST } from '@/config'
// 响应式数据 // 响应式数据
const headerPaddingTop = ref<number>(44) const headerPaddingTop = ref<number>(44)
const headerTotalHeight = ref<number>(88) const headerTotalHeight = ref<number>(88)
@@ -148,6 +154,8 @@ function loadUserInfo() {
// 消息列表 // 消息列表
const messages = reactive<ChatRoomMessageVO[]>([]) const messages = reactive<ChatRoomMessageVO[]>([])
// 初始加载遮罩(自动填充期间显示,完成后隐藏)
const showLoadingMask = ref<boolean>(true)
// 所有默认客服 // 所有默认客服
const defaultWorkers = reactive<CustomerVO[]>([]) const defaultWorkers = reactive<CustomerVO[]>([])
@@ -254,13 +262,28 @@ watch(roomId, (newRoomId, oldRoomId) => {
}) })
// 加载聊天室 // 加载聊天室
const PAGE_SIZE = 5 const PAGE_SIZE = 20
const messageTotal = ref<number>(0) const messageTotal = ref<number>(0)
async function loadChatRoom() { async function loadChatRoom() {
if (!roomId.value) return if (!roomId.value) return
loading.value = true loading.value = true
try { 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) const roomRes = await workcaseChatAPI.getChatRoomById(roomId.value)
if (roomRes.success && roomRes.data) { if (roomRes.success && roomRes.data) {
@@ -300,13 +323,116 @@ async function loadMessages() {
const reversedList = [...msgRes.dataList].reverse() const reversedList = [...msgRes.dataList].reverse()
messages.splice(0, messages.length, ...reversedList) messages.splice(0, messages.length, ...reversedList)
console.log('[loadMessages] 加载完成, 消息数:', messages.length) console.log('[loadMessages] 加载完成, 消息数:', messages.length)
// 加载完第一页后检查是否需要自动填充
if (currentPage.value === 1) {
await autoFillMessages() // 自动填充结束后会自动滚动到底部
} else {
// 非首次加载(如刷新等),直接滚动到底部
nextTick(() => scrollToBottom()) nextTick(() => scrollToBottom())
} }
}
} catch (e) { } catch (e) {
console.error('加载消息列表失败:', e) console.error('加载消息列表失败:', e)
} }
} }
// 自动填充消息直到出现滚动条
async function autoFillMessages() {
console.log('[autoFill] 开始自动填充, hasMore:', hasMore.value, 'messages:', messages.length)
// 等待DOM渲染
await new Promise(resolve => setTimeout(resolve, 500))
// 持续加载直到真正出现滚动或没有更多数据
let attempts = 0
const maxAttempts = 20 // 最多20次防止死循环
while (hasMore.value && attempts < maxAttempts) {
attempts++
try {
// 获取 scroll-view 容器高度
const query = uni.createSelectorQuery()
const chatAreaHeight: number = await new Promise((resolve) => {
query.select('.chat-area').boundingClientRect().exec((res: any[]) => {
const rect = res[0]
console.log('[autoFill] chat-area rect:', rect)
resolve(rect?.height || 0)
})
})
// 获取消息列表容器高度(.message-list
const query2 = uni.createSelectorQuery()
const contentHeight: number = await new Promise((resolve) => {
query2.select('.message-list').boundingClientRect().exec((res: any[]) => {
const rect = res[0]
console.log('[autoFill] message-list rect:', rect)
resolve(rect?.height || 0)
})
})
const fillPercent = chatAreaHeight > 0 ? Math.round(contentHeight / chatAreaHeight * 100) : 0
console.log(`[autoFill] 第${attempts}次检查 - 容器: ${chatAreaHeight}px, 内容: ${contentHeight}px, 填充率: ${fillPercent}%, 消息数: ${messages.length}, hasMore: ${hasMore.value}`)
// 判断是否已经溢出(内容高度 >= 容器高度)
if (chatAreaHeight > 0 && contentHeight >= chatAreaHeight) {
console.log(`[autoFill] ✓ 内容已溢出(${fillPercent}%),可以滚动!停止加载`)
break
}
// 内容不足,继续加载下一页
if (chatAreaHeight > 0) {
console.log('[autoFill] → 内容不足,继续加载历史消息...')
const nextPage = currentPage.value + 1
const msgRes = await workcaseChatAPI.getChatMessagePage({
filter: { roomId: roomId.value },
pageParam: { page: nextPage, pageSize: PAGE_SIZE }
})
if (msgRes.success && msgRes.dataList && msgRes.dataList.length > 0) {
const pageInfo = msgRes.pageDomain?.pageParam
const actualTotalPages = pageInfo?.totalPages || 1
currentPage.value = nextPage
hasMore.value = actualTotalPages > currentPage.value
// 反转后插入到列表前面
const reversedList = [...msgRes.dataList].reverse()
messages.unshift(...reversedList)
console.log(`[autoFill] ✓ 加载第${nextPage}页完成, 新增${msgRes.dataList.length}条, 总消息数: ${messages.length}, hasMore: ${hasMore.value}`)
// 等待DOM更新
await new Promise(resolve => setTimeout(resolve, 300))
} else {
console.log('[autoFill] ✗ 没有更多数据了')
hasMore.value = false
break
}
} else {
console.error('[autoFill] ✗ 无法获取容器高度')
break
}
} catch (error) {
console.error('[autoFill] ✗ 检查失败:', error)
break
}
}
if (attempts >= maxAttempts) {
console.warn('[autoFill] ⚠ 达到最大尝试次数,停止自动填充')
}
console.log(`[autoFill] 自动填充结束 - 共尝试${attempts}次, 最终消息数: ${messages.length}, hasMore: ${hasMore.value}`)
// 移除加载遮罩并滚动到底部
showLoadingMask.value = false
await new Promise(resolve => setTimeout(resolve, 50))
nextTick(() => {
scrollToBottom()
console.log('[autoFill] 遮罩已移除,已滚动到底部')
})
}
// 加载更多历史消息(滚动到顶部触发,加载下一页更早的消息) // 加载更多历史消息(滚动到顶部触发,加载下一页更早的消息)
async function loadMoreMessages() { async function loadMoreMessages() {
console.log('[loadMoreMessages] 触发, roomId:', roomId.value, 'loadingMore:', loadingMore.value, 'hasMore:', hasMore.value, 'currentPage:', currentPage.value) console.log('[loadMoreMessages] 触发, roomId:', roomId.value, 'loadingMore:', loadingMore.value, 'hasMore:', hasMore.value, 'currentPage:', currentPage.value)
@@ -475,9 +601,11 @@ async function initWebSocket() {
} }
// 构建WebSocket URL // 构建WebSocket URL
const protocol = 'wss:' // 生产环境使用wss // 开发环境ws://localhost:8180 或 ws://192.168.x.x:8180
const host = 'your-domain.com' // 需要替换为实际域名 // 生产环境wss://your-domain.com
const wsUrl = `${protocol}//${host}/api/urban-lifeline/workcase/ws/chat-sockjs?token=${encodeURIComponent(token)}` const protocol = 'ws:' // 开发环境使用ws生产环境改为wss
// 小程序使用原生WebSocket端点不是SockJS端点
const wsUrl = `${protocol}//${WS_HOST}/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}`
console.log('[chatRoom] 开始连接WebSocket') console.log('[chatRoom] 开始连接WebSocket')
await wsClient.connect(wsUrl, token) await wsClient.connect(wsUrl, token)

View File

@@ -50,8 +50,10 @@
} }
.list { .list {
height: calc(100vh - 88rpx); /* 减去导航栏高度 */
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
padding-bottom: 60rpx; padding-bottom: 60rpx;
box-sizing: border-box;
} }
.room-card { .room-card {

View File

@@ -54,6 +54,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
import { workcaseChatAPI } from '@/api' import { workcaseChatAPI } from '@/api'
import type { ChatRoomVO, TbChatRoomDTO, PageRequest, ChatRoomMessageVO } from '@/types' import type { ChatRoomVO, TbChatRoomDTO, PageRequest, ChatRoomMessageVO } from '@/types'
import { wsClient } from '@/utils/websocket' import { wsClient } from '@/utils/websocket'
import { WS_HOST } from '@/config'
// 导航栏 // 导航栏
const navPaddingTop = ref<number>(0) const navPaddingTop = ref<number>(0)
@@ -168,6 +169,7 @@ function getStatusText(status?: string): string {
// 进入聊天室 // 进入聊天室
function enterRoom(room: ChatRoomVO) { function enterRoom(room: ChatRoomVO) {
room.unreadCount = 0
uni.navigateTo({ uni.navigateTo({
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${room.roomId}&workcaseId=${room.workcaseId || ''}` url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${room.roomId}&workcaseId=${room.workcaseId || ''}`
}) })
@@ -190,9 +192,11 @@ async function initWebSocket() {
} }
// 构建WebSocket URL // 构建WebSocket URL
const protocol = 'wss:' // 生产环境使用wss // 开发环境ws://localhost:8180 或 ws://192.168.x.x:8180
const host = 'your-domain.com' // 需要替换为实际域名 // 生产环境wss://your-domain.com
const wsUrl = `${protocol}//${host}/api/urban-lifeline/workcase/ws/chat-sockjs?token=${encodeURIComponent(token)}` const protocol = 'ws:' // 开发环境使用ws生产环境改为wss
// 小程序使用原生WebSocket端点不是SockJS端点
const wsUrl = `${protocol}//${WS_HOST}/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}`
console.log('[chatRoomList] 开始连接WebSocket') console.log('[chatRoomList] 开始连接WebSocket')
await wsClient.connect(wsUrl, token) await wsClient.connect(wsUrl, token)
@@ -216,16 +220,32 @@ function disconnectWebSocket() {
} }
// 处理列表更新消息 // 处理列表更新消息
function handleListUpdate(message: ChatRoomMessageVO) { async function handleListUpdate(message: ChatRoomMessageVO) {
console.log('[chatRoomList] 收到列表更新消息:', message) console.log('[chatRoomList] 收到列表更新消息:', message)
// 更新对应聊天室的lastMessage和lastMessageTime // 更新对应聊天室的lastMessage和lastMessageTime
const roomIndex = chatRooms.value.findIndex((r: ChatRoomVO) => r.roomId === message.roomId) const roomIndex = chatRooms.value.findIndex((r: ChatRoomVO) => r.roomId === message.roomId)
if (roomIndex !== -1) { if (roomIndex !== -1) {
// 查询当前用户在该聊天室的未读数
let unreadCount = 0
try {
const userInfo = uni.getStorageSync('userInfo')
const userId = typeof userInfo === 'string' ? JSON.parse(userInfo).userId : userInfo?.userId
if (userId) {
const unreadResult = await workcaseChatAPI.getUnreadCount(message.roomId, userId)
if (unreadResult.success && unreadResult.data !== undefined) {
unreadCount = unreadResult.data
}
}
} catch (error) {
console.error('[chatRoomList] 查询未读数失败:', error)
}
chatRooms.value[roomIndex] = { chatRooms.value[roomIndex] = {
...chatRooms.value[roomIndex], ...chatRooms.value[roomIndex],
lastMessage: message.content || '', lastMessage: message.content || '',
lastMessageTime: message.sendTime || '' lastMessageTime: message.sendTime || '',
unreadCount: unreadCount
} }
// 将更新的聊天室移到列表顶部 // 将更新的聊天室移到列表顶部

View File

@@ -3,6 +3,12 @@
* 支持STOMP协议和uni.connectSocket API * 支持STOMP协议和uni.connectSocket API
*/ */
// uni-app 类型声明
declare const uni: {
connectSocket: (options: any) => any
getStorageSync: (key: string) => any
}
interface StompFrame { interface StompFrame {
command: string command: string
headers: Record<string, string> headers: Record<string, string>
@@ -31,13 +37,50 @@ export class WebSocketClient {
/** /**
* 连接WebSocket * 连接WebSocket
*/ */
connect(url: string, token: string): Promise<void> { async connect(url: string, token: string): Promise<void> {
return new Promise((resolve, reject) => { // 如果已经连接到相同的URL直接返回
if (this.connected && this.url === url) {
console.log('[WebSocket] 已连接到相同URL跳过')
return Promise.resolve()
}
// 如果有旧连接,先关闭并等待关闭完成
if (this.socketTask) {
console.log('[WebSocket] 关闭旧连接')
await new Promise<void>((resolveClose) => {
try {
this.socketTask.close({
success: () => {
console.log('[WebSocket] 旧连接已关闭')
this.socketTask = null
this.connected = false
resolveClose()
},
fail: () => {
console.warn('[WebSocket] 关闭旧连接失败')
this.socketTask = null
this.connected = false
resolveClose()
}
})
} catch (e) {
console.warn('[WebSocket] 关闭旧连接异常:', e)
this.socketTask = null
this.connected = false
resolveClose()
}
})
// 等待一小段时间确保旧连接完全关闭
await new Promise(resolve => setTimeout(resolve, 100))
}
this.url = url this.url = url
this.token = token this.token = token
console.log('[WebSocket] 开始连接:', url) console.log('[WebSocket] 开始连接:', url)
return new Promise((resolve, reject) => {
this.socketTask = uni.connectSocket({ this.socketTask = uni.connectSocket({
url: url, url: url,
success: () => { success: () => {
@@ -184,7 +227,7 @@ export class WebSocketClient {
success: () => { success: () => {
console.log('[WebSocket] 发送成功:', frame.command) console.log('[WebSocket] 发送成功:', frame.command)
}, },
fail: (err) => { fail: (err: any) => {
console.error('[WebSocket] 发送失败:', err) console.error('[WebSocket] 发送失败:', err)
} }
}) })