聊天判断修正

This commit is contained in:
2025-12-28 16:47:38 +08:00
parent 8448a801ce
commit 579e63efb0
6 changed files with 246 additions and 159 deletions

View File

@@ -68,7 +68,7 @@ public class ChatController {
chat.setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
chat.setUserType(true);
}
}
@@ -97,7 +97,7 @@ public class ChatController {
chat.setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
chat.setUserType(true);
}
}
@@ -116,7 +116,7 @@ public class ChatController {
chat.setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
chat.setUserType(true);
}
}
@@ -137,7 +137,7 @@ public class ChatController {
filter.setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
filter.setUserType(true);
}
}
@@ -157,7 +157,7 @@ public class ChatController {
pageRequest.getFilter().setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
pageRequest.getFilter().setUserType(true);
}
}
@@ -183,7 +183,7 @@ public class ChatController {
filter.setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
filter.setUserType(true);
}
}
@@ -214,7 +214,7 @@ public class ChatController {
chatPrepareData.setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
chatPrepareData.setUserType(true);
}
}
@@ -267,7 +267,7 @@ public class ChatController {
filter.setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
filter.setUserType(true);
}
}
@@ -300,7 +300,7 @@ public class ChatController {
filter.setUserType(false);
if(NonUtils.isNotEmpty(token)){
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if (NonUtils.isNotEmpty(loginDomain) && loginDomain.getUser().getStatus()!="guest") {
if (NonUtils.isNotEmpty(loginDomain) && !"guest".equals(loginDomain.getUser().getStatus())) {
filter.setUserType(true);
}
}

View File

@@ -131,7 +131,7 @@ public class WorkcaseChatContorller {
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
String userId = loginDomain.getUser().getUserId();
if("guest".equals(loginDomain.getUser().getStatus())){
if(!"guest".equals(loginDomain.getUser().getStatus())){
pageRequest.getFilter().setGuestId(userId);
}
return chatRoomService.getChatRoomPage(pageRequest, userId);

View File

@@ -94,7 +94,7 @@ public class WorkcaseController {
@PostMapping("/list")
public ResultDomain<TbWorkcaseDTO> getWorkcaseList(@RequestBody TbWorkcaseDTO filter) {
LoginDomain loginDomain = LoginUtil.getCurrentLogin();
if ("guest".equals(loginDomain.getUser().getStatus())) {
if (!"guest".equals(loginDomain.getUser().getStatus())) {
filter.setUserId(loginDomain.getUser().getUserId());
}
return workcaseService.getWorkcaseList(filter);

View File

@@ -110,7 +110,7 @@ public class VideoMeetingServiceImpl implements VideoMeetingService {
List<ChatMemberVO> members = chatRoomMemberMapper.selectChatRoomMemberList(memberFilter);
String userName = loginDomain.getUserInfo().getUsername();
String userType = "guest".equals(loginDomain.getUser().getStatus())?"guest":"user";
String userType = !"guest".equals(loginDomain.getUser().getStatus())?"guest":"user";
if (members != null && !members.isEmpty()) {
ChatMemberVO member = members.get(0);
userName = member.getUserName();

View File

@@ -15,160 +15,172 @@
<div class="filter-right">
<el-select v-model="statusFilter" placeholder="对话状态" clearable style="width: 120px;">
<el-option label="进行中" value="ongoing" />
<el-option label="已结束" value="ended" />
<el-option label="已转工单" value="converted" />
<el-option label="进行中" value="active" />
<el-option label="已结束" value="closed" />
</el-select>
<el-select v-model="satisfactionFilter" placeholder="满意度" clearable style="width: 120px;">
<el-option label="满意" value="satisfied" />
<el-option label="一般" value="normal" />
<el-option label="不满意" value="unsatisfied" />
</el-select>
<el-input v-model="searchKeyword" placeholder="搜索客户/内容" style="width: 200px;" :prefix-icon="Search" clearable />
<el-input v-model="searchKeyword" placeholder="搜索客户/聊天室" style="width: 200px;" :prefix-icon="Search" clearable @keyup.enter="loadChatRooms" />
<el-button type="primary" @click="loadChatRooms">搜索</el-button>
</div>
</div>
</el-card>
<!-- 对话列表 -->
<el-card>
<el-table :data="filteredChats" style="width: 100%">
<el-table-column prop="chatId" label="对话ID" width="140">
<el-card v-loading="loading">
<el-table :data="chatRooms" style="width: 100%">
<el-table-column prop="roomId" label="聊天室ID" width="180">
<template #default="{ row }">
<span style="color: #409eff; font-weight: 500;">{{ row.chatId }}</span>
<span style="color: #409eff; font-weight: 500;">{{ row.roomId?.substring(0, 8) }}...</span>
</template>
</el-table-column>
<el-table-column prop="customerName" label="客户信息" width="150">
<el-table-column prop="roomName" label="聊天室名称" width="180" />
<el-table-column prop="guestName" label="客户" width="120" />
<el-table-column prop="workcaseId" label="关联工单" width="140">
<template #default="{ row }">
<div class="customer-info">
<span class="name">{{ row.customerName }}</span>
<span class="phone">{{ row.customerPhone }}</span>
</div>
<span v-if="row.workcaseId" style="color: #67c23a;">{{ row.workcaseId }}</span>
<span v-else style="color: #909399;">-</span>
</template>
</el-table-column>
<el-table-column prop="agentName" label="客服人员" width="100" />
<el-table-column prop="messageCount" label="消息数" width="80" align="center" />
<el-table-column prop="duration" label="对话时长" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ row.statusName }}
{{ getStatusName(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="satisfaction" label="满意度" width="100">
<template #default="{ row }">
<el-rate v-model="row.satisfaction" disabled allow-half />
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="160" />
<el-table-column label="操作" width="200" fixed="right">
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="viewChat(row)">查看</el-button>
<el-button type="success" link size="small" @click="downloadChat(row)">下载</el-button>
<el-button v-if="row.status === 'ongoing'" type="warning" link size="small" @click="endChat(row)">结束</el-button>
<el-button v-if="row.status === 'active'" type="warning" link size="small" @click="closeChat(row)">关闭</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="chats.length" layout="total, prev, pager, next" />
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="loadChatRooms"
/>
</div>
</el-card>
</div>
<!-- 查看对话详情弹窗 -->
<el-dialog v-model="showChatDialog" title="对话详情" width="700px">
<div class="chat-messages">
<div v-for="(msg, idx) in currentChatMessages" :key="idx" class="message-item" :class="msg.type">
<div class="message-header">
<span class="sender">{{ msg.sender }}</span>
<span class="time">{{ msg.time }}</span>
</div>
<div class="message-content">{{ msg.content }}</div>
</div>
</div>
<!-- 查看对话详情弹窗 - 复用 ChatMessage 组件 -->
<el-dialog v-model="showChatDialog" title="对话详情" width="800px" class="chat-dialog">
<ChatMessage v-if="showChatDialog && currentRoomId" :room-id="currentRoomId" :chat-room="currentChatRoom" />
</el-dialog>
</AdminLayout>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, onMounted } from 'vue'
import AdminLayout from '@/views/admin/AdminLayout.vue'
import { ChatMessage } from '@/views/public/ChatRoom/'
import { Download, Search } from 'lucide-vue-next'
import { ElMessage } from 'element-plus'
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
import type { ChatRoomVO } from '@/types/workcase/chatRoom'
const dateRange = ref<[Date, Date] | null>(null)
const statusFilter = ref('')
const satisfactionFilter = ref('')
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const loading = ref(false)
const showChatDialog = ref(false)
const currentChatMessages = ref<any[]>([])
const currentRoomId = ref('')
const currentChatRoom = ref<ChatRoomVO | undefined>(undefined)
const chats = ref([
{ chatId: 'CH001', customerName: '张三', customerPhone: '13800138000', agentName: '王五', messageCount: 24, duration: '15分钟', status: 'ended', statusName: '已结束', satisfaction: 4, startTime: '2024-12-13 10:30' },
{ chatId: 'CH002', customerName: '李四', customerPhone: '13800138001', agentName: '赵六', messageCount: 18, duration: '12分钟', status: 'ended', statusName: '已结束', satisfaction: 5, startTime: '2024-12-13 09:15' },
{ chatId: 'CH003', customerName: '王五', customerPhone: '13800138002', agentName: '孙七', messageCount: 32, duration: '22分钟', status: 'converted', statusName: '已转工单', satisfaction: 3, startTime: '2024-12-12 14:20' },
{ chatId: 'CH004', customerName: '赵六', customerPhone: '13800138003', agentName: '李四', messageCount: 15, duration: '10分钟', status: 'ongoing', statusName: '进行中', satisfaction: 0, startTime: '2024-12-13 11:00' },
{ chatId: 'CH005', customerName: '孙七', customerPhone: '13800138004', agentName: '王五', messageCount: 28, duration: '18分钟', status: 'ended', statusName: '已结束', satisfaction: 4.5, startTime: '2024-12-13 08:45' }
])
// 聊天室列表
const chatRooms = ref<ChatRoomVO[]>([])
const filteredChats = computed(() => {
let result = chats.value
if (statusFilter.value) {
result = result.filter(c => c.status === statusFilter.value)
}
if (satisfactionFilter.value) {
const satisfaction = satisfactionFilter.value === 'satisfied' ? 4 : satisfactionFilter.value === 'normal' ? 2.5 : 1
result = result.filter(c => {
if (satisfactionFilter.value === 'satisfied') return c.satisfaction >= 4
if (satisfactionFilter.value === 'normal') return c.satisfaction >= 2 && c.satisfaction < 4
if (satisfactionFilter.value === 'unsatisfied') return c.satisfaction < 2
return true
// 加载聊天室列表
const loadChatRooms = async () => {
loading.value = true
try {
const filter: any = {}
if (statusFilter.value) {
filter.status = statusFilter.value
}
if (searchKeyword.value) {
filter.roomName = searchKeyword.value
}
const res = await workcaseChatAPI.getChatRoomPage({
filter,
pageParam: {
page: currentPage.value,
pageSize: pageSize.value
}
})
if (res.success) {
chatRooms.value = res.dataList || []
total.value = res.total || 0
} else {
ElMessage.error(res.message || '加载聊天室列表失败')
}
} catch (error) {
console.error('加载聊天室列表失败:', error)
ElMessage.error('加载聊天室列表失败')
} finally {
loading.value = false
}
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
result = result.filter(c =>
c.customerName.toLowerCase().includes(keyword) ||
c.customerPhone.includes(keyword)
)
}
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
})
}
const getStatusType = (status: string) => {
const map: Record<string, string> = {
ongoing: 'success',
ended: 'info',
converted: 'warning'
active: 'success',
closed: 'info'
}
return map[status] || 'info'
}
const viewChat = (row: any) => {
currentChatMessages.value = [
{ type: 'customer', sender: row.customerName, time: '10:30:15', content: '你好,我的设备出现了故障' },
{ type: 'agent', sender: row.agentName, time: '10:30:45', content: '您好,感谢您的咨询。请问是什么故障呢?' },
{ type: 'customer', sender: row.customerName, time: '10:31:20', content: '显示屏不亮了,但是有声音' },
{ type: 'agent', sender: row.agentName, time: '10:31:50', content: '好的,这可能是显示屏的问题。请问您的设备型号是什么?' },
{ type: 'customer', sender: row.customerName, time: '10:32:30', content: 'TH-500GF' },
{ type: 'agent', sender: row.agentName, time: '10:33:00', content: '好的,我为您创建了一个工单,技术人员会尽快联系您' }
]
const getStatusName = (status: string) => {
const map: Record<string, string> = {
active: '进行中',
closed: '已结束'
}
return map[status] || status
}
const viewChat = (row: ChatRoomVO) => {
currentRoomId.value = row.roomId || ''
currentChatRoom.value = row
showChatDialog.value = true
}
const downloadChat = (row: any) => {
ElMessage.success(`下载对话: ${row.chatId}`)
}
const endChat = (row: any) => {
ElMessage.info(`结束对话: ${row.chatId}`)
const closeChat = async (row: ChatRoomVO) => {
if (!row.roomId) return
try {
const loginDomain = JSON.parse(localStorage.getItem('loginDomain') || '{}')
const userId = loginDomain?.userInfo?.userId || ''
const res = await workcaseChatAPI.closeChatRoom(row.roomId, userId)
if (res.success) {
ElMessage.success('聊天室已关闭')
await loadChatRooms()
} else {
ElMessage.error(res.message || '关闭失败')
}
} catch (error) {
console.error('关闭聊天室失败:', error)
ElMessage.error('关闭聊天室失败')
}
}
const exportData = () => {
ElMessage.success('数据导出功')
ElMessage.success('数据导出功能开发中')
}
onMounted(() => {
loadChatRooms()
})
</script>
<style lang="scss" scoped>
@@ -196,50 +208,11 @@ const exportData = () => {
color: #909399;
}
.chat-messages {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 400px;
overflow-y: auto;
}
.message-item {
padding: 10px 12px;
border-radius: 8px;
background: #f5f7fa;
}
.message-item.customer {
background: #e6f7ff;
margin-left: 20px;
}
.message-item.agent {
background: #f0f9ff;
margin-right: 20px;
}
.message-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
font-size: 12px;
}
.message-header .sender {
font-weight: 500;
color: #303133;
}
.message-header .time {
color: #909399;
}
.message-content {
color: #303133;
line-height: 1.5;
word-break: break-word;
.chat-dialog {
:deep(.el-dialog__body) {
padding: 0;
max-height: 600px;
overflow: hidden;
}
}
</style>

View File

@@ -35,7 +35,15 @@
<!-- 内容区域 -->
<main class="chat-main">
<!-- 对话记录 -->
<div v-if="activeTab === 'record'" class="messages-container">
<div v-if="activeTab === 'record'" class="messages-container" ref="messagesContainerRef" @scroll="handleScroll">
<!-- 加载更多提示 -->
<div v-if="loadingMore" class="loading-more">
<span>加载中...</span>
</div>
<div v-else-if="!hasMore && messages.length > 0" class="no-more">
<span>没有更多消息了</span>
</div>
<div class="messages-list">
<div v-for="message in messages" :key="message.messageId"
class="message-item"
@@ -65,7 +73,7 @@
</div>
<!-- 空状态 -->
<div v-if="messages.length === 0" class="empty-state">
<div v-if="messages.length === 0 && !loading" class="empty-state">
<div class="empty-text">暂无对话记录</div>
</div>
</div>
@@ -92,7 +100,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch, computed, nextTick } from 'vue'
import type { ChatRoomVO, ChatRoomMessageVO } from '@/types/workcase/chatRoom'
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
@@ -113,11 +121,25 @@ const props = withDefaults(defineProps<Props>(), {
const activeTab = ref<'record' | 'summary'>('record')
const messages = ref<ChatRoomMessageVO[]>([])
const loading = ref(false)
const loadingMore = ref(false)
const summary = ref<Summary>({
overview: '',
demand: ''
})
// 分页相关
const currentPage = ref(1)
const pageSize = 20
const total = ref(0)
const hasMore = computed(() => messages.value.length < total.value)
// 滚动容器引用
const messagesContainerRef = ref<HTMLElement | null>(null)
// 计算目标 roomId
const targetRoomId = computed(() => props.roomId || props.chatRoom?.roomId || '')
// 格式化时间
const formatTime = (timeStr?: string) => {
if (!timeStr) return ''
@@ -137,23 +159,85 @@ const getAvatarText = (message: ChatRoomMessageVO) => {
return message.senderName?.charAt(0) || '电'
}
// 加载消息数据
// 加载消息数据(首次加载,获取最新消息)
const loadMessages = async () => {
const targetRoomId = props.roomId || props.chatRoom?.roomId
if (!targetRoomId) return
if (!targetRoomId.value) return
loading.value = true
currentPage.value = 1
try {
const res = await workcaseChatAPI.getChatMessagePage({
filter: { roomId: targetRoomId },
pageParam: { page: 1, pageSize: 100 }
filter: { roomId: targetRoomId.value },
pageParam: { page: 1, pageSize }
})
if (res.success && res.dataList) {
// 后端降序返回,需要反转
// 后端降序返回(最新在前),需要反转显示(最新在下)
messages.value = [...res.dataList].reverse()
total.value = res.total || 0
// 滚动到底部
await nextTick()
scrollToBottom()
}
} catch (error) {
console.error('加载消息失败:', error)
} finally {
loading.value = false
}
}
// 加载更多历史消息(滚动到顶部时触发)
const loadMoreMessages = async () => {
if (!targetRoomId.value || loadingMore.value || !hasMore.value) return
loadingMore.value = true
const container = messagesContainerRef.value
const oldScrollHeight = container?.scrollHeight || 0
try {
currentPage.value++
const res = await workcaseChatAPI.getChatMessagePage({
filter: { roomId: targetRoomId.value },
pageParam: { page: currentPage.value, pageSize }
})
if (res.success && res.dataList && res.dataList.length > 0) {
// 后端降序返回,反转后插入到消息列表前面
const olderMessages = [...res.dataList].reverse()
messages.value = [...olderMessages, ...messages.value]
// 保持滚动位置(新内容加载后保持原来的可视区域)
await nextTick()
if (container) {
const newScrollHeight = container.scrollHeight
container.scrollTop = newScrollHeight - oldScrollHeight
}
}
} catch (error) {
console.error('加载更多消息失败:', error)
currentPage.value-- // 回退页码
} finally {
loadingMore.value = false
}
}
// 滚动到底部
const scrollToBottom = () => {
const container = messagesContainerRef.value
if (container) {
container.scrollTop = container.scrollHeight
}
}
// 处理滚动事件
const handleScroll = () => {
const container = messagesContainerRef.value
if (!container) return
// 滚动到顶部附近时加载更多距离顶部50px以内
if (container.scrollTop < 50 && hasMore.value && !loadingMore.value) {
loadMoreMessages()
}
}
@@ -175,7 +259,37 @@ onMounted(async () => {
await loadMessages()
generateSummary()
})
// 监听 roomId 变化,重新加载数据
watch(targetRoomId, async (newVal) => {
if (newVal) {
messages.value = []
summary.value = { overview: '', demand: '' }
currentPage.value = 1
total.value = 0
await loadMessages()
generateSummary()
}
})
</script>
<style scoped lang="scss">
@import url("./ChatMessage.scss");
.loading-more,
.no-more {
text-align: center;
padding: 12px;
color: #909399;
font-size: 12px;
}
.loading-more span {
display: inline-block;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
</style>