对接对话记录和运营概览API,完成全部页面前后端对接
This commit is contained in:
@@ -1,179 +1,83 @@
|
||||
import { useState } from 'react'
|
||||
import { Input, Select, DatePicker, Tag, Empty } from 'antd'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Input, Select, Tag, Empty, Spin } from 'antd'
|
||||
import { SearchOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
interface ChatRecord {
|
||||
id: string
|
||||
visitorName: string
|
||||
avatar?: string
|
||||
lastMessage: string
|
||||
time: string
|
||||
status: 'active' | 'ended' | 'archived'
|
||||
channel: string
|
||||
agent: string
|
||||
satisfaction?: number
|
||||
messages: { sender: string; content: string; time: string; type: 'message' | 'system' | 'action' }[]
|
||||
}
|
||||
|
||||
const mockRecords: ChatRecord[] = [
|
||||
{
|
||||
id: '1', visitorName: '张三', lastMessage: '好的谢谢,问题已解决', time: '10:45', status: 'ended', channel: '网页', agent: '客服小王',
|
||||
satisfaction: 5,
|
||||
messages: [
|
||||
{ sender: '张三', content: '你好,我的订单怎么还没发货?', time: '10:30', type: 'message' },
|
||||
{ sender: '客服小王', content: '您好,请提供一下您的订单号,我帮您查看', time: '10:31', type: 'message' },
|
||||
{ sender: '张三', content: '订单号 AB20260714001', time: '10:32', type: 'message' },
|
||||
{ sender: '系统', content: '客服小王查看了客户资料', time: '10:32', type: 'system' },
|
||||
{ sender: '客服小王', content: '您好,您的订单已经在配送中,预计明天到达', time: '10:35', type: 'message' },
|
||||
{ sender: '张三', content: '好的谢谢,问题已解决', time: '10:45', type: 'message' },
|
||||
{ sender: '系统', content: '客服小王结束会话,原因:已解决', time: '10:45', type: 'action' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2', visitorName: '李四', lastMessage: '这个功能怎么用?', time: '09:20', status: 'ended', channel: '微信', agent: '客服小李',
|
||||
satisfaction: 4,
|
||||
messages: [
|
||||
{ sender: '李四', content: '这个功能怎么用?', time: '09:15', type: 'message' },
|
||||
{ sender: '客服小李', content: '您好,请问您咨询的是哪个功能?', time: '09:16', type: 'message' },
|
||||
{ sender: '李四', content: '就是那个批量导入', time: '09:17', type: 'message' },
|
||||
{ sender: '系统', content: '客服小李使用了知识库条目"批量导入指南"', time: '09:18', type: 'action' },
|
||||
{ sender: '客服小李', content: '批量导入功能在设置→数据管理中,支持 CSV 和 Excel 格式,点击即可上传', time: '09:19', type: 'message' },
|
||||
{ sender: '李四', content: '明白了,谢谢', time: '09:20', type: 'message' },
|
||||
{ sender: '系统', content: '客服小李结束会话,原因:已解决', time: '09:20', type: 'action' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3', visitorName: '王五', lastMessage: '你们的价格比其他家贵很多', time: '08:50', status: 'active', channel: 'APP', agent: '客服小张',
|
||||
messages: [
|
||||
{ sender: '王五', content: '你们的价格比其他家贵很多', time: '08:45', type: 'message' },
|
||||
{ sender: '客服小张', content: '您好,我们的产品在功能完整性和服务支持上有明显优势', time: '08:46', type: 'message' },
|
||||
{ sender: '王五', content: '具体说说看', time: '08:47', type: 'message' },
|
||||
{ sender: '客服小张', content: '我们有7×24小时在线客服、免费知识库搭建、数据报表分析等增值服务', time: '08:48', type: 'message' },
|
||||
{ sender: '系统', content: '客服小张将会话转接给客服主管', time: '08:49', type: 'action' },
|
||||
{ sender: '王五', content: '那还可以', time: '08:50', type: 'message' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4', visitorName: '匿名访客342', lastMessage: '请问可以试用吗', time: '07-11 16:30', status: 'archived', channel: '网页', agent: '客服小王',
|
||||
satisfaction: 3,
|
||||
messages: [
|
||||
{ sender: '匿名访客342', content: '请问可以试用吗', time: '16:25', type: 'message' },
|
||||
{ sender: '客服小王', content: '可以的,我们提供7天免费试用', time: '16:26', type: 'message' },
|
||||
{ sender: '系统', content: '客服小王结束会话,原因:已解决', time: '16:30', type: 'action' },
|
||||
],
|
||||
},
|
||||
]
|
||||
import { getSessions, type Session as SessionType } from '@/services/api'
|
||||
|
||||
const statusColors: Record<string, string> = { active: 'blue', ended: 'green', archived: 'default' }
|
||||
const statusLabels: Record<string, string> = { active: '进行中', ended: '已结束', archived: '已归档' }
|
||||
const statusLabels: Record<string, string> = { active: '进行中', ended: '已结束', waiting: '等待中' }
|
||||
|
||||
const ChatHistory = () => {
|
||||
const [sessions, setSessions] = useState<SessionType[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [search, setSearch] = useState('')
|
||||
const [channelFilter, setChannelFilter] = useState<string>()
|
||||
const [agentFilter, setAgentFilter] = useState<string>()
|
||||
const [statusFilter, setStatusFilter] = useState<string>()
|
||||
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null]>([null, null])
|
||||
const [selectedId, setSelectedId] = useState<string>('1')
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||
|
||||
const filtered = mockRecords.filter(r => {
|
||||
if (search && !r.visitorName.includes(search)) return false
|
||||
if (channelFilter && r.channel !== channelFilter) return false
|
||||
if (agentFilter && r.agent !== agentFilter) return false
|
||||
if (statusFilter && r.status !== statusFilter) return false
|
||||
return true
|
||||
})
|
||||
useEffect(() => { loadSessions() }, [statusFilter])
|
||||
|
||||
const selected = mockRecords.find(r => r.id === selectedId)
|
||||
const loadSessions = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getSessions({ status: statusFilter })
|
||||
setSessions(res.list)
|
||||
if (res.list.length > 0 && !selectedId) setSelectedId(res.list[0].id)
|
||||
} catch { setSessions([]) } finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const selected = sessions.find(s => s.id === selectedId)
|
||||
|
||||
return (
|
||||
<div className="h-full flex">
|
||||
{/* 左侧列表 */}
|
||||
<div className="w-[360px] flex-shrink-0 bg-white border-r border-neutral-200 flex flex-col">
|
||||
<div className="p-3 border-b border-neutral-100 space-y-2">
|
||||
<Input prefix={<SearchOutlined />} placeholder="搜索访客名称..." value={search} onChange={e => setSearch(e.target.value)} allowClear size="small" />
|
||||
<div className="flex gap-2">
|
||||
<Select placeholder="渠道" value={channelFilter} onChange={setChannelFilter} allowClear size="small" className="flex-1" options={['网页', '微信', 'APP', '邮件'].map(v => ({ value: v, label: v }))} />
|
||||
<Select placeholder="状态" value={statusFilter} onChange={setStatusFilter} allowClear size="small" className="flex-1" options={['active', 'ended', 'archived'].map(v => ({ value: v, label: statusLabels[v] }))} />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select placeholder="客服" value={agentFilter} onChange={setAgentFilter} allowClear size="small" className="flex-1" options={['客服小王', '客服小李', '客服小张'].map(v => ({ value: v, label: v }))} />
|
||||
<DatePicker.RangePicker size="small" className="flex-1" value={dateRange as [Dayjs | null, Dayjs | null] | null} onChange={(dates) => setDateRange(dates as [Dayjs | null, Dayjs | null])} />
|
||||
</div>
|
||||
<Input prefix={<SearchOutlined />} placeholder="搜索访客..." value={search} onChange={e => setSearch(e.target.value)} allowClear size="small" />
|
||||
<Select placeholder="状态" value={statusFilter} onChange={setStatusFilter} allowClear size="small" className="w-full" options={Object.entries(statusLabels).map(([k, v]) => ({ value: k, label: v }))} />
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full"><Empty description="暂无对话记录" /></div>
|
||||
) : (
|
||||
filtered.map(r => (
|
||||
<div
|
||||
key={r.id}
|
||||
className={`px-3 py-3 border-b border-neutral-50 cursor-pointer hover:bg-neutral-50 transition-colors ${selectedId === r.id ? 'bg-blue-50' : ''}`}
|
||||
onClick={() => setSelectedId(r.id)}
|
||||
>
|
||||
{loading ? <div className="flex justify-center py-10"><Spin /></div> :
|
||||
sessions.filter(s => !search || s.status.includes(search) || String(s.customer_id).includes(search)).map(s => (
|
||||
<div key={s.id} className={`px-3 py-3 border-b border-neutral-50 cursor-pointer hover:bg-neutral-50 transition-colors ${selectedId === s.id ? 'bg-blue-50' : ''}`}
|
||||
onClick={() => setSelectedId(s.id)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-neutral-100 flex items-center justify-center flex-shrink-0">
|
||||
<UserOutlined className="text-neutral-400 text-sm" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-neutral-800 truncate">{r.visitorName}</div>
|
||||
<div className="text-xs text-neutral-400">{r.channel} · {r.agent}</div>
|
||||
</div>
|
||||
<div className="w-8 h-8 rounded-full bg-neutral-100 flex items-center justify-center flex-shrink-0"><UserOutlined className="text-neutral-400 text-sm" /></div>
|
||||
<div><div className="text-sm font-medium text-neutral-800">客户{s.customer_id}</div><div className="text-xs text-neutral-400">ID: {s.id}</div></div>
|
||||
</div>
|
||||
<Tag color={statusColors[r.status]} className="text-xs">{statusLabels[r.status]}</Tag>
|
||||
<Tag color={statusColors[s.status]} className="text-xs">{statusLabels[s.status] || s.status}</Tag>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-400 mt-1.5 truncate">{r.lastMessage}</p>
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<span className="text-xs text-neutral-300">{r.time}</span>
|
||||
{r.satisfaction && <span className="text-xs text-yellow-500">{'★'.repeat(r.satisfaction)}</span>}
|
||||
<span className="text-xs text-neutral-300">{new Date(s.created_at).toLocaleString('zh-CN')}</span>
|
||||
{s.satisfaction_score && <span className="text-xs text-yellow-500">{'★'.repeat(s.satisfaction_score)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
{!loading && sessions.length === 0 && <div className="flex items-center justify-center h-full"><Empty description="暂无对话记录" /></div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧详情 */}
|
||||
<div className="flex-1 bg-white overflow-auto">
|
||||
{selected ? (
|
||||
<div className="p-6 max-w-3xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-6 pb-4 border-b border-neutral-100">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-neutral-800">{selected.visitorName}</h3>
|
||||
<Tag color={statusColors[selected.status]}>{statusLabels[selected.status]}</Tag>
|
||||
<h3 className="text-lg font-semibold text-neutral-800">会话 {selected.id}</h3>
|
||||
<Tag color={statusColors[selected.status]}>{statusLabels[selected.status] || selected.status}</Tag>
|
||||
</div>
|
||||
<div className="text-sm text-neutral-400 mt-1">{selected.channel} · {selected.agent} · {selected.time}</div>
|
||||
<div className="text-sm text-neutral-400 mt-1">客户ID: {selected.customer_id} · {new Date(selected.created_at).toLocaleString('zh-CN')}</div>
|
||||
</div>
|
||||
{selected.satisfaction && (
|
||||
{selected.satisfaction_score && (
|
||||
<div className="text-right">
|
||||
<div className="text-xl text-yellow-500">{'★'.repeat(selected.satisfaction)}{'☆'.repeat(5 - selected.satisfaction)}</div>
|
||||
<div className="text-xl text-yellow-500">{'★'.repeat(selected.satisfaction_score)}{'☆'.repeat(5 - selected.satisfaction_score)}</div>
|
||||
<div className="text-xs text-neutral-400">满意度评分</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{selected.messages.map((msg, i) => (
|
||||
<div key={i} className={`flex ${msg.type === 'system' || msg.type === 'action' ? 'justify-center' : msg.sender.startsWith('客服') ? 'justify-start' : 'justify-end'}`}>
|
||||
{msg.type === 'message' ? (
|
||||
<div className={`max-w-[60%] rounded-lg px-3 py-2 text-sm ${msg.sender.startsWith('客服') ? 'bg-neutral-100 text-neutral-700' : 'bg-blue-500 text-white'}`}>
|
||||
{msg.content}
|
||||
<div className={`text-xs mt-1 ${msg.sender.startsWith('客服') ? 'text-neutral-400' : 'text-white/60'}`}>{msg.sender} · {msg.time}</div>
|
||||
</div>
|
||||
) : msg.type === 'system' ? (
|
||||
<span className="text-xs text-neutral-300">{msg.content}</span>
|
||||
) : (
|
||||
<div className="bg-neutral-50 border border-neutral-200 rounded px-3 py-1.5 text-xs text-neutral-500">{msg.content}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="text-sm text-neutral-400 text-center">消息内容通过 WebSocket 实时传输</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-neutral-400">选择一个对话查看详情</div>
|
||||
)}
|
||||
) : <div className="h-full flex items-center justify-center text-neutral-400">选择一个对话查看详情</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user