聊天室完成
This commit is contained in:
@@ -66,7 +66,7 @@ public interface ChatRoomService {
|
||||
/**
|
||||
* @description 分页查询聊天室(含当前用户未读数)
|
||||
* @param pageRequest 分页请求参数
|
||||
* @param userId 当前用户ID
|
||||
* @param userId 当前用户ID(用于查询未读数)
|
||||
* @author cascade
|
||||
* @since 2025-12-22
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
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.RequestParam;
|
||||
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.ChatRoomVO;
|
||||
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.page.PageRequest;
|
||||
import org.xyzh.common.utils.validation.ValidationResult;
|
||||
@@ -56,6 +59,9 @@ public class WorkcaseChatContorller {
|
||||
@Autowired
|
||||
private ChatRoomService chatRoomService;
|
||||
|
||||
@Autowired
|
||||
private JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
// ========================= ChatRoom聊天室管理(实时IM) =========================
|
||||
|
||||
@Operation(summary = "创建聊天室(转人工时调用)")
|
||||
@@ -107,8 +113,7 @@ public class WorkcaseChatContorller {
|
||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||
@PostMapping("/room/page")
|
||||
public ResultDomain<ChatRoomVO> getChatRoomPage(
|
||||
@RequestBody PageRequest<TbChatRoomDTO> pageRequest,
|
||||
@RequestParam(value = "userId", required = true) String userId) {
|
||||
@RequestBody PageRequest<TbChatRoomDTO> pageRequest) {
|
||||
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
|
||||
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
|
||||
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
|
||||
@@ -116,6 +121,9 @@ public class WorkcaseChatContorller {
|
||||
if (!vr.isValid()) {
|
||||
return ResultDomain.failure(vr.getAllErrors());
|
||||
}
|
||||
|
||||
String userId = LoginUtil.getCurrentUserId();
|
||||
|
||||
return chatRoomService.getChatRoomPage(pageRequest, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public interface TbChatRoomMapper {
|
||||
List<ChatRoomVO> selectChatRoomList(@Param("filter") TbChatRoomDTO filter);
|
||||
|
||||
/**
|
||||
* 分页查询聊天室
|
||||
* 分页查询聊天室(含未读数)
|
||||
*/
|
||||
List<ChatRoomVO> selectChatRoomPage(@Param("filter") TbChatRoomDTO filter, @Param("pageParam") PageParam pageParam, @Param("userId") String userId);
|
||||
|
||||
|
||||
@@ -233,7 +233,13 @@ public class ChatRoomServiceImpl implements ChatRoomService {
|
||||
filter.setUserId(member.getUserId());
|
||||
List<ChatMemberVO> existingMembers = chatRoomMemberMapper.selectChatRoomMemberList(filter);
|
||||
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()) {
|
||||
|
||||
@@ -60,11 +60,11 @@ export const workcaseChatAPI = {
|
||||
|
||||
/**
|
||||
* 分页查询聊天室(含当前用户未读数)
|
||||
* @param pageRequest - 分页请求,filter.guestId用于过滤聊天室(guest用)
|
||||
* 注:userId从token中自动获取,无需传递
|
||||
*/
|
||||
async getChatRoomPage(pageRequest: PageRequest<TbChatRoomDTO>, userId: string): Promise<ResultDomain<ChatRoomVO>> {
|
||||
const response = await api.post<ChatRoomVO>(`${this.baseUrl}/room/page`, pageRequest, {
|
||||
params: { userId }
|
||||
})
|
||||
async getChatRoomPage(pageRequest: PageRequest<TbChatRoomDTO>): Promise<ResultDomain<ChatRoomVO>> {
|
||||
const response = await api.post<ChatRoomVO>(`${this.baseUrl}/room/page`, pageRequest)
|
||||
return response.data
|
||||
},
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ $brand-color-hover: #004488;
|
||||
|
||||
// ==================== 消息列表 ====================
|
||||
.messages-list {
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px;
|
||||
|
||||
|
||||
@@ -644,3 +644,46 @@ $brand-color-hover: #004488;
|
||||
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); }
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<button
|
||||
v-for="room in filteredRooms.slice(0, 8)"
|
||||
:key="room.roomId"
|
||||
@click="selectRoom(room.roomId!); toggleSidebar()"
|
||||
@click="selectRoom(room)"
|
||||
class="sidebar-icon-btn"
|
||||
:class="{ active: currentRoomId === room.roomId }"
|
||||
:title="room.roomName"
|
||||
@@ -55,7 +55,7 @@
|
||||
:key="room.roomId"
|
||||
class="room-item"
|
||||
:class="{ active: currentRoomId === room.roomId }"
|
||||
@click="selectRoom(room.roomId!)"
|
||||
@click="selectRoom(room)"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<div class="room-avatar">
|
||||
@@ -85,6 +85,13 @@
|
||||
<!-- 主聊天区域 -->
|
||||
<main class="chat-main" :class="{ 'sidebar-open': isSidebarOpen }">
|
||||
<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
|
||||
ref="chatRoomRef"
|
||||
:messages="messages"
|
||||
@@ -123,6 +130,7 @@
|
||||
</ElButton>
|
||||
</template>
|
||||
</ChatRoom>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
@@ -193,6 +201,8 @@ const userType = true //web端固定这个
|
||||
const loading = ref(false)
|
||||
const messageLoading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
// 自动填充加载状态
|
||||
const autoFilling = ref(false)
|
||||
|
||||
// 分页状态
|
||||
const PAGE_SIZE = 5
|
||||
@@ -242,9 +252,11 @@ const fetchChatRooms = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await workcaseChatAPI.getChatRoomPage({
|
||||
filter: { status: 'active' },
|
||||
filter: {
|
||||
status: 'active'
|
||||
},
|
||||
pageParam: { page: 1, pageSize: 100, total: 0 }
|
||||
}, loginDomain.user.userId)
|
||||
})
|
||||
if (result.success && result.pageDomain) {
|
||||
chatRooms.value = result.pageDomain.dataList || []
|
||||
}
|
||||
@@ -258,24 +270,25 @@ const fetchChatRooms = async () => {
|
||||
}
|
||||
|
||||
// 选择聊天室
|
||||
const selectRoom = async (roomId: string) => {
|
||||
currentRoomId.value = roomId
|
||||
const selectRoom = async (room: ChatRoomVO) => {
|
||||
currentRoomId.value = room.roomId!
|
||||
|
||||
// 自动加入聊天室成员表(如果不存在)
|
||||
try {
|
||||
const memberData: TbChatRoomMemberDTO = {
|
||||
roomId: roomId,
|
||||
roomId: room.roomId,
|
||||
userId: loginDomain.user.userId,
|
||||
userName: loginDomain.userInfo.username,
|
||||
userType: 'staff'
|
||||
}
|
||||
await workcaseChatAPI.addChatRoomMember(memberData)
|
||||
room.unreadCount = 0
|
||||
} catch (error) {
|
||||
// 已存在成员或其他错误,忽略
|
||||
console.debug('加入聊天室:', error)
|
||||
}
|
||||
|
||||
loadMessages(roomId)
|
||||
loadMessages(room.roomId!)
|
||||
}
|
||||
|
||||
// 加载消息(初始加载page1,后端降序返回)
|
||||
@@ -296,6 +309,9 @@ const loadMessages = async (roomId: string) => {
|
||||
// 后端降序返回,需要反转后显示(早的在上,新的在下)
|
||||
const dataList = result.pageDomain.dataList || []
|
||||
messages.value = [...dataList].reverse()
|
||||
|
||||
// 首次加载后自动填充消息直到出现滚动条
|
||||
await autoFillMessages(roomId)
|
||||
}
|
||||
// 加载完成后滚动到底部
|
||||
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 () => {
|
||||
if (!currentRoomId.value || loadingMore.value || !hasMore.value) return
|
||||
|
||||
@@ -10,9 +10,9 @@ declare const uni: {
|
||||
}
|
||||
|
||||
import type { ResultDomain } from '../types'
|
||||
|
||||
import { BASE_URL as CONFIG_BASE_URL } from '../config'
|
||||
// API 基础配置
|
||||
const BASE_URL = 'http://localhost:8180'
|
||||
const BASE_URL = CONFIG_BASE_URL
|
||||
|
||||
// 通用请求方法
|
||||
export function request<T>(options: {
|
||||
|
||||
@@ -105,6 +105,13 @@ export const workcaseChatAPI = {
|
||||
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消息管理 ======================
|
||||
|
||||
/**
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export const AGENT_ID = '17664699513920001'
|
||||
export const BASE_URL = 'http://localhost:8180'
|
||||
export const WS_HOST = 'localhost:8180' // WebSocket host(不包含协议)
|
||||
@@ -260,6 +260,31 @@
|
||||
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 {
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
|
||||
@@ -46,6 +46,12 @@
|
||||
:style="{ top: (headerPaddingTop + 88) + 'px' }"
|
||||
@scrolltoupper="loadMoreMessages"
|
||||
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">
|
||||
<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 { workcaseChatAPI } from '@/api/workcase'
|
||||
import { wsClient } from '@/utils/websocket'
|
||||
|
||||
import { WS_HOST } from '@/config'
|
||||
// 响应式数据
|
||||
const headerPaddingTop = ref<number>(44)
|
||||
const headerTotalHeight = ref<number>(88)
|
||||
@@ -148,6 +154,8 @@ function loadUserInfo() {
|
||||
|
||||
// 消息列表
|
||||
const messages = reactive<ChatRoomMessageVO[]>([])
|
||||
// 初始加载遮罩(自动填充期间显示,完成后隐藏)
|
||||
const showLoadingMask = ref<boolean>(true)
|
||||
|
||||
// 所有默认客服
|
||||
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)
|
||||
|
||||
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) {
|
||||
@@ -300,13 +323,116 @@ async function loadMessages() {
|
||||
const reversedList = [...msgRes.dataList].reverse()
|
||||
messages.splice(0, messages.length, ...reversedList)
|
||||
console.log('[loadMessages] 加载完成, 消息数:', messages.length)
|
||||
|
||||
// 加载完第一页后检查是否需要自动填充
|
||||
if (currentPage.value === 1) {
|
||||
await autoFillMessages() // 自动填充结束后会自动滚动到底部
|
||||
} else {
|
||||
// 非首次加载(如刷新等),直接滚动到底部
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
}
|
||||
} catch (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() {
|
||||
console.log('[loadMoreMessages] 触发, roomId:', roomId.value, 'loadingMore:', loadingMore.value, 'hasMore:', hasMore.value, 'currentPage:', currentPage.value)
|
||||
@@ -475,9 +601,11 @@ async function initWebSocket() {
|
||||
}
|
||||
|
||||
// 构建WebSocket URL
|
||||
const protocol = 'wss:' // 生产环境使用wss
|
||||
const host = 'your-domain.com' // 需要替换为实际域名
|
||||
const wsUrl = `${protocol}//${host}/api/urban-lifeline/workcase/ws/chat-sockjs?token=${encodeURIComponent(token)}`
|
||||
// 开发环境: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)
|
||||
|
||||
@@ -50,8 +50,10 @@
|
||||
}
|
||||
|
||||
.list {
|
||||
height: calc(100vh - 88rpx); /* 减去导航栏高度 */
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: 60rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.room-card {
|
||||
|
||||
@@ -54,6 +54,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { workcaseChatAPI } from '@/api'
|
||||
import type { ChatRoomVO, TbChatRoomDTO, PageRequest, ChatRoomMessageVO } from '@/types'
|
||||
import { wsClient } from '@/utils/websocket'
|
||||
import { WS_HOST } from '@/config'
|
||||
|
||||
// 导航栏
|
||||
const navPaddingTop = ref<number>(0)
|
||||
@@ -168,6 +169,7 @@ function getStatusText(status?: string): string {
|
||||
|
||||
// 进入聊天室
|
||||
function enterRoom(room: ChatRoomVO) {
|
||||
room.unreadCount = 0
|
||||
uni.navigateTo({
|
||||
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${room.roomId}&workcaseId=${room.workcaseId || ''}`
|
||||
})
|
||||
@@ -190,9 +192,11 @@ async function initWebSocket() {
|
||||
}
|
||||
|
||||
// 构建WebSocket URL
|
||||
const protocol = 'wss:' // 生产环境使用wss
|
||||
const host = 'your-domain.com' // 需要替换为实际域名
|
||||
const wsUrl = `${protocol}//${host}/api/urban-lifeline/workcase/ws/chat-sockjs?token=${encodeURIComponent(token)}`
|
||||
// 开发环境: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('[chatRoomList] 开始连接WebSocket')
|
||||
await wsClient.connect(wsUrl, token)
|
||||
@@ -216,16 +220,32 @@ function disconnectWebSocket() {
|
||||
}
|
||||
|
||||
// 处理列表更新消息
|
||||
function handleListUpdate(message: ChatRoomMessageVO) {
|
||||
async function handleListUpdate(message: ChatRoomMessageVO) {
|
||||
console.log('[chatRoomList] 收到列表更新消息:', message)
|
||||
|
||||
// 更新对应聊天室的lastMessage和lastMessageTime
|
||||
const roomIndex = chatRooms.value.findIndex((r: ChatRoomVO) => r.roomId === message.roomId)
|
||||
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],
|
||||
lastMessage: message.content || '',
|
||||
lastMessageTime: message.sendTime || ''
|
||||
lastMessageTime: message.sendTime || '',
|
||||
unreadCount: unreadCount
|
||||
}
|
||||
|
||||
// 将更新的聊天室移到列表顶部
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
* 支持STOMP协议和uni.connectSocket API
|
||||
*/
|
||||
|
||||
// uni-app 类型声明
|
||||
declare const uni: {
|
||||
connectSocket: (options: any) => any
|
||||
getStorageSync: (key: string) => any
|
||||
}
|
||||
|
||||
interface StompFrame {
|
||||
command: string
|
||||
headers: Record<string, string>
|
||||
@@ -31,13 +37,50 @@ export class WebSocketClient {
|
||||
/**
|
||||
* 连接WebSocket
|
||||
*/
|
||||
connect(url: string, token: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
async connect(url: string, token: string): Promise<void> {
|
||||
// 如果已经连接到相同的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.token = token
|
||||
|
||||
console.log('[WebSocket] 开始连接:', url)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
this.socketTask = uni.connectSocket({
|
||||
url: url,
|
||||
success: () => {
|
||||
@@ -184,7 +227,7 @@ export class WebSocketClient {
|
||||
success: () => {
|
||||
console.log('[WebSocket] 发送成功:', frame.command)
|
||||
},
|
||||
fail: (err) => {
|
||||
fail: (err: any) => {
|
||||
console.error('[WebSocket] 发送失败:', err)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user