xx
This commit is contained in:
+134
-103
@@ -33,7 +33,7 @@
|
||||
@click="selectTicket(ticket)"
|
||||
>
|
||||
<div class="ticket-avatar">
|
||||
<el-avatar :size="40">{{ ticket.username.charAt(0) }}</el-avatar>
|
||||
<el-avatar :size="40" :src="ticket.avatar">{{ ticket.username.charAt(0) }}</el-avatar>
|
||||
</div>
|
||||
<div class="ticket-content">
|
||||
<div class="ticket-top">
|
||||
@@ -96,8 +96,8 @@
|
||||
:class="['message-item', message.isAdmin ? 'message-admin' : message.isSystem ? 'message-system' : 'message-user']"
|
||||
>
|
||||
<div class="message-avatar" v-if="!message.isAdmin && !message.isSystem">
|
||||
<el-avatar :size="36" :src="getUserAvatar(message.userId)">
|
||||
{{ currentTicket.username.charAt(0) }}
|
||||
<el-avatar :size="36" :src="message.avatar">
|
||||
{{ message.userId === currentTicket.userId ? currentTicket.username.charAt(0) : 'U' }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
@@ -117,7 +117,7 @@
|
||||
<div class="message-time">{{ formatMessageTime(message.time) }}</div>
|
||||
</div>
|
||||
<div class="message-avatar" v-if="message.isAdmin && !message.isSystem">
|
||||
<el-avatar :size="36" :src="getUserAvatar(message.userId || 1)">A</el-avatar>
|
||||
<el-avatar :size="36" :src="message.avatar">A</el-avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,21 +204,24 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Loading } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getTickerList,
|
||||
getTickerList,
|
||||
getTicketDetail,
|
||||
replyTicket,
|
||||
closeTicket,
|
||||
getUserAvatar,
|
||||
getFileImage,
|
||||
parseFilesToImages
|
||||
parseFilesToImages,
|
||||
getTicketCount
|
||||
} from '@/api/ticket'
|
||||
import notificationSound from '@/assets/7.wav'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
|
||||
// 路由相关
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 管理员ID列表(客服ID)
|
||||
const adminUserIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // 假设这些ID是客服ID
|
||||
// 用户 store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 头像
|
||||
const adminAvatar = ref('')
|
||||
@@ -271,6 +274,11 @@ const stats = reactive({
|
||||
isLoadingStats: false
|
||||
})
|
||||
|
||||
// 上一次的待处理数量,用于判断是否有新工单
|
||||
const previousPendingCount = ref(0)
|
||||
// 音频对象
|
||||
const audio = new Audio(notificationSound)
|
||||
|
||||
// 快捷回复选项
|
||||
const quickReplies = ref([
|
||||
{ title: '您好,有什么可以帮助您的?', content: '您好,有什么可以帮助您的?' },
|
||||
@@ -327,8 +335,9 @@ const fetchTicketList = async (append = false) => {
|
||||
const mappedTickets = tickets.map(item => ({
|
||||
id: item.work_id,
|
||||
title: item.name,
|
||||
username: `用户${item.user_id}`, // 用户名,真实环境可能需要获取用户信息
|
||||
userId: item.user_id,
|
||||
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
|
||||
userId: item.user?.userId,
|
||||
avatar: item.user?.coverUrl || '',
|
||||
createTime: new Date(item.created_at).toLocaleString(),
|
||||
lastReplyTime: new Date(item.update_time).toLocaleString(),
|
||||
status: convertStatusToString(item.status),
|
||||
@@ -368,44 +377,35 @@ const fetchTicketList = async (append = false) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个状态的工单数量
|
||||
const fetchStatusStat = async (status) => {
|
||||
try {
|
||||
// 将状态字符串转换为API所需的状态值
|
||||
let statusValue = '';
|
||||
if (status === 'pending') statusValue = '0';
|
||||
else if (status === 'processing') statusValue = '1';
|
||||
else if (status === 'replied') statusValue = '2';
|
||||
else if (status === 'completed') statusValue = '3';
|
||||
|
||||
const res = await getTickerList(10, 1, statusValue) // 只请求一条数据,但获取总数
|
||||
|
||||
if (res.code === 200) {
|
||||
if (status === '') {
|
||||
stats.total = res.data.all_count
|
||||
} else {
|
||||
stats[status] = res.data.all_count
|
||||
}
|
||||
} else {
|
||||
console.error(`获取${status || '全部'}工单统计失败:`, res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取${status || '全部'}工单统计出错:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有状态的工单数量
|
||||
const fetchAllStats = async () => {
|
||||
stats.isLoadingStats = true
|
||||
try {
|
||||
// 并行获取各个状态的工单数量
|
||||
await Promise.all([
|
||||
fetchStatusStat(''), // 获取全部工单数量
|
||||
fetchStatusStat('pending'), // 待处理
|
||||
fetchStatusStat('processing'), // 处理中
|
||||
fetchStatusStat('replied'), // 已回复
|
||||
fetchStatusStat('completed') // 已完成
|
||||
])
|
||||
const res = await getTicketCount()
|
||||
if (res.code === 200) {
|
||||
const data = res.data
|
||||
|
||||
// 检查是否有新工单(待处理数量增加)
|
||||
if (data.wait_count > previousPendingCount.value && previousPendingCount.value !== 0) {
|
||||
try {
|
||||
audio.play().catch(e => console.error('播放提示音失败:', e))
|
||||
} catch (e) {
|
||||
console.error('播放提示音出错:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新上一次的数量
|
||||
previousPendingCount.value = data.wait_count
|
||||
|
||||
stats.total = data.all_count
|
||||
stats.pending = data.wait_count
|
||||
stats.replied = data.reply_count
|
||||
stats.completed = data.close_count
|
||||
// 计算处理中的数量:总数 - 待处理 - 已回复 - 已完成
|
||||
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
|
||||
} else {
|
||||
console.error('获取工单统计失败:', res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取工单统计数据出错:', error)
|
||||
} finally {
|
||||
@@ -413,18 +413,12 @@ const fetchAllStats = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 只刷新当前分类的统计数据(用于定时刷新,减少请求)
|
||||
// 刷新统计数据(用于定时刷新)
|
||||
const fetchCurrentStatusStat = async () => {
|
||||
try {
|
||||
// 只获取当前选中分类的统计数据
|
||||
await fetchStatusStat(activeStatus.value)
|
||||
// 同时获取全部工单数量(因为顶部显示需要)
|
||||
await fetchStatusStat('')
|
||||
} catch (error) {
|
||||
console.error('获取当前分类统计数据出错:', error)
|
||||
}
|
||||
await fetchAllStats()
|
||||
}
|
||||
|
||||
|
||||
// 加载更多工单
|
||||
const loadMoreTickets = () => {
|
||||
if (!hasMore.value || isLoading.value) return
|
||||
@@ -476,9 +470,9 @@ const filteredTickets = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 判断是否是客服
|
||||
// 判断是否是当前登录的管理员
|
||||
const isAdmin = (userId) => {
|
||||
return adminUserIds.includes(userId)
|
||||
return userId === userStore.userInfo?.user_id
|
||||
}
|
||||
|
||||
// 状态转换
|
||||
@@ -540,25 +534,25 @@ const fetchTicketMessages = async (workId) => {
|
||||
}
|
||||
|
||||
// 处理消息列表
|
||||
if (detail.Content && detail.Content.length > 0) {
|
||||
// 使用Promise.all一次性处理所有消息和图片
|
||||
const messagesPromises = detail.Content.map(async (msg) => {
|
||||
const isAdminMsg = isAdmin(msg.UserId)
|
||||
const images = await parseFilesToImages(msg.Flies)
|
||||
if (detail.content && detail.content.length > 0) {
|
||||
// 处理所有消息
|
||||
const messages = detail.content.map((msg) => {
|
||||
const isAdminMsg = isAdmin(msg.user?.userId)
|
||||
// 从 flies 数组中提取图片 URL
|
||||
const images = msg.flies ? msg.flies.map(file => file.url) : []
|
||||
|
||||
return {
|
||||
id: msg.Id,
|
||||
content: msg.Content !== 'empty' ? msg.Content : null,
|
||||
id: msg.id,
|
||||
content: msg.content !== 'empty' ? msg.content : null,
|
||||
images: images,
|
||||
time: new Date(msg.CreatedAt).toLocaleString(),
|
||||
time: new Date().toLocaleString(), // API 没有返回时间,使用当前时间
|
||||
isAdmin: isAdminMsg,
|
||||
isSystem: false,
|
||||
userId: msg.UserId
|
||||
userId: msg.user?.userId,
|
||||
avatar: msg.user?.coverUrl || ''
|
||||
}
|
||||
})
|
||||
|
||||
// 等待所有消息处理完成
|
||||
const messages = await Promise.all(messagesPromises)
|
||||
currentMessages.value = messages
|
||||
}
|
||||
} else {
|
||||
@@ -589,23 +583,29 @@ const sendMessage = async () => {
|
||||
const fileIds = []
|
||||
|
||||
try {
|
||||
// 添加一个临时的"正在发送"消息
|
||||
// 保存输入内容
|
||||
const inputMsg = messageInput.value.trim()
|
||||
const inputImages = [...selectedImages.value]
|
||||
|
||||
// 清空输入和已选图片
|
||||
messageInput.value = ''
|
||||
selectedImages.value = []
|
||||
|
||||
// 立即添加消息到界面(不显示 loading)
|
||||
const tempMsg = {
|
||||
content: messageInput.value.trim() || null,
|
||||
images: selectedImages.value.length > 0 ? [...selectedImages.value] : null,
|
||||
id: Date.now(), // 临时 ID
|
||||
content: inputMsg || null,
|
||||
images: inputImages.length > 0 ? inputImages : [],
|
||||
time: new Date().toLocaleString(),
|
||||
isAdmin: true,
|
||||
isLoading: true,
|
||||
isSystem: false,
|
||||
userId: userStore.userInfo?.user_id,
|
||||
avatar: userStore.userInfo?.cover_url || '',
|
||||
isTempMessage: true
|
||||
}
|
||||
|
||||
currentMessages.value.push(tempMsg)
|
||||
|
||||
// 清空输入和已选图片
|
||||
const inputMsg = messageInput.value
|
||||
messageInput.value = ''
|
||||
selectedImages.value = []
|
||||
|
||||
// 滚动到底部
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
@@ -633,6 +633,7 @@ const sendMessage = async () => {
|
||||
|
||||
// 恢复输入内容
|
||||
messageInput.value = inputMsg
|
||||
selectedImages.value = inputImages
|
||||
|
||||
ElMessage.error(res.message || '发送失败')
|
||||
}
|
||||
@@ -838,8 +839,9 @@ const refreshTicketList = async () => {
|
||||
const mappedTickets = tickets.map(item => ({
|
||||
id: item.work_id,
|
||||
title: item.name,
|
||||
username: `用户${item.user_id}`,
|
||||
userId: item.user_id,
|
||||
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
|
||||
userId: item.user?.userId,
|
||||
avatar: item.user?.coverUrl || '',
|
||||
createTime: new Date(item.created_at).toLocaleString(),
|
||||
lastReplyTime: new Date(item.update_time).toLocaleString(),
|
||||
status: convertStatusToString(item.status),
|
||||
@@ -874,6 +876,39 @@ const refreshTicketList = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:去除 URL 中的查询参数
|
||||
const normalizeUrl = (url) => {
|
||||
if (!url) return ''
|
||||
return url.split('?')[0]
|
||||
}
|
||||
|
||||
// 辅助函数:比较两个消息数组是否相同(忽略 URL 查询参数)
|
||||
const areMessagesEqual = (messages1, messages2) => {
|
||||
if (messages1.length !== messages2.length) return false
|
||||
|
||||
for (let i = 0; i < messages1.length; i++) {
|
||||
const msg1 = messages1[i]
|
||||
const msg2 = messages2[i]
|
||||
|
||||
// 比较消息 ID 和内容
|
||||
if (msg1.id !== msg2.id || msg1.content !== msg2.content) return false
|
||||
|
||||
// 比较图片数量
|
||||
if ((msg1.images?.length || 0) !== (msg2.images?.length || 0)) return false
|
||||
|
||||
// 比较图片 URL(去除查询参数)
|
||||
if (msg1.images && msg2.images) {
|
||||
for (let j = 0; j < msg1.images.length; j++) {
|
||||
if (normalizeUrl(msg1.images[j]) !== normalizeUrl(msg2.images[j])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 静默刷新聊天记录(不显示loading状态)
|
||||
const refreshTicketMessages = async (workId) => {
|
||||
try {
|
||||
@@ -886,37 +921,33 @@ const refreshTicketMessages = async (workId) => {
|
||||
if (currentTicket.value) {
|
||||
// 只有非待处理状态才直接更新,待处理状态保持不变,等待回复后再更新
|
||||
if (currentTicket.value.status !== 'pending') {
|
||||
currentTicket.value.status = convertStatusToString(detail.Status)
|
||||
currentTicket.value.status = convertStatusToString(detail.status)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理消息列表
|
||||
if (detail.Content && detail.Content.length > 0) {
|
||||
// 检查是否有新消息
|
||||
const lastMsgId = currentMessages.value.length > 0 ?
|
||||
currentMessages.value[currentMessages.value.length - 1].id : 0;
|
||||
const hasNewMessage = detail.Content.some(msg => msg.Id > lastMsgId);
|
||||
|
||||
if (hasNewMessage) {
|
||||
// 有新消息时才更新
|
||||
const messagesPromises = detail.Content.map(async (msg) => {
|
||||
const isAdminMsg = isAdmin(msg.UserId)
|
||||
const images = await parseFilesToImages(msg.Flies)
|
||||
|
||||
return {
|
||||
id: msg.Id,
|
||||
content: msg.Content !== 'empty' ? msg.Content : null,
|
||||
images: images,
|
||||
time: new Date(msg.CreatedAt).toLocaleString(),
|
||||
isAdmin: isAdminMsg,
|
||||
isSystem: false,
|
||||
userId: msg.UserId
|
||||
}
|
||||
})
|
||||
if (detail.content && detail.content.length > 0) {
|
||||
// 构建新消息列表
|
||||
const newMessages = detail.content.map((msg) => {
|
||||
const isAdminMsg = isAdmin(msg.user?.userId)
|
||||
// 从 flies 数组中提取图片 URL
|
||||
const images = msg.flies ? msg.flies.map(file => file.url) : []
|
||||
|
||||
// 等待所有消息处理完成
|
||||
const messages = await Promise.all(messagesPromises)
|
||||
currentMessages.value = messages
|
||||
return {
|
||||
id: msg.id,
|
||||
content: msg.content !== 'empty' ? msg.content : null,
|
||||
images: images,
|
||||
time: new Date().toLocaleString(), // API 没有返回时间,使用当前时间
|
||||
isAdmin: isAdminMsg,
|
||||
isSystem: false,
|
||||
userId: msg.user?.userId,
|
||||
avatar: msg.user?.coverUrl || ''
|
||||
}
|
||||
})
|
||||
|
||||
// 只有在消息真正发生变化时才更新(忽略 URL 查询参数的变化)
|
||||
if (!areMessagesEqual(currentMessages.value, newMessages)) {
|
||||
currentMessages.value = newMessages
|
||||
|
||||
// 如果有新消息,滚动到底部
|
||||
nextTick(() => {
|
||||
|
||||
Reference in New Issue
Block a user