小程序聊天查询
This commit is contained in:
@@ -54,6 +54,12 @@ public class ResultDomain<T> implements Serializable {
|
|||||||
this.message = message;
|
this.message = message;
|
||||||
this.success = false;
|
this.success = false;
|
||||||
this.pageDomain = pageDomain;
|
this.pageDomain = pageDomain;
|
||||||
|
if(pageDomain != null) this.dataList = pageDomain.getDataList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPageDomain(PageDomain<T> pageDomain){
|
||||||
|
this.pageDomain = pageDomain;
|
||||||
|
if(pageDomain != null) this.dataList = pageDomain.getDataList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 静态工厂方法 - 推荐使用(简洁、清晰)
|
// 静态工厂方法 - 推荐使用(简洁、清晰)
|
||||||
@@ -88,6 +94,7 @@ public class ResultDomain<T> implements Serializable {
|
|||||||
result.success = true;
|
result.success = true;
|
||||||
result.message = message;
|
result.message = message;
|
||||||
result.pageDomain = pageDomain;
|
result.pageDomain = pageDomain;
|
||||||
|
if(pageDomain != null) result.dataList = pageDomain.getDataList();
|
||||||
result.code = HttpStatus.OK.value();
|
result.code = HttpStatus.OK.value();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,27 +92,17 @@ public class WorkcaseChatContorller {
|
|||||||
@Operation(summary = "关闭聊天室")
|
@Operation(summary = "关闭聊天室")
|
||||||
@PreAuthorize("hasAuthority('workcase:room:close')")
|
@PreAuthorize("hasAuthority('workcase:room:close')")
|
||||||
@PostMapping("/room/{roomId}/close")
|
@PostMapping("/room/{roomId}/close")
|
||||||
public ResultDomain<Boolean> closeChatRoom(@PathVariable String roomId, @RequestParam String closedBy) {
|
public ResultDomain<Boolean> closeChatRoom(@PathVariable(value = "roomId") String roomId, @RequestParam String closedBy) {
|
||||||
return chatRoomService.closeChatRoom(roomId, closedBy);
|
return chatRoomService.closeChatRoom(roomId, closedBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取聊天室详情")
|
@Operation(summary = "获取聊天室详情")
|
||||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||||
@GetMapping("/room/{roomId}")
|
@GetMapping("/room/{roomId}")
|
||||||
public ResultDomain<TbChatRoomDTO> getChatRoomById(@PathVariable String roomId) {
|
public ResultDomain<TbChatRoomDTO> getChatRoomById(@PathVariable(value = "roomId") String roomId) {
|
||||||
return chatRoomService.getChatRoomById(roomId);
|
return chatRoomService.getChatRoomById(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "绑定工单到聊天室")
|
|
||||||
@PreAuthorize("hasAuthority('workcase:room:update')")
|
|
||||||
@PostMapping("/room/{roomId}/bind-workcase")
|
|
||||||
public ResultDomain<TbChatRoomDTO> bindWorkcaseToRoom(@PathVariable String roomId, @RequestParam String workcaseId) {
|
|
||||||
TbChatRoomDTO chatRoom = new TbChatRoomDTO();
|
|
||||||
chatRoom.setRoomId(roomId);
|
|
||||||
chatRoom.setWorkcaseId(workcaseId);
|
|
||||||
return chatRoomService.updateChatRoom(chatRoom);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(summary = "分页查询聊天室")
|
@Operation(summary = "分页查询聊天室")
|
||||||
@PreAuthorize("hasAuthority('workcase:room:view')")
|
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||||
@PostMapping("/room/page")
|
@PostMapping("/room/page")
|
||||||
@@ -146,14 +136,14 @@ public class WorkcaseChatContorller {
|
|||||||
@Operation(summary = "移除聊天室成员")
|
@Operation(summary = "移除聊天室成员")
|
||||||
@PreAuthorize("hasAuthority('workcase:room:member')")
|
@PreAuthorize("hasAuthority('workcase:room:member')")
|
||||||
@DeleteMapping("/room/member/{memberId}")
|
@DeleteMapping("/room/member/{memberId}")
|
||||||
public ResultDomain<Boolean> removeChatRoomMember(@PathVariable String memberId) {
|
public ResultDomain<Boolean> removeChatRoomMember(@PathVariable(value = "memberId") String memberId) {
|
||||||
return chatRoomService.removeChatRoomMember(memberId);
|
return chatRoomService.removeChatRoomMember(memberId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取聊天室成员列表")
|
@Operation(summary = "获取聊天室成员列表")
|
||||||
@PreAuthorize("hasAuthority('workcase:room:member')")
|
@PreAuthorize("hasAuthority('workcase:room:member')")
|
||||||
@GetMapping("/room/{roomId}/members")
|
@GetMapping("/room/{roomId}/members")
|
||||||
public ResultDomain<ChatMemberVO> getChatRoomMemberList(@PathVariable String roomId) {
|
public ResultDomain<ChatMemberVO> getChatRoomMemberList(@PathVariable(value = "roomId") String roomId) {
|
||||||
return chatRoomService.getChatRoomMemberList(roomId);
|
return chatRoomService.getChatRoomMemberList(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +181,7 @@ public class WorkcaseChatContorller {
|
|||||||
@Operation(summary = "删除聊天室消息")
|
@Operation(summary = "删除聊天室消息")
|
||||||
@PreAuthorize("hasAuthority('workcase:room:message')")
|
@PreAuthorize("hasAuthority('workcase:room:message')")
|
||||||
@DeleteMapping("/room/message/{messageId}")
|
@DeleteMapping("/room/message/{messageId}")
|
||||||
public ResultDomain<Boolean> deleteRoomMessage(@PathVariable String messageId) {
|
public ResultDomain<Boolean> deleteRoomMessage(@PathVariable(value = "messageId") String messageId) {
|
||||||
return chatRoomService.deleteMessage(messageId);
|
return chatRoomService.deleteMessage(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +213,7 @@ public class WorkcaseChatContorller {
|
|||||||
|
|
||||||
@Operation(summary = "删除客服人员")
|
@Operation(summary = "删除客服人员")
|
||||||
@DeleteMapping("/customer-service/{userId}")
|
@DeleteMapping("/customer-service/{userId}")
|
||||||
public ResultDomain<Boolean> deleteCustomerService(@PathVariable String userId) {
|
public ResultDomain<Boolean> deleteCustomerService(@PathVariable(value = "userId") String userId) {
|
||||||
return chatRoomService.deleteCustomerService(userId);
|
return chatRoomService.deleteCustomerService(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,8 +231,8 @@ public class WorkcaseChatContorller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "更新客服在线状态")
|
@Operation(summary = "更新客服在线状态")
|
||||||
@PostMapping("/customer-service/{userId}/status")
|
@PostMapping("/customer-service/{userId}/status/{status}")
|
||||||
public ResultDomain<Boolean> updateCustomerServiceStatus(@PathVariable String userId, @RequestParam String status) {
|
public ResultDomain<Boolean> updateCustomerServiceStatus(@PathVariable(value = "userId") String userId, @PathVariable(value = "status") String status) {
|
||||||
return chatRoomService.updateCustomerServiceStatus(userId, status);
|
return chatRoomService.updateCustomerServiceStatus(userId, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +244,7 @@ public class WorkcaseChatContorller {
|
|||||||
|
|
||||||
@Operation(summary = "自动分配客服")
|
@Operation(summary = "自动分配客服")
|
||||||
@PostMapping("/room/{roomId}/assign")
|
@PostMapping("/room/{roomId}/assign")
|
||||||
public ResultDomain<CustomerServiceVO> assignCustomerService(@PathVariable String roomId) {
|
public ResultDomain<CustomerServiceVO> assignCustomerService(@PathVariable(value = "roomId") String roomId) {
|
||||||
return chatRoomService.assignCustomerService(roomId);
|
return chatRoomService.assignCustomerService(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class WorkcaseController {
|
|||||||
@Operation(summary = "删除工单")
|
@Operation(summary = "删除工单")
|
||||||
@PreAuthorize("hasAuthority('workcase:ticket:update')")
|
@PreAuthorize("hasAuthority('workcase:ticket:update')")
|
||||||
@DeleteMapping("/{workcaseId}")
|
@DeleteMapping("/{workcaseId}")
|
||||||
public ResultDomain<TbWorkcaseDTO> deleteWorkcase(@PathVariable String workcaseId) {
|
public ResultDomain<TbWorkcaseDTO> deleteWorkcase(@PathVariable(value = "workcaseId") String workcaseId) {
|
||||||
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
|
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
|
||||||
workcase.setWorkcaseId(workcaseId);
|
workcase.setWorkcaseId(workcaseId);
|
||||||
return workcaseService.deleteWorkcase(workcase);
|
return workcaseService.deleteWorkcase(workcase);
|
||||||
@@ -81,7 +81,7 @@ public class WorkcaseController {
|
|||||||
@Operation(summary = "获取工单详情")
|
@Operation(summary = "获取工单详情")
|
||||||
@PreAuthorize("hasAuthority('workcase:ticket:view')")
|
@PreAuthorize("hasAuthority('workcase:ticket:view')")
|
||||||
@GetMapping("/{workcaseId}")
|
@GetMapping("/{workcaseId}")
|
||||||
public ResultDomain<TbWorkcaseDTO> getWorkcaseById(@PathVariable String workcaseId) {
|
public ResultDomain<TbWorkcaseDTO> getWorkcaseById(@PathVariable(value = "workcaseId") String workcaseId) {
|
||||||
return workcaseService.getWorkcaseById(workcaseId);
|
return workcaseService.getWorkcaseById(workcaseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ public class WorkcaseController {
|
|||||||
@Operation(summary = "删除工单处理过程")
|
@Operation(summary = "删除工单处理过程")
|
||||||
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
@PreAuthorize("hasAuthority('workcase:ticket:process')")
|
||||||
@DeleteMapping("/process/{processId}")
|
@DeleteMapping("/process/{processId}")
|
||||||
public ResultDomain<TbWorkcaseProcessDTO> deleteWorkcaseProcess(@PathVariable String processId) {
|
public ResultDomain<TbWorkcaseProcessDTO> deleteWorkcaseProcess(@PathVariable(value = "processId") String processId) {
|
||||||
TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO();
|
TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO();
|
||||||
process.setProcessId(processId);
|
process.setProcessId(processId);
|
||||||
return workcaseService.deleteWorkcaseProcess(process);
|
return workcaseService.deleteWorkcaseProcess(process);
|
||||||
@@ -219,7 +219,7 @@ public class WorkcaseController {
|
|||||||
@Operation(summary = "删除工单设备")
|
@Operation(summary = "删除工单设备")
|
||||||
@PreAuthorize("hasAuthority('workcase:ticket:device')")
|
@PreAuthorize("hasAuthority('workcase:ticket:device')")
|
||||||
@DeleteMapping("/device/{workcaseId}/{device}")
|
@DeleteMapping("/device/{workcaseId}/{device}")
|
||||||
public ResultDomain<TbWorkcaseDeviceDTO> deleteWorkcaseDevice(@PathVariable String workcaseId, @PathVariable String device) {
|
public ResultDomain<TbWorkcaseDeviceDTO> deleteWorkcaseDevice(@PathVariable(value = "workcaseId") String workcaseId, @PathVariable(value = "device") String device) {
|
||||||
TbWorkcaseDeviceDTO deviceDTO = new TbWorkcaseDeviceDTO();
|
TbWorkcaseDeviceDTO deviceDTO = new TbWorkcaseDeviceDTO();
|
||||||
deviceDTO.setWorkcaseId(workcaseId);
|
deviceDTO.setWorkcaseId(workcaseId);
|
||||||
deviceDTO.setDevice(device);
|
deviceDTO.setDevice(device);
|
||||||
|
|||||||
@@ -558,14 +558,7 @@ public class ChatRoomServiceImpl implements ChatRoomService {
|
|||||||
filter.setStatus("online");
|
filter.setStatus("online");
|
||||||
List<CustomerServiceVO> list = customerServiceMapper.selectCustomerServiceList(filter);
|
List<CustomerServiceVO> list = customerServiceMapper.selectCustomerServiceList(filter);
|
||||||
|
|
||||||
// 过滤工作量未满的客服
|
return ResultDomain.success("查询成功", list);
|
||||||
List<CustomerServiceVO> availableList = list.stream()
|
|
||||||
.filter(cs -> cs.getCurrentWorkload() == null ||
|
|
||||||
cs.getMaxConcurrent() == null ||
|
|
||||||
cs.getCurrentWorkload() < cs.getMaxConcurrent())
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return ResultDomain.success("查询成功", availableList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -156,9 +156,7 @@ export const workcaseChatAPI = {
|
|||||||
* 更新客服在线状态
|
* 更新客服在线状态
|
||||||
*/
|
*/
|
||||||
async updateCustomerServiceStatus(userId: string, status: string): Promise<ResultDomain<boolean>> {
|
async updateCustomerServiceStatus(userId: string, status: string): Promise<ResultDomain<boolean>> {
|
||||||
const response = await api.post<boolean>(`${this.baseUrl}/customer-service/${userId}/status`, null, {
|
const response = await api.post<boolean>(`${this.baseUrl}/customer-service/${userId}/status/${status}`)
|
||||||
params: { status }
|
|
||||||
})
|
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -45,170 +45,6 @@ const BASE_URL = 'http://localhost:8180'
|
|||||||
export const workcaseChatAPI = {
|
export const workcaseChatAPI = {
|
||||||
baseUrl: '/urban-lifeline/workcase/chat',
|
baseUrl: '/urban-lifeline/workcase/chat',
|
||||||
|
|
||||||
// ====================== AI对话管理 ======================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建对话
|
|
||||||
* @param param agentId和userId必传
|
|
||||||
*/
|
|
||||||
createChat(param: CreateChatParam): Promise<ResultDomain<TbChat>> {
|
|
||||||
return request<TbChat>({ url: this.baseUrl, method: 'POST', data: param })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新对话
|
|
||||||
*/
|
|
||||||
updateChat(chat: TbChat): Promise<ResultDomain<TbChat>> {
|
|
||||||
return request<TbChat>({ url: this.baseUrl, method: 'PUT', data: chat })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询对话列表
|
|
||||||
* @param param userId必传
|
|
||||||
*/
|
|
||||||
getChatList(param: ChatListParam): Promise<ResultDomain<TbChat[]>> {
|
|
||||||
return request<TbChat[]>({ url: `${this.baseUrl}/list`, method: 'POST', data: param })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取对话消息列表
|
|
||||||
* @param param chatId必传
|
|
||||||
*/
|
|
||||||
getChatMessageList(param: ChatMessageListParam): Promise<ResultDomain<TbChatMessage[]>> {
|
|
||||||
return request<TbChatMessage[]>({ url: `${this.baseUrl}/message/list`, method: 'POST', data: param })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 准备流式对话会话
|
|
||||||
* @param param chatId和message必传
|
|
||||||
*/
|
|
||||||
prepareChatMessageSession(param: PrepareChatParam): Promise<ResultDomain<string>> {
|
|
||||||
return request<string>({ url: `${this.baseUrl}/prepare`, method: 'POST', data: param })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 流式对话(SSE)- 返回EventSource URL
|
|
||||||
*/
|
|
||||||
getStreamUrl(sessionId: string): string {
|
|
||||||
return `${this.baseUrl}/stream/${sessionId}`
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 建立SSE流式对话连接
|
|
||||||
* @param sessionId 会话ID(必传)
|
|
||||||
* @param callbacks 回调函数
|
|
||||||
* @returns SSETask 可用于中止请求
|
|
||||||
*/
|
|
||||||
streamChat(sessionId: string, callbacks: SSECallbacks): SSETask {
|
|
||||||
const url = `${BASE_URL}${this.baseUrl}/stream/${sessionId}`
|
|
||||||
const token = uni.getStorageSync('token') || ''
|
|
||||||
|
|
||||||
const requestTask = uni.request({
|
|
||||||
url: url,
|
|
||||||
method: 'GET',
|
|
||||||
header: {
|
|
||||||
'Accept': 'text/event-stream',
|
|
||||||
'Authorization': token ? `Bearer ${token}` : ''
|
|
||||||
},
|
|
||||||
enableChunked: true,
|
|
||||||
success: (res: any) => {
|
|
||||||
console.log('SSE请求完成:', res)
|
|
||||||
// 处理非200状态码
|
|
||||||
if (res.statusCode !== 200) {
|
|
||||||
console.error('SSE请求状态码异常:', res.statusCode)
|
|
||||||
let errorMsg = '抱歉,服务暂时不可用,请稍后重试。'
|
|
||||||
if (res.statusCode === 401) {
|
|
||||||
errorMsg = '登录已过期,请重新登录。'
|
|
||||||
} else if (res.statusCode === 403) {
|
|
||||||
errorMsg = '无权限访问,请联系管理员。'
|
|
||||||
} else if (res.statusCode === 404) {
|
|
||||||
errorMsg = '会话不存在或已过期,请重新发起对话。'
|
|
||||||
} else if (res.statusCode >= 500) {
|
|
||||||
errorMsg = '服务器异常,请稍后重试。'
|
|
||||||
}
|
|
||||||
callbacks.onError?.(errorMsg)
|
|
||||||
}
|
|
||||||
callbacks.onComplete?.()
|
|
||||||
},
|
|
||||||
fail: (err: any) => {
|
|
||||||
console.error('SSE请求失败:', err)
|
|
||||||
callbacks.onError?.('网络连接失败,请稍后重试。')
|
|
||||||
callbacks.onComplete?.()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听分块数据
|
|
||||||
requestTask.onChunkReceived((res: any) => {
|
|
||||||
try {
|
|
||||||
const decoder = new TextDecoder('utf-8')
|
|
||||||
const text = decoder.decode(new Uint8Array(res.data))
|
|
||||||
|
|
||||||
const lines = text.split('\n')
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith('data:')) {
|
|
||||||
const dataStr = line.substring(5).trim()
|
|
||||||
if (dataStr && dataStr !== '[DONE]') {
|
|
||||||
try {
|
|
||||||
const data: SSEMessageData = JSON.parse(dataStr)
|
|
||||||
const event = data.event
|
|
||||||
|
|
||||||
if (event === 'message' || event === 'agent_message') {
|
|
||||||
callbacks.onMessage?.(data)
|
|
||||||
} else if (event === 'message_end') {
|
|
||||||
callbacks.onEnd?.(data.task_id || '')
|
|
||||||
} else if (event === 'error') {
|
|
||||||
callbacks.onError?.(data.message || '发生错误,请稍后重试。')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('解析SSE数据失败:', dataStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('处理分块数据失败:', e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
abort: () => {
|
|
||||||
requestTask.abort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止对话
|
|
||||||
* @param param taskId, agentId, userId必传
|
|
||||||
*/
|
|
||||||
stopChat(param: StopChatParam): Promise<ResultDomain<boolean>> {
|
|
||||||
return request<boolean>({ url: `${this.baseUrl}/stop/${param.taskId}`, method: 'POST', data: param })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 评价对话消息
|
|
||||||
* @param param agentId, chatId, messageId, comment, userId必传
|
|
||||||
*/
|
|
||||||
commentChatMessage(param: CommentMessageParam): Promise<ResultDomain<boolean>> {
|
|
||||||
return request<boolean>({ url: `${this.baseUrl}/comment?messageId=${param.messageId}&comment=${param.comment}`, method: 'POST', data: param })
|
|
||||||
},
|
|
||||||
|
|
||||||
// ====================== 对话分析 ======================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分析对话(AI预填工单信息)
|
|
||||||
*/
|
|
||||||
analyzeChat(chatId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
|
|
||||||
return request<TbWorkcaseDTO>({ url: `${this.baseUrl}/analyze/${chatId}`, method: 'GET' })
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 总结对话
|
|
||||||
*/
|
|
||||||
summaryChat(chatId: string): Promise<ResultDomain<TbWorkcaseDTO>> {
|
|
||||||
return request<TbWorkcaseDTO>({ url: `${this.baseUrl}/summary/${chatId}`, method: 'POST' })
|
|
||||||
},
|
|
||||||
|
|
||||||
// ====================== ChatRoom聊天室管理 ======================
|
// ====================== ChatRoom聊天室管理 ======================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,7 +162,7 @@ export const workcaseChatAPI = {
|
|||||||
* 更新客服在线状态
|
* 更新客服在线状态
|
||||||
*/
|
*/
|
||||||
updateCustomerServiceStatus(userId: string, status: string): Promise<ResultDomain<boolean>> {
|
updateCustomerServiceStatus(userId: string, status: string): Promise<ResultDomain<boolean>> {
|
||||||
return request<boolean>({ url: `${this.baseUrl}/customer-service/${userId}/status?status=${status}`, method: 'POST' })
|
return request<boolean>({ url: `${this.baseUrl}/customer-service/${userId}/status/${status}`, method: 'POST' })
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -238,11 +238,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
background-color: #5B8FF9;
|
background-color: #5B8FF9;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn[disabled] {
|
.submit-btn.is-disabled {
|
||||||
background-color: #CCCCCC;
|
background-color: #CCCCCC;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
<view class="creator-footer">
|
<view class="creator-footer">
|
||||||
<button class="cancel-btn" @tap="onCancel">取消</button>
|
<button class="cancel-btn" @tap="onCancel">取消</button>
|
||||||
<button class="submit-btn" @tap="onSubmit" :disabled="!canSubmit">提交工单</button>
|
<button class="submit-btn" :class="{ 'is-disabled': !canSubmit }" @tap="onSubmit" :disabled="!canSubmit">提交工单</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -58,9 +58,9 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
// 小程序需要为右侧胶囊按钮留出空间
|
// 小程序需要为右侧胶囊按钮留出空间
|
||||||
/* #ifdef MP-WEIXIN */
|
// /* #ifdef MP-WEIXIN */
|
||||||
padding-right: 100px;
|
// padding-right: 100px;
|
||||||
/* #endif */
|
// /* #endif */
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btn {
|
.nav-btn {
|
||||||
@@ -100,11 +100,185 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-area {
|
// 工具栏(人员数量 + 按钮)
|
||||||
|
.room-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 44px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 99;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-count {
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border: 2rpx solid #ff4d4f;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-count-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8rpx 24rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
border: 2rpx solid #ff4d4f;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.meeting-btn {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.meeting-btn .meeting-text {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 人员弹窗
|
||||||
|
.members-popup-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-popup {
|
||||||
|
position: absolute;
|
||||||
|
left: 24rpx;
|
||||||
|
width: 300rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 16rpx 0;
|
||||||
|
max-height: 400rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(145deg, #e8f7ff 0%, #c5e4ff 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #173294;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-top: 176rpx;
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-status {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-status.online {
|
||||||
|
background: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-status.offline {
|
||||||
|
background: #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-empty {
|
||||||
|
padding: 32rpx 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.members-empty-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more {
|
||||||
|
padding: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-area {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 120rpx;
|
||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
padding-bottom: 180rpx;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-list {
|
.message-list {
|
||||||
|
|||||||
@@ -9,21 +9,52 @@
|
|||||||
<view class="nav-back-icon"></view>
|
<view class="nav-back-icon"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="nav-title">{{ roomName }}</text>
|
<text class="nav-title">{{ roomName }}</text>
|
||||||
<view class="nav-right">
|
</view>
|
||||||
<button class="nav-btn" @tap="handleWorkcaseAction">
|
<!-- 聊天室人员和操作 -->
|
||||||
<text class="nav-btn-text">{{ workcaseId ? '查看工单' : '创建工单' }}</text>
|
<view class="room-toolbar" :style="{ top: headerPaddingTop + 44 + 'px' }">
|
||||||
|
<view class="member-count" @tap="showMembers = !showMembers">
|
||||||
|
<text class="member-count-text">{{ totalMembers.length > 0 ? totalMembers.length + ' 人在线' : '暂无人员' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="toolbar-right">
|
||||||
|
<button class="toolbar-btn" @tap="handleWorkcaseAction">
|
||||||
|
<text class="toolbar-btn-text">{{ workcaseId ? '查看工单' : '创建工单' }}</text>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn meeting-btn" @tap="startMeeting">
|
<button class="toolbar-btn meeting-btn" @tap="startMeeting">
|
||||||
<text class="nav-btn-text meeting-text">发起会议</text>
|
<text class="toolbar-btn-text meeting-text">发起会议</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<!-- 弹窗显示人员列表和在线情况 -->
|
||||||
|
<view v-if="showMembers" class="members-popup-mask" @tap="showMembers = false">
|
||||||
|
<view class="members-popup" :style="{ top: headerPaddingTop + 88 + 'px' }" @tap.stop>
|
||||||
|
<view class="members-list">
|
||||||
|
<view v-if="totalMembers.length === 0" class="members-empty">
|
||||||
|
<text class="members-empty-text">暂无人员在线</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="member-item" v-for="member in totalMembers" :key="member.oderId">
|
||||||
|
<view class="member-avatar">
|
||||||
|
<text class="member-avatar-text">{{ member.userName?.charAt(0) || '客' }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="member-name">{{ member.userName || '未知' }}</text>
|
||||||
|
<view class="member-status" :class="member.isOnline ? 'online' : 'offline'"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<!-- 聊天消息区域 -->
|
<!-- 聊天消息区域 -->
|
||||||
<scroll-view class="chat-area" scroll-y="true" :scroll-top="scrollTop"
|
<scroll-view class="chat-area" scroll-y="true" :scroll-top="scrollTop"
|
||||||
:style="{ marginTop: headerTotalHeight + 'px' }">
|
:style="{ top: (headerPaddingTop + 88) + 'px' }"
|
||||||
|
@scrolltoupper="loadMoreMessages"
|
||||||
|
upper-threshold="50">
|
||||||
|
<!-- 加载更多提示 -->
|
||||||
|
<view v-if="loadingMore" class="loading-more">
|
||||||
|
<text class="loading-more-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!hasMore" class="loading-more">
|
||||||
|
<text class="loading-more-text">没有更多消息了</text>
|
||||||
|
</view>
|
||||||
<view class="message-list">
|
<view class="message-list">
|
||||||
<view class="message-item" v-for="(msg, index) in messages" :key="index"
|
<view class="message-item" v-for="msg in messages" :key="msg.messageId"
|
||||||
:class="msg.senderType === 'guest' ? 'self' : 'other'">
|
:class="msg.senderType === 'guest' ? 'self' : 'other'">
|
||||||
<!-- 对方消息(左侧) -->
|
<!-- 对方消息(左侧) -->
|
||||||
<view class="message-row other-row" v-if="msg.senderType !== 'guest'">
|
<view class="message-row other-row" v-if="msg.senderType !== 'guest'">
|
||||||
@@ -77,9 +108,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, onMounted } from 'vue'
|
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
|
||||||
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
|
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
|
||||||
import type { ChatRoomMessageVO } from '@/types/workcase'
|
import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO } from '@/types/workcase'
|
||||||
|
import { workcaseChatAPI } from '@/api/workcase'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const headerPaddingTop = ref<number>(44)
|
const headerPaddingTop = ref<number>(44)
|
||||||
@@ -90,55 +122,82 @@ const roomName = ref<string>('聊天室')
|
|||||||
const inputText = ref<string>('')
|
const inputText = ref<string>('')
|
||||||
const scrollTop = ref<number>(0)
|
const scrollTop = ref<number>(0)
|
||||||
const showWorkcaseCreator = ref<boolean>(false)
|
const showWorkcaseCreator = ref<boolean>(false)
|
||||||
|
const loading = ref<boolean>(false)
|
||||||
|
const sending = ref<boolean>(false)
|
||||||
|
const loadingMore = ref<boolean>(false)
|
||||||
|
const currentPage = ref<number>(1)
|
||||||
|
const hasMore = ref<boolean>(true)
|
||||||
|
|
||||||
|
// 用户信息(从storage获取)
|
||||||
|
const currentUserId = ref<string>('')
|
||||||
|
const currentUserName = ref<string>('我')
|
||||||
|
|
||||||
|
function loadUserInfo() {
|
||||||
|
try {
|
||||||
|
const userInfo = uni.getStorageSync('userInfo')
|
||||||
|
if (userInfo) {
|
||||||
|
const user = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
|
||||||
|
currentUserId.value = user.userId || user.id || ''
|
||||||
|
currentUserName.value = user.username || user.nickName || '我'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取用户信息失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 消息列表
|
// 消息列表
|
||||||
const messages = ref<ChatRoomMessageVO[]>([
|
const messages = reactive<ChatRoomMessageVO[]>([])
|
||||||
{
|
|
||||||
messageId: '1',
|
// 所有默认客服
|
||||||
roomId: 'room001',
|
const defaultWorkers = reactive<CustomerVO[]>([])
|
||||||
senderId: 'agent001',
|
async function loadDefaultWorkers() {
|
||||||
senderType: 'agent',
|
const res = await workcaseChatAPI.getAvailableCustomerServices()
|
||||||
senderName: '客服小张',
|
if(res.success && res.dataList) {
|
||||||
content: '您好,我是客服小张,请问有什么可以帮助您的?',
|
defaultWorkers.splice(0, defaultWorkers.length, ...res.dataList)
|
||||||
sendTime: '2024-12-17 16:00:00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '2',
|
|
||||||
roomId: 'room001',
|
|
||||||
senderId: 'guest001',
|
|
||||||
senderType: 'guest',
|
|
||||||
senderName: '李经理',
|
|
||||||
content: '我们的设备出现了控制系统故障,无法正常启动',
|
|
||||||
sendTime: '2024-12-17 16:02:00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '3',
|
|
||||||
roomId: 'room001',
|
|
||||||
senderId: 'agent001',
|
|
||||||
senderType: 'agent',
|
|
||||||
senderName: '客服小张',
|
|
||||||
content: '好的,请问是哪个型号的设备?能否提供一下设备序列号?',
|
|
||||||
sendTime: '2024-12-17 16:03:00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '4',
|
|
||||||
roomId: 'room001',
|
|
||||||
senderId: 'guest001',
|
|
||||||
senderType: 'guest',
|
|
||||||
senderName: '李经理',
|
|
||||||
content: '型号是TH-500GF,序列号是TH20230501001',
|
|
||||||
sendTime: '2024-12-17 16:05:00'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: '5',
|
|
||||||
roomId: 'room001',
|
|
||||||
senderId: 'agent001',
|
|
||||||
senderType: 'agent',
|
|
||||||
senderName: '客服小张',
|
|
||||||
content: '好的,我已经记录了您的问题。建议您创建一个工单,我们会安排工程师尽快上门处理。',
|
|
||||||
sendTime: '2024-12-17 16:08:00'
|
|
||||||
}
|
}
|
||||||
])
|
}
|
||||||
|
// 查询进入聊天室的人员, 包含了访客
|
||||||
|
const chatMembers = reactive<ChatMemberVO[]>([])
|
||||||
|
async function loadChatMembers() {
|
||||||
|
const res = await workcaseChatAPI.getChatRoomMemberList(roomId.value)
|
||||||
|
if(res.success && res.dataList) {
|
||||||
|
chatMembers.splice(0, chatMembers.length, ...res.dataList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const showMembers = ref(false)
|
||||||
|
|
||||||
|
// 计算聊天室人数: 默认客服转成ChatMemberVO + 进入聊天室的人员,去重,进入聊天室的人员是在线状态
|
||||||
|
interface MemberDisplay {
|
||||||
|
oderId: string
|
||||||
|
userId: string
|
||||||
|
userName: string
|
||||||
|
isOnline: boolean
|
||||||
|
}
|
||||||
|
const totalMembers = computed<MemberDisplay[]>(() => {
|
||||||
|
const memberMap = new Map<string, MemberDisplay>()
|
||||||
|
|
||||||
|
// 先添加默认客服(离线状态)
|
||||||
|
defaultWorkers.forEach((worker, index) => {
|
||||||
|
memberMap.set(worker.userId || '', {
|
||||||
|
oderId: `worker-${index}`,
|
||||||
|
userId: worker.userId || '',
|
||||||
|
userName: worker.username || '客服',
|
||||||
|
isOnline: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 再添加聊天室成员(在线状态),覆盖同一用户
|
||||||
|
chatMembers.forEach((member, index) => {
|
||||||
|
memberMap.set(member.userId || '', {
|
||||||
|
oderId: member.memberId || `member-${index}`,
|
||||||
|
userId: member.userId || '',
|
||||||
|
userName: member.userName || '未知',
|
||||||
|
isOnline: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(memberMap.values())
|
||||||
|
})
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -169,15 +228,107 @@ onMounted(() => {
|
|||||||
workcaseId.value = currentPage.options.workcaseId || ''
|
workcaseId.value = currentPage.options.workcaseId || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadUserInfo()
|
||||||
loadChatRoom()
|
loadChatRoom()
|
||||||
scrollToBottom()
|
loadDefaultWorkers()
|
||||||
|
loadChatMembers()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 加载聊天室
|
// 加载聊天室
|
||||||
function loadChatRoom() {
|
const PAGE_SIZE = 5
|
||||||
console.log('加载聊天室:', roomId.value)
|
const messageTotal = ref<number>(0)
|
||||||
// TODO: 调用 workcaseChatAPI.getChatRoomById() 获取聊天室信息
|
|
||||||
// TODO: 调用 workcaseChatAPI.getChatMessagePage() 获取消息列表
|
async function loadChatRoom() {
|
||||||
|
if (!roomId.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 获取聊天室信息
|
||||||
|
const roomRes = await workcaseChatAPI.getChatRoomById(roomId.value)
|
||||||
|
if (roomRes.success && roomRes.data) {
|
||||||
|
roomName.value = roomRes.data.roomName || '聊天室'
|
||||||
|
workcaseId.value = roomRes.data.workcaseId || ''
|
||||||
|
messageTotal.value = roomRes.data.messageCount || 0
|
||||||
|
}
|
||||||
|
// 后端是降序查询,page1是最新消息
|
||||||
|
currentPage.value = 1
|
||||||
|
hasMore.value = true // 默认有更多,loadMessages中会根据实际情况更新
|
||||||
|
// 获取消息列表
|
||||||
|
await loadMessages()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载聊天室失败:', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载消息列表(后端降序,page1是最新消息,需要反转显示)
|
||||||
|
async function loadMessages() {
|
||||||
|
console.log('[loadMessages] 开始加载, currentPage:', currentPage.value, 'roomId:', roomId.value)
|
||||||
|
if (!roomId.value) return
|
||||||
|
try {
|
||||||
|
const msgRes = await workcaseChatAPI.getChatMessagePage({
|
||||||
|
filter: { roomId: roomId.value },
|
||||||
|
pageParam: { page: currentPage.value, pageSize: PAGE_SIZE }
|
||||||
|
})
|
||||||
|
console.log('[loadMessages] 响应:', msgRes)
|
||||||
|
if (msgRes.success && msgRes.dataList) {
|
||||||
|
const pageInfo = msgRes.pageDomain?.pageParam
|
||||||
|
const actualTotalPages = pageInfo?.totalPages || 1
|
||||||
|
hasMore.value = actualTotalPages > currentPage.value
|
||||||
|
console.log('[loadMessages] pageInfo:', pageInfo, 'actualTotalPages:', actualTotalPages, 'hasMore:', hasMore.value)
|
||||||
|
|
||||||
|
// 后端降序返回,需要反转后显示(早的在上,新的在下)
|
||||||
|
const reversedList = [...msgRes.dataList].reverse()
|
||||||
|
messages.splice(0, messages.length, ...reversedList)
|
||||||
|
console.log('[loadMessages] 加载完成, 消息数:', messages.length)
|
||||||
|
nextTick(() => scrollToBottom())
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载消息列表失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载更多历史消息(滚动到顶部触发,加载下一页更早的消息)
|
||||||
|
async function loadMoreMessages() {
|
||||||
|
console.log('[loadMoreMessages] 触发, roomId:', roomId.value, 'loadingMore:', loadingMore.value, 'hasMore:', hasMore.value, 'currentPage:', currentPage.value)
|
||||||
|
if (!roomId.value || loadingMore.value || !hasMore.value) {
|
||||||
|
console.log('[loadMoreMessages] 跳过加载 - roomId:', !roomId.value, 'loadingMore:', loadingMore.value, '!hasMore:', !hasMore.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载下一页(更早的消息)
|
||||||
|
const nextPage = currentPage.value + 1
|
||||||
|
console.log('[loadMoreMessages] 准备加载页:', nextPage)
|
||||||
|
|
||||||
|
loadingMore.value = true
|
||||||
|
try {
|
||||||
|
const msgRes = await workcaseChatAPI.getChatMessagePage({
|
||||||
|
filter: { roomId: roomId.value },
|
||||||
|
pageParam: { page: nextPage, pageSize: PAGE_SIZE }
|
||||||
|
})
|
||||||
|
console.log('[loadMoreMessages] 响应:', msgRes)
|
||||||
|
if (msgRes.success && msgRes.dataList && msgRes.dataList.length > 0) {
|
||||||
|
const pageInfo = msgRes.pageDomain?.pageParam
|
||||||
|
const actualTotalPages = pageInfo?.totalPages || 1
|
||||||
|
console.log('[loadMoreMessages] pageInfo:', pageInfo, 'actualTotalPages:', actualTotalPages)
|
||||||
|
|
||||||
|
currentPage.value = nextPage
|
||||||
|
hasMore.value = actualTotalPages > currentPage.value
|
||||||
|
console.log('[loadMoreMessages] 更新后 currentPage:', currentPage.value, 'hasMore:', hasMore.value)
|
||||||
|
|
||||||
|
// 后端降序返回,反转后插入到列表前面
|
||||||
|
const reversedList = [...msgRes.dataList].reverse()
|
||||||
|
messages.unshift(...reversedList)
|
||||||
|
console.log('[loadMoreMessages] 加载完成, 消息数:', messages.length)
|
||||||
|
} else {
|
||||||
|
console.log('[loadMoreMessages] 没有更多数据')
|
||||||
|
hasMore.value = false
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载更多消息失败:', e)
|
||||||
|
} finally {
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化时间(兼容 iOS)
|
// 格式化时间(兼容 iOS)
|
||||||
@@ -191,28 +342,63 @@ function formatTime(time?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
function sendMessage() {
|
async function sendMessage() {
|
||||||
const text = inputText.value.trim()
|
const text = inputText.value.trim()
|
||||||
if (!text) return
|
if (!text || sending.value) return
|
||||||
|
|
||||||
const newMsg: ChatRoomMessageVO = {
|
sending.value = true
|
||||||
messageId: Date.now().toString(),
|
const tempId = Date.now().toString()
|
||||||
|
|
||||||
|
// 先添加临时消息到界面
|
||||||
|
const tempMsg: ChatRoomMessageVO = {
|
||||||
|
messageId: tempId,
|
||||||
roomId: roomId.value,
|
roomId: roomId.value,
|
||||||
senderId: 'guest001',
|
senderId: currentUserId.value,
|
||||||
senderType: 'guest',
|
senderType: 'guest',
|
||||||
senderName: '我',
|
senderName: currentUserName.value,
|
||||||
content: text,
|
content: text,
|
||||||
sendTime: new Date().toISOString()
|
sendTime: new Date().toISOString(),
|
||||||
|
status: 'sending'
|
||||||
}
|
}
|
||||||
|
messages.push(tempMsg)
|
||||||
messages.value.push(newMsg)
|
|
||||||
inputText.value = ''
|
inputText.value = ''
|
||||||
|
nextTick(() => scrollToBottom())
|
||||||
|
|
||||||
nextTick(() => {
|
try {
|
||||||
scrollToBottom()
|
// 调用API发送消息
|
||||||
})
|
const msgDTO: TbChatRoomMessageDTO = {
|
||||||
|
roomId: roomId.value,
|
||||||
// TODO: 调用 workcaseChatAPI.sendMessage() 发送消息
|
senderId: currentUserId.value,
|
||||||
|
senderType: 'guest',
|
||||||
|
senderName: currentUserName.value,
|
||||||
|
messageType: 'text',
|
||||||
|
content: text
|
||||||
|
}
|
||||||
|
const res = await workcaseChatAPI.sendMessage(msgDTO)
|
||||||
|
if (res.success && res.data) {
|
||||||
|
// 更新临时消息为真实消息
|
||||||
|
const idx = messages.findIndex(m => m.messageId === tempId)
|
||||||
|
if (idx !== -1) {
|
||||||
|
messages[idx] = { ...res.data, status: 'sent' }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 发送失败,标记状态
|
||||||
|
const idx = messages.findIndex(m => m.messageId === tempId)
|
||||||
|
if (idx !== -1) {
|
||||||
|
messages[idx].status = 'failed'
|
||||||
|
}
|
||||||
|
uni.showToast({ title: res.message || '发送失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('发送消息失败:', e)
|
||||||
|
const idx = messages.findIndex(m => m.messageId === tempId)
|
||||||
|
if (idx !== -1) {
|
||||||
|
messages[idx].status = 'failed'
|
||||||
|
}
|
||||||
|
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user