第一次提交:Vue3后台前端最新版本
This commit is contained in:
442
src/views/chat/Agent.vue
Normal file
442
src/views/chat/Agent.vue
Normal file
@@ -0,0 +1,442 @@
|
||||
<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-button
|
||||
:type="isOnline ? 'danger' : 'primary'"
|
||||
size="small"
|
||||
@click="toggleOnline"
|
||||
:loading="switching"
|
||||
>
|
||||
{{ isOnline ? '下线' : '上线' }}
|
||||
</el-button>
|
||||
<span class="unread-tip" v-if="unreadTotal">
|
||||
<i class="el-icon-chat-dot-round" style="color: red; margin-right: 4px;"></i>
|
||||
{{ unreadTotal }}条未读
|
||||
</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-radio-group v-model="activeRole" size="small" @change="applyFilter">
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
<el-radio-button
|
||||
v-for="r in availableRoles"
|
||||
:key="r.prefix"
|
||||
:label="r.prefix"
|
||||
>{{ r.name }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体区域 -->
|
||||
<div class="agent-body">
|
||||
<div class="conversation-panel">
|
||||
<el-tabs v-model="tabActive" class="conv-tabs">
|
||||
<el-tab-pane :label="`待接入 (${pendingList.length})`" name="pending">
|
||||
<div class="conv-list" v-if="pendingList.length">
|
||||
<div
|
||||
v-for="conv in pendingList"
|
||||
:key="conv.id"
|
||||
class="conv-item"
|
||||
@click="acceptConversation(conv)"
|
||||
>
|
||||
<div class="conv-avatar">
|
||||
<el-avatar :size="36" :src="conv.data?.avatar || defaultAvatar" />
|
||||
</div>
|
||||
<div class="conv-info">
|
||||
<div class="conv-name">{{ conv.data?.name || conv.id }}</div>
|
||||
<div class="conv-preview">待接入...</div>
|
||||
</div>
|
||||
<el-button type="success" size="small" plain>接听</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无待接入会话" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`已接入 (${activeList.length})`" name="active">
|
||||
<div class="conv-list" v-if="activeList.length">
|
||||
<div
|
||||
v-for="conv in activeList"
|
||||
:key="conv.id"
|
||||
class="conv-item"
|
||||
@click="openChat(conv)"
|
||||
>
|
||||
<div class="conv-avatar">
|
||||
<el-avatar :size="36" :src="conv.data?.avatar || defaultAvatar" />
|
||||
</div>
|
||||
<div class="conv-info">
|
||||
<div class="conv-name">{{ conv.data?.name || conv.id }}</div>
|
||||
<div class="conv-preview">{{ conv.lastMessage?.payload?.text || '[其他消息]' }}</div>
|
||||
</div>
|
||||
<div class="conv-actions">
|
||||
<span class="unread-badge" v-if="conv.unread">{{ conv.unread }}</span>
|
||||
<el-button type="warning" size="small" plain @click.stop="endConversation(conv)">
|
||||
结束
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无已接入会话" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:主动搜索用户 -->
|
||||
<div class="search-panel">
|
||||
<div class="search-title">主动联系用户</div>
|
||||
<el-select
|
||||
v-model="searchRole"
|
||||
placeholder="选择身份"
|
||||
size="small"
|
||||
style="width: 100%; margin-bottom: 10px"
|
||||
>
|
||||
<el-option
|
||||
v-for="r in availableRoles"
|
||||
:key="r.prefix"
|
||||
:label="r.name"
|
||||
:value="r.prefix"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="searchUid"
|
||||
placeholder="输入用户UID(纯数字)"
|
||||
size="small"
|
||||
class="search-input"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :disabled="!searchRole || !searchUid" @click="startNewChat">发起会话</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="search-tip">示例:选择“打手”,输入123456,将联系 Ds123456</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天弹窗 -->
|
||||
<ChatDialog
|
||||
v-if="dialogVisible"
|
||||
v-model:visible="dialogVisible"
|
||||
:team-id="teamId"
|
||||
:customer-id="currentCustomerId"
|
||||
:customer-data="currentCustomerData"
|
||||
@close="currentCustomerId = ''"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const availableRoles = [
|
||||
{ prefix: 'Ds', name: '打手' },
|
||||
{ prefix: 'Boss', name: '老板' },
|
||||
{ prefix: 'Gs', name: '管事' },
|
||||
{ prefix: 'Sj', name: '商家' },
|
||||
{ prefix: 'Zz', name: '组长' }
|
||||
]
|
||||
|
||||
const isOnline = ref(false)
|
||||
const switching = ref(false)
|
||||
const tabActive = ref('pending')
|
||||
const pendingList = ref([])
|
||||
const activeList = ref([])
|
||||
const unreadTotal = ref(0)
|
||||
const teamId = 'support_team'
|
||||
|
||||
const searchRole = ref('')
|
||||
const searchUid = ref('')
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const currentCustomerId = ref('')
|
||||
const currentCustomerData = ref({ name: '', avatar: '' })
|
||||
|
||||
const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
|
||||
const activeRole = ref('all')
|
||||
|
||||
function getCsteam() {
|
||||
if (!window.goeasy) return null
|
||||
return window.goeasy.im.csteam(teamId)
|
||||
}
|
||||
|
||||
// 过滤
|
||||
function filterConversations(convs) {
|
||||
if (activeRole.value === 'all') return convs
|
||||
return convs.filter(c => {
|
||||
const userId = c.userId || c.id || ''
|
||||
return userId.startsWith(activeRole.value)
|
||||
})
|
||||
}
|
||||
|
||||
const applyFilter = () => {
|
||||
fetchPendingConversations()
|
||||
fetchActiveConversations()
|
||||
}
|
||||
|
||||
// 监听绑定(连接成功后立即执行)
|
||||
let conversationsUpdatedHandler = null
|
||||
let pendingUpdatedHandler = null
|
||||
|
||||
function bindConversationListeners() {
|
||||
// 先清旧
|
||||
if (conversationsUpdatedHandler) {
|
||||
window.goeasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, conversationsUpdatedHandler)
|
||||
}
|
||||
if (pendingUpdatedHandler) {
|
||||
window.goeasy.im.off(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, pendingUpdatedHandler)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
window.goeasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, conversationsUpdatedHandler)
|
||||
|
||||
pendingUpdatedHandler = (data) => {
|
||||
pendingList.value = filterConversations(data.conversations || [])
|
||||
}
|
||||
window.goeasy.im.on(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, pendingUpdatedHandler)
|
||||
}
|
||||
|
||||
// 连接成功回调
|
||||
const onConnected = () => {
|
||||
console.log('[Agent] 收到连接成功事件,立刻绑定会话监听')
|
||||
bindConversationListeners()
|
||||
fetchPendingConversations()
|
||||
fetchActiveConversations()
|
||||
}
|
||||
|
||||
// 上线
|
||||
const csteamOnline = () => {
|
||||
if (!window.goeasy) {
|
||||
console.warn('[Agent] 未建立连接,无法上线')
|
||||
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()
|
||||
}
|
||||
setTimeout(() => { switching.value = false }, 1000)
|
||||
}
|
||||
|
||||
// 拉取待接入
|
||||
const fetchPendingConversations = () => {
|
||||
if (!window.goeasy) return
|
||||
window.goeasy.im.pendingConversations({
|
||||
onSuccess: (result) => {
|
||||
pendingList.value = filterConversations(result.content?.conversations || [])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 拉取已接入
|
||||
const fetchActiveConversations = () => {
|
||||
if (!window.goeasy) return
|
||||
window.goeasy.im.latestConversations({
|
||||
onSuccess: (result) => {
|
||||
const all = result.content?.conversations || []
|
||||
activeList.value = filterConversations(all.filter(c => c.type === 'cs' && !c.ended))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const acceptConversation = (conv) => {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const endConversation = (conv) => {
|
||||
const csteam = getCsteam()
|
||||
csteam.end({
|
||||
id: conv.id,
|
||||
onSuccess: () => {
|
||||
ElMessage.success('会话已结束')
|
||||
fetchActiveConversations()
|
||||
},
|
||||
onFailed: (error) => {
|
||||
ElMessage.error('结束失败:' + error.content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openChat = (conv) => {
|
||||
currentCustomerId.value = conv.id || conv.userId
|
||||
currentCustomerData.value = conv.data || { name: currentCustomerId.value, avatar: defaultAvatar }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
currentCustomerId.value = fullId
|
||||
currentCustomerData.value = { name: fullId, avatar: defaultAvatar }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('[Agent] 页面挂载')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (conversationsUpdatedHandler) {
|
||||
window.goeasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, conversationsUpdatedHandler)
|
||||
}
|
||||
if (pendingUpdatedHandler) {
|
||||
window.goeasy.im.off(GoEasy.IM_EVENT.PENDING_CONVERSATIONS_UPDATED, pendingUpdatedHandler)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agent-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #eef2ff;
|
||||
}
|
||||
.agent-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.unread-tip {
|
||||
color: #ff4d4f;
|
||||
font-weight: bold;
|
||||
}
|
||||
.agent-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.conversation-panel {
|
||||
flex: 2;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
.search-panel {
|
||||
flex: 1;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
.conv-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.conv-item:hover {
|
||||
background: rgba(0,242,255,0.1);
|
||||
}
|
||||
.conv-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.conv-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
.conv-preview {
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.unread-badge {
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
padding: 0 6px;
|
||||
margin-right: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.search-tip {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user