实现P1业务模块:客户管理、对话记录、知识库、数据统计页面

This commit is contained in:
yml2213
2026-07-14 11:13:02 +08:00
parent 2492daae3b
commit a1ba0475ac
4 changed files with 652 additions and 20 deletions
+181 -5
View File
@@ -1,6 +1,182 @@
const ChatHistory = () => (
<div className="h-full flex items-center justify-center text-neutral-400 text-center">
<div><div className="text-lg mb-1"></div><div className="text-sm">...</div></div>
</div>
)
import { useState } from 'react'
import { Input, Select, DatePicker, Tag, Empty } 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' },
],
},
]
const statusColors: Record<string, string> = { active: 'blue', ended: 'green', archived: 'default' }
const statusLabels: Record<string, string> = { active: '进行中', ended: '已结束', archived: '已归档' }
const ChatHistory = () => {
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 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
})
const selected = mockRecords.find(r => r.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>
</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)}
>
<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>
<Tag color={statusColors[r.status]} className="text-xs">{statusLabels[r.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>}
</div>
</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>
</div>
<div className="text-sm text-neutral-400 mt-1">{selected.channel} · {selected.agent} · {selected.time}</div>
</div>
{selected.satisfaction && (
<div className="text-right">
<div className="text-xl text-yellow-500">{'★'.repeat(selected.satisfaction)}{'☆'.repeat(5 - selected.satisfaction)}</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>
</div>
) : (
<div className="h-full flex items-center justify-center text-neutral-400"></div>
)}
</div>
</div>
)
}
export default ChatHistory