Files
akmon/pages/mall/service/index.uvue
2026-01-20 08:04:15 +08:00

1002 lines
21 KiB
Plaintext

<!-- 客服端首页 - UTS Android 兼容 -->
<template>
<view class="service-container">
<!-- 头部工作状态栏 -->
<view class="header">
<view class="agent-info">
<image :src="agentInfo.avatar_url || '/static/default-avatar.png'" class="avatar" mode="aspectFit" />
<view class="agent-details">
<text class="agent-name">{{ agentInfo.name }}</text>
<view class="status-info">
<text class="status-badge" :class="getStatusClass()">{{ getStatusText() }}</text>
<text class="work-time">工作时长: {{ workDuration }}</text>
</view>
</view>
</view>
<view class="status-controls">
<switch :checked="isOnline" @change="toggleWorkStatus" color="#4CAF50" />
<text class="status-label">{{ isOnline ? '在线服务' : '离线休息' }}</text>
</view>
</view>
<!-- 今日工作统计 -->
<view class="stats-section">
<text class="section-title">今日工作统计</text>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value">{{ todayStats.handled_conversations }}</text>
<text class="stat-label">处理会话</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayStats.resolved_issues }}</text>
<text class="stat-label">解决问题</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayStats.avg_response_time }}s</text>
<text class="stat-label">平均响应</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayStats.satisfaction_rate }}%</text>
<text class="stat-label">满意度</text>
</view>
</view>
</view>
<!-- 当前会话队列 -->
<view class="queue-section">
<view class="section-header">
<text class="section-title">会话队列 ({{ conversations.length }})</text>
<text class="queue-status" :class="getQueueStatusClass()">{{ getQueueStatusText() }}</text>
</view>
<view v-if="conversations.length === 0" class="empty-queue">
<text class="empty-icon">💬</text>
<text class="empty-text">当前没有待处理会话</text>
<text class="empty-subtitle">{{ isOnline ? '请保持在线,等待用户咨询' : '请上线接受用户咨询' }}</text>
</view>
<view v-for="conversation in conversations" :key="conversation.id" class="conversation-item" @click="openConversation(conversation.id)">
<view class="conversation-header">
<view class="user-info">
<image :src="conversation.user_avatar || '/static/default-avatar.png'" class="user-avatar" mode="aspectFit" />
<view class="user-details">
<text class="user-name">{{ conversation.user_name }}</text>
<text class="conversation-type">{{ getConversationTypeText(conversation.type) }}</text>
</view>
</view>
<view class="conversation-meta">
<text class="priority-badge" :class="getPriorityClass(conversation.priority)">{{ getPriorityText(conversation.priority) }}</text>
<text class="wait-time">等待 {{ formatWaitTime(conversation.created_at) }}</text>
</view>
</view>
<view class="conversation-preview">
<text class="last-message">{{ conversation.last_message }}</text>
<text class="message-time">{{ formatTime(conversation.last_message_time) }}</text>
</view>
<view class="conversation-actions">
<button class="action-btn primary" @click.stop="acceptConversation(conversation.id)">接受</button>
<button class="action-btn secondary" @click.stop="viewUserProfile(conversation.user_id)">用户信息</button>
</view>
</view>
</view>
<!-- 快速处理工具 -->
<view class="tools-section">
<text class="section-title">快速处理</text>
<view class="tools-grid">
<view class="tool-item" @click="goToOrderInquiry">
<text class="tool-icon">📋</text>
<text class="tool-text">订单查询</text>
</view>
<view class="tool-item" @click="goToRefundProcess">
<text class="tool-icon">💰</text>
<text class="tool-text">退款处理</text>
</view>
<view class="tool-item" @click="goToComplaintHandle">
<text class="tool-icon">⚠️</text>
<text class="tool-text">投诉处理</text>
</view>
<view class="tool-item" @click="goToKnowledgeBase">
<text class="tool-icon">📚</text>
<text class="tool-text">知识库</text>
</view>
<view class="tool-item" @click="goToQuickReplies">
<text class="tool-icon">💬</text>
<text class="tool-text">快捷回复</text>
</view>
<view class="tool-item" @click="goToEscalation">
<text class="tool-icon">⬆️</text>
<text class="tool-text">问题升级</text>
</view>
</view>
</view>
<!-- 待办事项 -->
<view class="todo-section">
<view class="section-header">
<text class="section-title">待办事项</text>
<text class="todo-count">{{ todoItems.length }}项</text>
</view>
<view v-if="todoItems.length === 0" class="empty-todo">
<text class="empty-text">暂无待办事项</text>
</view>
<view v-for="todo in todoItems" :key="todo.id" class="todo-item" @click="handleTodoItem(todo)">
<view class="todo-content">
<text class="todo-title">{{ todo.title }}</text>
<text class="todo-description">{{ todo.description }}</text>
<text class="todo-deadline">截止: {{ formatDeadline(todo.deadline) }}</text>
</view>
<view class="todo-actions">
<text class="todo-priority" :class="getTodoPriorityClass(todo.priority)">{{ getTodoPriorityText(todo.priority) }}</text>
<button class="todo-btn" @click.stop="completeTodo(todo.id)">完成</button>
</view>
</view>
</view>
<!-- 常用功能快捷入口 -->
<view class="shortcuts-section">
<text class="section-title">常用功能</text>
<view class="shortcuts-list">
<view class="shortcut-item" @click="goToConversationHistory">
<text class="shortcut-icon">📝</text>
<text class="shortcut-text">会话记录</text>
</view>
<view class="shortcut-item" @click="goToPerformanceReport">
<text class="shortcut-icon">📊</text>
<text class="shortcut-text">绩效报表</text>
</view>
<view class="shortcut-item" @click="goToTrainingCenter">
<text class="shortcut-icon">🎓</text>
<text class="shortcut-text">培训中心</text>
</view>
<view class="shortcut-item" @click="goToFeedback">
<text class="shortcut-icon">💡</text>
<text class="shortcut-text">意见反馈</text>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
type AgentInfoType = {
id: string
name: string
avatar_url: string | null
status: number
department: string
}
type TodayStatsType = {
handled_conversations: number
resolved_issues: number
avg_response_time: number
satisfaction_rate: number
}
type ConversationType = {
id: string
user_id: string
user_name: string
user_avatar: string | null
type: number
priority: number
last_message: string
last_message_time: string
created_at: string
}
type TodoItemType = {
id: string
title: string
description: string
priority: number
deadline: string
created_at: string
}
export default {
data() {
return {
isOnline: false,
workDuration: '0小时0分钟',
agentInfo: {
id: '',
name: '客服代表',
avatar_url: '',
status: 0,
department: '客服部'
} as AgentInfoType,
todayStats: {
handled_conversations: 0,
resolved_issues: 0,
avg_response_time: 0,
satisfaction_rate: 0
} as TodayStatsType,
conversations: [] as Array<ConversationType>,
todoItems: [] as Array<TodoItemType>
}
},
onLoad() {
this.loadAgentInfo()
this.loadTodayStats()
this.loadConversations()
this.loadTodoItems()
this.startWorkTimer()
},
onShow() {
this.refreshData()
},
methods: {
// 加载客服信息
loadAgentInfo() {
// TODO: 调用API获取客服信息
this.agentInfo.name = '张小美'
this.agentInfo.department = '在线客服部'
},
// 加载今日统计
loadTodayStats() {
// TODO: 调用API获取今日统计
this.todayStats = {
handled_conversations: 25,
resolved_issues: 23,
avg_response_time: 45,
satisfaction_rate: 96
}
},
// 加载会话列表
loadConversations() {
if (!this.isOnline) {
this.conversations = []
return
}
// TODO: 调用API获取待处理会话
this.conversations = [
{
id: '1',
user_id: '1',
user_name: '张先生',
user_avatar: '/static/user1.jpg',
type: 1,
priority: 2,
last_message: '我的订单什么时候能发货?',
last_message_time: '2025-01-08T15:30:00Z',
created_at: '2025-01-08T15:28:00Z'
},
{
id: '2',
user_id: '2',
user_name: '李女士',
user_avatar: '/static/user2.jpg',
type: 2,
priority: 3,
last_message: '我要申请退款,商品有质量问题',
last_message_time: '2025-01-08T15:25:00Z',
created_at: '2025-01-08T15:20:00Z'
}
]
},
// 加载待办事项
loadTodoItems() {
// TODO: 调用API获取待办事项
this.todoItems = [
{
id: '1',
title: '处理投诉单 #CS001',
description: '用户反馈配送延迟问题,需要跟进处理',
priority: 3,
deadline: '2025-01-08T18:00:00Z',
created_at: '2025-01-08T14:00:00Z'
}
]
},
// 刷新数据
refreshData() {
this.loadConversations()
this.loadTodoItems()
this.loadTodayStats()
},
// 开始工作计时
startWorkTimer() {
// TODO: 实现工作时长计时器
this.workDuration = '2小时15分钟'
},
// 切换工作状态
toggleWorkStatus(event: UniSwitchChangeEvent) {
this.isOnline = event.detail.value
if (this.isOnline) {
this.goOnline()
} else {
this.goOffline()
}
},
// 上线
goOnline() {
// TODO: 调用API上线
this.loadConversations()
uni.showToast({
title: '已上线接受咨询',
icon: 'success'
})
},
// 下线
goOffline() {
// TODO: 调用API下线
this.conversations = []
uni.showToast({
title: '已下线休息',
icon: 'none'
})
},
// 获取状态样式
getStatusClass(): string {
return this.isOnline ? 'status-online' : 'status-offline'
},
// 获取状态文本
getStatusText(): string {
return this.isOnline ? '在线' : '离线'
},
// 获取队列状态样式
getQueueStatusClass(): string {
if (this.conversations.length === 0) return 'queue-empty'
if (this.conversations.length > 5) return 'queue-busy'
return 'queue-normal'
},
// 获取队列状态文本
getQueueStatusText(): string {
if (this.conversations.length === 0) return '空闲'
if (this.conversations.length > 5) return '繁忙'
return '正常'
},
// 获取会话类型文本
getConversationTypeText(type: number): string {
switch (type) {
case 1: return '订单咨询'
case 2: return '退款申请'
case 3: return '商品咨询'
case 4: return '投诉建议'
default: return '一般咨询'
}
},
// 获取优先级样式
getPriorityClass(priority: number): string {
switch (priority) {
case 3: return 'priority-high'
case 2: return 'priority-medium'
default: return 'priority-low'
}
},
// 获取优先级文本
getPriorityText(priority: number): string {
switch (priority) {
case 3: return '紧急'
case 2: return '重要'
default: return '普通'
}
},
// 获取待办优先级样式
getTodoPriorityClass(priority: number): string {
switch (priority) {
case 3: return 'todo-high'
case 2: return 'todo-medium'
default: return 'todo-low'
}
},
// 获取待办优先级文本
getTodoPriorityText(priority: number): string {
switch (priority) {
case 3: return '高'
case 2: return '中'
default: return '低'
}
},
// 格式化等待时间
formatWaitTime(timeStr: string): string {
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / (1000 * 60))
if (minutes < 60) {
return `${minutes}分钟`
} else {
return `${Math.floor(minutes / 60)}小时${minutes % 60}分钟`
}
},
// 格式化时间
formatTime(timeStr: string): string {
const date = new Date(timeStr)
const now = new Date()
const diff = now.getTime() - date.getTime()
const minutes = Math.floor(diff / (1000 * 60))
if (minutes < 60) {
return `${minutes}分钟前`
} else {
return `${Math.floor(minutes / 60)}小时前`
}
},
// 格式化截止时间
formatDeadline(timeStr: string): string {
const date = new Date(timeStr)
return `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
},
// 会话操作
openConversation(conversationId: string) {
uni.navigateTo({
url: `/pages/mall/service/conversation?id=${conversationId}`
})
},
acceptConversation(conversationId: string) {
// TODO: 调用API接受会话
uni.showToast({
title: '已接受会话',
icon: 'success'
})
this.openConversation(conversationId)
},
viewUserProfile(userId: string) {
uni.navigateTo({
url: `/pages/mall/service/user-profile?id=${userId}`
})
},
// 待办事项操作
handleTodoItem(todo: TodoItemType) {
uni.navigateTo({
url: `/pages/mall/service/todo-detail?id=${todo.id}`
})
},
completeTodo(todoId: string) {
// TODO: 调用API完成待办
uni.showToast({
title: '待办已完成',
icon: 'success'
})
this.loadTodoItems()
},
// 导航方法
goToOrderInquiry() {
uni.navigateTo({
url: '/pages/mall/service/order-inquiry'
})
},
goToRefundProcess() {
uni.navigateTo({
url: '/pages/mall/service/refund-process'
})
},
goToComplaintHandle() {
uni.navigateTo({
url: '/pages/mall/service/complaint-handle'
})
},
goToKnowledgeBase() {
uni.navigateTo({
url: '/pages/mall/service/knowledge-base'
})
},
goToQuickReplies() {
uni.navigateTo({
url: '/pages/mall/service/quick-replies'
})
},
goToEscalation() {
uni.navigateTo({
url: '/pages/mall/service/escalation'
})
},
goToConversationHistory() {
uni.navigateTo({
url: '/pages/mall/service/conversation-history'
})
},
goToPerformanceReport() {
uni.navigateTo({
url: '/pages/mall/service/performance-report'
})
},
goToTrainingCenter() {
uni.navigateTo({
url: '/pages/mall/service/training-center'
})
},
goToFeedback() {
uni.navigateTo({
url: '/pages/mall/service/feedback'
})
}
}
}
</script>
<style>
.service-container {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 40rpx;
}
.header {
background: linear-gradient(135deg, #36D1DC 0%, #5B86E5 100%);
padding: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.agent-info {
display: flex;
align-items: center;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 20rpx;
}
.agent-details {
display: flex;
flex-direction: column;
}
.agent-name {
font-size: 32rpx;
font-weight: bold;
color: #fff;
margin-bottom: 8rpx;
}
.status-info {
display: flex;
align-items: center;
}
.status-badge {
font-size: 20rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
margin-right: 15rpx;
}
.status-online {
background-color: rgba(76, 175, 80, 0.2);
color: #4CAF50;
}
.status-offline {
background-color: rgba(255, 152, 0, 0.2);
color: #FF9800;
}
.work-time {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.8);
}
.status-controls {
display: flex;
flex-direction: column;
align-items: center;
}
.status-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
margin-top: 8rpx;
}
.stats-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
}
.stats-grid {
display: flex;
justify-content: space-between;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #36D1DC;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.queue-section {
margin: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.queue-status {
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 20rpx;
}
.queue-empty {
background-color: #E8F5E8;
color: #4CAF50;
}
.queue-normal {
background-color: #E3F2FD;
color: #2196F3;
}
.queue-busy {
background-color: #FFEBEE;
color: #F44336;
}
.empty-queue {
background-color: #fff;
padding: 80rpx 30rpx;
border-radius: 16rpx;
text-align: center;
}
.empty-icon {
font-size: 64rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-bottom: 15rpx;
}
.empty-subtitle {
font-size: 24rpx;
color: #ccc;
}
.conversation-item {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 15rpx;
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.user-info {
display: flex;
align-items: center;
}
.user-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
margin-right: 15rpx;
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-size: 28rpx;
color: #333;
margin-bottom: 5rpx;
}
.conversation-type {
font-size: 22rpx;
color: #666;
}
.conversation-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.priority-badge {
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
margin-bottom: 8rpx;
}
.priority-high {
background-color: #FFEBEE;
color: #F44336;
}
.priority-medium {
background-color: #FFF3E0;
color: #FF9800;
}
.priority-low {
background-color: #F3E5F5;
color: #9C27B0;
}
.wait-time {
font-size: 20rpx;
color: #999;
}
.conversation-preview {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
padding: 15rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
}
.last-message {
font-size: 26rpx;
color: #333;
flex: 1;
margin-right: 15rpx;
}
.message-time {
font-size: 20rpx;
color: #999;
}
.conversation-actions {
display: flex;
gap: 15rpx;
}
.action-btn {
flex: 1;
height: 70rpx;
border-radius: 8rpx;
font-size: 26rpx;
border: none;
}
.primary {
background-color: #36D1DC;
color: #fff;
}
.secondary {
background-color: #f0f0f0;
color: #333;
}
.tools-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.tools-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.tool-item {
width: 30%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 30rpx;
}
.tool-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
}
.tool-text {
font-size: 22rpx;
color: #333;
text-align: center;
}
.todo-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.todo-count {
font-size: 24rpx;
color: #666;
background-color: #f0f0f0;
padding: 8rpx 16rpx;
border-radius: 20rpx;
}
.empty-todo {
text-align: center;
padding: 40rpx;
}
.todo-item {
border: 1rpx solid #e5e5e5;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 15rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.todo-content {
display: flex;
flex-direction: column;
flex: 1;
}
.todo-title {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.todo-description {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.todo-deadline {
font-size: 22rpx;
color: #999;
}
.todo-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.todo-priority {
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
margin-bottom: 10rpx;
}
.todo-high {
background-color: #FFEBEE;
color: #F44336;
}
.todo-medium {
background-color: #FFF3E0;
color: #FF9800;
}
.todo-low {
background-color: #E8F5E8;
color: #4CAF50;
}
.todo-btn {
background-color: #36D1DC;
color: #fff;
border: none;
padding: 10rpx 20rpx;
border-radius: 6rpx;
font-size: 22rpx;
}
.shortcuts-section {
background-color: #fff;
margin: 20rpx;
padding: 30rpx;
border-radius: 16rpx;
}
.shortcuts-list {
display: flex;
justify-content: space-between;
}
.shortcut-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.shortcut-icon {
font-size: 48rpx;
margin-bottom: 15rpx;
}
.shortcut-text {
font-size: 24rpx;
color: #333;
text-align: center;
}
</style>