fix: 登录页校验JWT有效性;同步后台多项功能更新

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-01 11:36:13 +08:00
parent 4ed234454b
commit adaf9b2522
10 changed files with 1224 additions and 185 deletions

View File

@@ -1,15 +1,11 @@
<template>
<div class="agent-container">
<!-- 客服连接组件无界面负责建连 -->
<KefuConnect @connected="onConnected" />
<!-- 顶部操作栏 -->
<div class="agent-header">
<div class="header-left">
<el-tag :type="isOnline ? 'success' : 'info'" size="large">
{{ isOnline ? '在线' : '离线' }}
</el-tag>
<el-tag :type="statusTagType" size="large">{{ statusText }}</el-tag>
<el-button
v-if="goeasyReady"
:type="isOnline ? 'danger' : 'primary'"
size="small"
@click="toggleOnline"
@@ -17,6 +13,7 @@
>
{{ isOnline ? '下线' : '上线' }}
</el-button>
<span class="online-tip" v-if="goeasyReady && isOnline">切换页面不会下线</span>
<span class="unread-tip" v-if="unreadTotal">
<i class="el-icon-chat-dot-round" style="color: red; margin-right: 4px;"></i>
{{ unreadTotal }}条未读
@@ -123,6 +120,7 @@
:team-id="teamId"
:customer-id="currentCustomerId"
:customer-data="currentCustomerData"
:agent-online="isOnline"
@close="currentCustomerId = ''"
/>
</div>
@@ -132,8 +130,18 @@
import { ref, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import ChatDialog from '@/components/ChatDialog.vue'
import KefuConnect from '@/components/KefuConnect.vue'
import GoEasy from 'goeasy'
import {
KEFU_TEAM_ID,
getCsteam,
refreshKefuUnread,
calcUnreadFromConversations,
ensureCustomerAccepted,
goOnlineManually,
goOfflineManually,
isGoeasyConnected,
checkCsOnlineStatus
} from '@/utils/kefuChat'
const { proxy } = getCurrentInstance()
@@ -145,13 +153,14 @@ const availableRoles = [
{ prefix: 'Zz', name: '组长' }
]
const isOnline = ref(false)
const goeasyReady = ref(isGoeasyConnected())
const isOnline = ref(!!window.__kefuCsOnline)
const switching = ref(false)
const tabActive = ref('pending')
const pendingList = ref([])
const activeList = ref([])
const unreadTotal = ref(0)
const teamId = 'support_team'
const teamId = KEFU_TEAM_ID
const searchRole = ref('')
const searchUid = ref('')
@@ -164,9 +173,18 @@ const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726
const activeRole = ref('all')
function getCsteam() {
if (!window.goeasy) return null
return window.goeasy.im.csteam(teamId)
const statusText = computed(() => {
if (!goeasyReady.value) return '消息连接中…'
return isOnline.value ? '在线接待中' : '已离线'
})
const statusTagType = computed(() => {
if (!goeasyReady.value) return 'info'
return isOnline.value ? 'success' : 'warning'
})
function getCsteamLocal() {
return getCsteam()
}
// 过滤
@@ -199,8 +217,8 @@ function bindConversationListeners() {
conversationsUpdatedHandler = (data) => {
const all = data.conversations || []
activeList.value = filterConversations(all.filter(c => c.type === 'cs' && !c.ended))
unreadTotal.value = data.unreadTotal || 0
proxy.$emitter.emit('agent-unread', unreadTotal.value)
unreadTotal.value = data.unreadTotal || calcUnreadFromConversations(activeList.value)
refreshKefuUnread(proxy.$emitter)
}
window.goeasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, conversationsUpdatedHandler)
@@ -218,63 +236,52 @@ const onConnected = () => {
fetchActiveConversations()
}
// 上线
const csteamOnline = () => {
if (!window.goeasy) {
console.warn('[Agent] 未建立连接,无法上线')
const toggleOnline = () => {
if (switching.value || !goeasyReady.value) {
ElMessage.warning('消息服务尚未连接,请稍候…')
return
}
const csteam = getCsteam()
if (!csteam) return
csteam.online({
teamData: { name: '阿龙电竞客服中心', avatar: '' },
agentData: {
name: localStorage.getItem('username') || 'KF',
avatar: ''
},
onSuccess: () => {
isOnline.value = true
console.log('[Agent] 上线成功')
ElMessage.success('已上线,开始接收会话')
fetchPendingConversations()
fetchActiveConversations()
},
onFailed: (error) => {
console.error('[Agent] 上线失败:', error)
ElMessage.error('上线失败:' + error.content)
}
})
}
// 下线
const csteamOffline = () => {
const csteam = getCsteam()
if (!csteam) return
csteam.offline({
onSuccess: () => {
isOnline.value = false
console.log('[Agent] 下线成功')
ElMessage.success('已下线')
pendingList.value = []
},
onFailed: (error) => {
ElMessage.error('下线失败:' + error.content)
}
})
}
const toggleOnline = () => {
if (switching.value) return
switching.value = true
if (isOnline.value) {
csteamOffline()
} else {
csteamOnline()
const phone = localStorage.getItem('username')
const stopSpin = () => { switching.value = false }
const safetyTimer = setTimeout(stopSpin, 15000)
const wrapDone = (fn) => (...args) => {
clearTimeout(safetyTimer)
stopSpin()
fn?.(...args)
}
if (isOnline.value) {
goOfflineManually({
emitter: proxy.$emitter,
onSuccess: wrapDone(() => {
isOnline.value = false
ElMessage.success('已下线,不再接收新会话')
refreshKefuUnread(proxy.$emitter)
}),
onFailed: wrapDone((error) => {
ElMessage.error('下线失败:' + (error?.content || '未知错误'))
})
})
} else {
goOnlineManually({
phone,
emitter: proxy.$emitter,
onSuccess: wrapDone(() => {
isOnline.value = true
ElMessage.success('已上线,开始接收会话')
fetchPendingConversations()
fetchActiveConversations()
refreshKefuUnread(proxy.$emitter)
}),
onFailed: wrapDone((error) => {
ElMessage.error('上线失败:' + (error?.content || '未知错误'))
})
})
}
setTimeout(() => { switching.value = false }, 1000)
}
// 拉取待接入
const fetchPendingConversations = () => {
if (!window.goeasy) return
window.goeasy.im.pendingConversations({
@@ -295,27 +302,43 @@ const fetchActiveConversations = () => {
})
}
const acceptConversation = (conv) => {
const acceptAndOpenChat = async (conv, openDialog = true) => {
if (activeList.value.length >= 20) {
ElMessage.warning('最多同时接入20个会话')
return
}
const csteam = getCsteam()
csteam.accept({
customer: { id: conv.id, data: conv.data || { name: conv.id, avatar: defaultAvatar } },
onSuccess: () => {
ElMessage.success('已接入')
fetchPendingConversations()
fetchActiveConversations()
},
onFailed: (error) => {
ElMessage.error('接入失败:' + error.content)
if (!goeasyReady.value) {
ElMessage.warning('消息服务连接中,请稍候…')
return
}
if (!isOnline.value) {
ElMessage.warning('请先上线后再接入会话')
return
}
const customerId = conv.id || conv.userId
const customerData = conv.data || { name: customerId, avatar: defaultAvatar }
try {
await ensureCustomerAccepted(customerId, customerData)
ElMessage.success('已接入')
fetchPendingConversations()
fetchActiveConversations()
if (openDialog) {
currentCustomerId.value = customerId
currentCustomerData.value = customerData
dialogVisible.value = true
tabActive.value = 'active'
}
})
} catch (error) {
ElMessage.error('接入失败:' + (error?.content || '未知错误'))
}
}
const acceptConversation = (conv) => {
acceptAndOpenChat(conv, true)
}
const endConversation = (conv) => {
const csteam = getCsteam()
const csteam = getCsteamLocal()
csteam.end({
id: conv.id,
onSuccess: () => {
@@ -328,34 +351,109 @@ const endConversation = (conv) => {
})
}
const openChat = (conv) => {
currentCustomerId.value = conv.id || conv.userId
currentCustomerData.value = conv.data || { name: currentCustomerId.value, avatar: defaultAvatar }
dialogVisible.value = true
const openChat = async (conv) => {
const customerId = conv.id || conv.userId
const customerData = conv.data || { name: customerId, avatar: defaultAvatar }
if (!goeasyReady.value) {
ElMessage.warning('消息服务连接中,请稍候…')
return
}
if (!isOnline.value) {
ElMessage.warning('请先上线后再接入会话')
return
}
try {
await ensureCustomerAccepted(customerId, customerData)
currentCustomerId.value = customerId
currentCustomerData.value = customerData
dialogVisible.value = true
} catch (error) {
ElMessage.error('接入会话失败:' + (error?.content || '未知错误'))
}
}
const startNewChat = () => {
if (!searchRole.value || !searchUid.value) return
const fullId = searchRole.value + searchUid.value
const exists = activeList.value.find(c => c.id === fullId)
if (exists) {
openChat(exists)
if (!goeasyReady.value) {
ElMessage.warning('消息服务连接中,请稍候…')
return
}
currentCustomerId.value = fullId
currentCustomerData.value = { name: fullId, avatar: defaultAvatar }
dialogVisible.value = true
if (!isOnline.value) {
ElMessage.warning('请先上线后再接入会话')
return
}
const uid = String(searchUid.value).trim()
if (!/^\d+$/.test(uid)) {
ElMessage.warning('UID 请输入纯数字')
return
}
const fullId = searchRole.value + uid
const active = activeList.value.find(c => (c.id === fullId || c.userId === fullId))
if (active) {
openChat(active)
return
}
const pending = pendingList.value.find(c => c.id === fullId)
if (pending) {
acceptAndOpenChat(pending, true)
return
}
acceptAndOpenChat({
id: fullId,
data: { name: fullId, avatar: defaultAvatar }
}, true)
}
onMounted(() => {
console.log('[Agent] 页面挂载')
const onAgentUnread = (count) => {
unreadTotal.value = count
}
const onGoeasyReady = (ready) => {
goeasyReady.value = !!ready
}
const onCsOnlineChange = (online) => {
isOnline.value = !!online
}
const onGoEasyConnected = () => {
onConnected()
goeasyReady.value = isGoeasyConnected()
onCsOnlineChange(window.__kefuCsOnline)
}
onMounted(async () => {
goeasyReady.value = isGoeasyConnected()
if (goeasyReady.value) {
const online = await checkCsOnlineStatus()
isOnline.value = online
proxy.$emitter.emit('kefu-cs-online', online)
} else {
isOnline.value = !!window.__kefuCsOnline
}
proxy.$emitter.on('kefu-goeasy-ready', onGoeasyReady)
proxy.$emitter.on('kefu-cs-online', onCsOnlineChange)
proxy.$emitter.on('agent-unread', onAgentUnread)
if (window.goeasy && isGoeasyConnected()) {
onConnected()
} else {
proxy.$emitter.on('goeasy-connected', onGoEasyConnected)
}
})
onUnmounted(() => {
if (conversationsUpdatedHandler) {
proxy.$emitter.off('goeasy-connected', onGoEasyConnected)
proxy.$emitter.off('kefu-goeasy-ready', onGoeasyReady)
proxy.$emitter.off('kefu-cs-online', onCsOnlineChange)
proxy.$emitter.off('agent-unread', onAgentUnread)
if (window.goeasy && conversationsUpdatedHandler) {
window.goeasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, conversationsUpdatedHandler)
}
if (pendingUpdatedHandler) {
if (window.goeasy && pendingUpdatedHandler) {
window.goeasy.im.off(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, pendingUpdatedHandler)
}
})
@@ -439,4 +537,8 @@ onUnmounted(() => {
color: #888;
margin-top: 8px;
}
.online-tip {
font-size: 12px;
color: #8bc34a;
}
</style>