336 lines
11 KiB
TypeScript
336 lines
11 KiB
TypeScript
/**
|
|
* 客服会话相关模拟数据
|
|
*/
|
|
import type {
|
|
ConversationAgent,
|
|
ConversationListQuery,
|
|
ConversationListResult,
|
|
ConversationMessage,
|
|
ConversationMetrics,
|
|
ConversationPriority,
|
|
ConversationSession,
|
|
ConversationStatus,
|
|
ConversationTag
|
|
} from '@/types'
|
|
|
|
const supportAgents: ConversationAgent[] = [
|
|
{
|
|
id: 11,
|
|
name: '林清',
|
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=agent_lin',
|
|
title: '资深客服',
|
|
workload: 8,
|
|
expertise: ['会员权益', '支付问题']
|
|
},
|
|
{
|
|
id: 12,
|
|
name: '周晚',
|
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=agent_zhou',
|
|
title: '体验顾问',
|
|
workload: 5,
|
|
expertise: ['产品咨询', '活动政策']
|
|
},
|
|
{
|
|
id: 13,
|
|
name: '程斐',
|
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=agent_cheng',
|
|
title: '资深专家',
|
|
workload: 6,
|
|
expertise: ['订单售后', '履约异常']
|
|
},
|
|
{
|
|
id: 14,
|
|
name: '白凝',
|
|
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=agent_bai',
|
|
title: '运营支持',
|
|
workload: 3,
|
|
expertise: ['社群转接', '投诉升级']
|
|
}
|
|
]
|
|
|
|
const sessionTags: ConversationTag[] = [
|
|
{ id: 1, name: '支付', color: '#1890ff' },
|
|
{ id: 2, name: '权益', color: '#722ed1' },
|
|
{ id: 3, name: '功能', color: '#13c2c2' },
|
|
{ id: 4, name: '投诉', color: '#ff4d4f' },
|
|
{ id: 5, name: '活动', color: '#fa8c16' },
|
|
{ id: 6, name: 'BUG', color: '#a0d911' }
|
|
]
|
|
|
|
const intents = ['开票需求', '会员续费', '提现异常', '功能咨询', '活动规则', '体验反馈']
|
|
const sources = ['App 内反馈', '官网咨询', '小程序客服', '社群转接']
|
|
const customerLevels = ['L1', 'L2', 'L3', 'L4', 'L5']
|
|
const cities = ['杭州', '上海', '成都', '武汉', '广州', '深圳']
|
|
const customerNames = ['南鸢', '时年', '望舒', '青禾', '阿岚', '林舟', '潮生', '江迟', '枝夏', '安意']
|
|
|
|
const channelPool: Array<ConversationSession['channel']> = ['app', 'web', 'wechat', 'miniapp']
|
|
const priorityPool: ConversationPriority[] = ['normal', 'normal', 'high', 'vip']
|
|
const statusPool: ConversationStatus[] = ['waiting', 'active', 'pending', 'resolved', 'active', 'pending']
|
|
|
|
function randomItem<T>(arr: T[]): T {
|
|
return arr[Math.floor(Math.random() * arr.length)]!
|
|
}
|
|
|
|
function randomInt(min: number, max: number): number {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
}
|
|
|
|
function pickTags(): ConversationTag[] {
|
|
const count = randomInt(1, 3)
|
|
const shuffled = [...sessionTags].sort(() => Math.random() - 0.5)
|
|
return shuffled.slice(0, count)
|
|
}
|
|
|
|
function buildMessages(
|
|
customerName: string,
|
|
customerAvatar: string,
|
|
agent?: ConversationAgent,
|
|
startTime?: Date
|
|
): ConversationMessage[] {
|
|
const userMessages = [
|
|
'你好,我想咨询一下昨天支付的订单还没有到账是怎么回事?',
|
|
'我看到权益说明里有些不一致,麻烦帮我核实下。',
|
|
'现在打开功能的时候会提示网络错误,可以帮忙看看吗?',
|
|
'关于新的社群活动我有些疑问,规则是不是已经更新了?'
|
|
]
|
|
const agentMessages = [
|
|
'您好,已经为您核实到订单,目前支付渠道反馈处理中,大约 10 分钟内到账。',
|
|
'权益说明我们刚刚做了更新,我发一份最新版给您,您稍等查看。',
|
|
'收到,我们正在排查这个异常,稍后会第一时间同步处理进度。',
|
|
'关于活动规则我帮您确认了,新的版本中需要满足邀请条件才可报名。'
|
|
]
|
|
|
|
const messages: ConversationMessage[] = []
|
|
const base = startTime ? new Date(startTime) : new Date(Date.now() - randomInt(10, 72) * 60 * 1000)
|
|
let pointer = base.getTime()
|
|
const rounds = agent ? randomInt(3, 6) : randomInt(2, 4)
|
|
|
|
for (let i = 0; i < rounds; i++) {
|
|
const userMessage: ConversationMessage = {
|
|
id: Number(`${pointer}${i}`),
|
|
sender: 'user',
|
|
senderName: customerName,
|
|
content: randomItem(userMessages),
|
|
timestamp: new Date(pointer).toISOString(),
|
|
avatar: customerAvatar
|
|
}
|
|
messages.push(userMessage)
|
|
pointer += randomInt(1, 6) * 60 * 1000
|
|
if (agent) {
|
|
const agentMessage: ConversationMessage = {
|
|
id: Number(`${pointer}${i}`),
|
|
sender: 'agent',
|
|
senderName: agent.name,
|
|
content: randomItem(agentMessages),
|
|
timestamp: new Date(pointer).toISOString(),
|
|
avatar: agent.avatar
|
|
}
|
|
messages.push(agentMessage)
|
|
pointer += randomInt(2, 8) * 60 * 1000
|
|
}
|
|
}
|
|
|
|
return messages
|
|
}
|
|
|
|
function generateMockConversations(): ConversationSession[] {
|
|
const sessions: ConversationSession[] = []
|
|
for (let i = 1; i <= 36; i++) {
|
|
const customer = {
|
|
id: 2000 + i,
|
|
nickname: customerNames[i % customerNames.length]!,
|
|
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=session_${i}`,
|
|
city: randomItem(cities),
|
|
vip: Math.random() > 0.7,
|
|
level: randomItem(customerLevels),
|
|
intents: [randomItem(intents)]
|
|
}
|
|
const assignedAgent = Math.random() > 0.2 ? randomItem(supportAgents) : undefined
|
|
let status = randomItem(statusPool)
|
|
if (!assignedAgent && status !== 'waiting') {
|
|
status = 'waiting'
|
|
}
|
|
const createdAt = new Date(Date.now() - randomInt(1, 7) * 24 * 60 * 60 * 1000)
|
|
const messages = buildMessages(customer.nickname, customer.avatar, assignedAgent, createdAt)
|
|
const lastMessage = messages[messages.length - 1]!
|
|
const firstAgentMessage = messages.find(msg => msg.sender === 'agent')
|
|
const priority = randomItem(priorityPool)
|
|
|
|
sessions.push({
|
|
id: i,
|
|
sessionCode: `KF${202400 + i}`,
|
|
channel: randomItem(channelPool),
|
|
status,
|
|
priority,
|
|
createdAt: messages[0]!.timestamp,
|
|
lastMessageAt: lastMessage.timestamp,
|
|
lastMessage: lastMessage.content,
|
|
unreadCount: status === 'waiting' ? randomInt(1, 6) : Math.max(0, randomInt(0, 3) - 1),
|
|
totalMessages: messages.length,
|
|
waitingTime: status === 'waiting' ? randomInt(5, 25) : randomInt(1, 10),
|
|
firstResponseAt: firstAgentMessage?.timestamp,
|
|
resolvedAt: status === 'resolved' ? lastMessage.timestamp : undefined,
|
|
satisfaction: status === 'resolved' ? Number((4.2 + Math.random() * 0.7).toFixed(1)) : undefined,
|
|
autoDetectedIntent: randomItem(intents),
|
|
source: randomItem(sources),
|
|
tags: pickTags(),
|
|
customer,
|
|
assignedAgent: assignedAgent ? { ...assignedAgent } : undefined,
|
|
messages
|
|
})
|
|
}
|
|
|
|
return sessions.sort((a, b) => new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime())
|
|
}
|
|
|
|
let conversationSessions: ConversationSession[] = generateMockConversations()
|
|
|
|
function delay(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
}
|
|
|
|
function buildMetrics(sessions: ConversationSession[]): ConversationMetrics {
|
|
const waitingCount = sessions.filter(session => session.status === 'waiting').length
|
|
const activeCount = sessions.filter(session => session.status === 'active').length
|
|
const pendingCount = sessions.filter(session => session.status === 'pending').length
|
|
const resolvedToday = sessions.filter(session => {
|
|
if (!session.resolvedAt) return false
|
|
const resolvedDate = new Date(session.resolvedAt)
|
|
const now = new Date()
|
|
return (
|
|
resolvedDate.getFullYear() === now.getFullYear() &&
|
|
resolvedDate.getMonth() === now.getMonth() &&
|
|
resolvedDate.getDate() === now.getDate()
|
|
)
|
|
}).length
|
|
|
|
const responseDiffs = sessions
|
|
.filter(session => session.firstResponseAt)
|
|
.map(session => new Date(session.firstResponseAt!).getTime() - new Date(session.createdAt).getTime())
|
|
|
|
const satisfactionScores = sessions
|
|
.filter(session => session.satisfaction)
|
|
.map(session => session.satisfaction!)
|
|
|
|
const avgFirstResponse =
|
|
responseDiffs.length > 0 ? Math.round(responseDiffs.reduce((a, b) => a + b, 0) / responseDiffs.length / 60000) : 0
|
|
const satisfaction =
|
|
satisfactionScores.length > 0
|
|
? Number((satisfactionScores.reduce((a, b) => a + b, 0) / satisfactionScores.length).toFixed(1))
|
|
: 0
|
|
|
|
return {
|
|
waitingCount,
|
|
activeCount,
|
|
pendingCount,
|
|
resolvedToday,
|
|
satisfaction,
|
|
avgFirstResponse
|
|
}
|
|
}
|
|
|
|
export async function mockGetConversationList(
|
|
params: ConversationListQuery = {}
|
|
): Promise<ConversationListResult> {
|
|
await delay(250)
|
|
const {
|
|
page = 1,
|
|
pageSize = 10,
|
|
keyword,
|
|
status = 'all',
|
|
channel = 'all',
|
|
priority = 'all',
|
|
vipOnly,
|
|
dateRange
|
|
} = params
|
|
|
|
let filtered = [...conversationSessions]
|
|
|
|
if (keyword) {
|
|
filtered = filtered.filter(session =>
|
|
[session.sessionCode, session.customer.nickname, session.lastMessage, session.assignedAgent?.name || '']
|
|
.join(' ')
|
|
.toLowerCase()
|
|
.includes(keyword.toLowerCase())
|
|
)
|
|
}
|
|
|
|
if (status !== 'all') {
|
|
filtered = filtered.filter(session => session.status === status)
|
|
}
|
|
|
|
if (channel !== 'all') {
|
|
filtered = filtered.filter(session => session.channel === channel)
|
|
}
|
|
|
|
if (priority !== 'all') {
|
|
filtered = filtered.filter(session => session.priority === priority)
|
|
}
|
|
|
|
if (vipOnly) {
|
|
filtered = filtered.filter(session => session.customer.vip)
|
|
}
|
|
|
|
if (dateRange && dateRange.length === 2) {
|
|
const [start, end] = dateRange
|
|
const startTime = new Date(start).getTime()
|
|
const endTime = new Date(end).getTime()
|
|
filtered = filtered.filter(session => {
|
|
const created = new Date(session.createdAt).getTime()
|
|
return created >= startTime && created <= endTime
|
|
})
|
|
}
|
|
|
|
const start = (page - 1) * pageSize
|
|
const list = filtered.slice(start, start + pageSize)
|
|
|
|
return {
|
|
list,
|
|
total: filtered.length,
|
|
page,
|
|
pageSize,
|
|
metrics: buildMetrics(conversationSessions)
|
|
}
|
|
}
|
|
|
|
export async function mockUpdateConversationStatus(
|
|
sessionId: number,
|
|
status: ConversationStatus
|
|
): Promise<ConversationSession | undefined> {
|
|
await delay(200)
|
|
const target = conversationSessions.find(session => session.id === sessionId)
|
|
if (!target) return undefined
|
|
target.status = status
|
|
if (status === 'resolved' || status === 'closed') {
|
|
target.resolvedAt = new Date().toISOString()
|
|
target.unreadCount = 0
|
|
target.waitingTime = 0
|
|
}
|
|
if (status !== 'waiting' && !target.firstResponseAt) {
|
|
target.firstResponseAt = new Date().toISOString()
|
|
}
|
|
return target
|
|
}
|
|
|
|
export async function mockAssignConversationAgent(
|
|
sessionId: number,
|
|
agentId: number
|
|
): Promise<ConversationSession | undefined> {
|
|
await delay(200)
|
|
const target = conversationSessions.find(session => session.id === sessionId)
|
|
const agent = supportAgents.find(item => item.id === agentId)
|
|
if (!target || !agent) return undefined
|
|
target.assignedAgent = { ...agent }
|
|
if (target.status === 'waiting') {
|
|
target.status = 'active'
|
|
}
|
|
if (!target.firstResponseAt) {
|
|
target.firstResponseAt = new Date().toISOString()
|
|
}
|
|
return target
|
|
}
|
|
|
|
export function mockGetSupportAgents(): ConversationAgent[] {
|
|
return supportAgents.map(agent => ({ ...agent }))
|
|
}
|