第一次提交:Vue3后台前端最新版本

This commit is contained in:
XingQue
2026-06-14 02:43:42 +08:00
commit cf07faccc4
68 changed files with 29381 additions and 0 deletions

442
src/views/chat/Agent.vue Normal file
View 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>