聊天判断修正
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user