对接前后端:种子数据填充、Dashboard和客户管理页面接入API

This commit is contained in:
yml2213
2026-07-14 11:32:35 +08:00
parent 6d437b4e82
commit 1f9915ff5a
3 changed files with 356 additions and 249 deletions
+168 -128
View File
@@ -1,186 +1,226 @@
import { useState } from 'react'
import { Badge, Input, Tooltip } from 'antd'
import { useState, useEffect } from 'react'
import { Input, Spin, message } from 'antd'
import { SearchOutlined, UserOutlined, StarFilled } from '@ant-design/icons'
import { useAuth } from '@/stores/auth'
import { getSessions, getSession, type Session as SessionType } from '@/services/api'
interface Session {
id: string
name: string
avatar?: string
lastMessage: string
time: string
priority: 'urgent' | 'waiting' | 'active'
unread: number
interface SessionDetail {
id: number
customerName: string
phone: string
email: string
source: string
tags: string[]
status: string
conversationCount: number
satisfaction: number
note: string
messages: { sender: string; content: string; time: string }[]
}
const mockSessions: Session[] = [
{ id: '1', name: '张三', lastMessage: '我的订单什么时候发货?', time: '10:32', priority: 'urgent', unread: 3 },
{ id: '2', name: '李四', lastMessage: '怎么退货啊', time: '10:28', priority: 'active', unread: 0 },
{ id: '3', name: '王五', lastMessage: '你们这个产品好用吗', time: '10:15', priority: 'active', unread: 1 },
{ id: '4', name: '赵六科技', lastMessage: '企业版价格能否优惠', time: '09:58', priority: 'waiting', unread: 0 },
{ id: '5', name: '钱七', lastMessage: '谢谢,问题已解决', time: '09:45', priority: 'active', unread: 0 },
]
const priorityColors = { urgent: '#dc2626', waiting: '#d97706', active: '#2563eb' }
const priorityLabels = { urgent: '紧急', waiting: '等待中', active: '进行中' }
const priorityColors: Record<string, string> = { urgent: '#dc2626', normal: '#d97706', waiting: '#d97706' }
const priorityLabels: Record<string, string> = { urgent: '紧急', waiting: '等待中', active: '进行中', normal: '进行中' }
const Dashboard = () => {
const [selectedId, setSelectedId] = useState<string>('1')
const [message, setMessage] = useState('')
const { user } = useAuth()
const [sessions, setSessions] = useState<SessionType[]>([])
const [loading, setLoading] = useState(true)
const [selectedId, setSelectedId] = useState<number | null>(null)
const [detail, setDetail] = useState<SessionDetail | null>(null)
const [detailLoading, setDetailLoading] = useState(false)
const [messageInput, setMessageInput] = useState('')
const selectedSession = mockSessions.find(s => s.id === selectedId)
useEffect(() => {
loadSessions()
}, [])
const loadSessions = async () => {
try {
setLoading(true)
const res = await getSessions()
setSessions(res.list)
if (res.list.length > 0 && !selectedId) {
setSelectedId(res.list[0].id)
}
} catch {
// fallback to empty
} finally {
setLoading(false)
}
}
useEffect(() => {
if (selectedId) {
loadDetail(selectedId)
}
}, [selectedId])
const loadDetail = async (id: number) => {
setDetailLoading(true)
try {
const res = await getSession(id)
const { session } = res.data
setDetail({
id: session.id,
customerName: `客户${session.customer_id}`,
phone: '获取中...',
email: '',
source: '网页',
tags: session.priority === 'urgent' ? ['VIP'] : [],
status: session.status,
conversationCount: 0,
satisfaction: session.satisfaction_score || 0,
note: '',
messages: (res.data as any).messages?.map((m: any) => ({
sender: m.sender_type === 'agent' ? `客服(${m.sender_id})` : '客户',
content: m.content,
time: new Date(m.sent_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
})) || [],
})
} catch {
message.error('加载会话详情失败')
} finally {
setDetailLoading(false)
}
}
const handleSend = () => {
if (!messageInput.trim()) return
setMessageInput('')
message.info('WebSocket 消息发送(待连接)')
}
if (loading) {
return <div className="h-full flex items-center justify-center"><Spin size="large" /></div>
}
return (
<div className="h-full flex">
{/* 左侧:会话列表 */}
<div className="w-[300px] flex-shrink-0 border-r border-neutral-200 bg-white flex flex-col">
<div className="p-3 border-b border-neutral-100">
<Input prefix={<SearchOutlined />} placeholder="搜索会话..." variant="borderless" />
<div className="px-3 py-3 border-b border-neutral-100">
<div className="text-sm font-medium text-neutral-800">{user?.nickname || '客服'}</div>
<div className="text-xs text-neutral-400">线</div>
</div>
<div className="flex px-3 py-2 gap-2 border-b border-neutral-100">
{(['urgent', 'waiting', 'active'] as const).map(p => (
<span key={p} className="text-xs px-2 py-0.5 rounded-full cursor-pointer hover:bg-neutral-100" style={{ color: priorityColors[p] }}>
{priorityLabels[p]}
</span>
))}
<div className="p-3 border-b border-neutral-100">
<Input prefix={<SearchOutlined />} placeholder="搜索会话..." variant="borderless" size="small" />
</div>
<div className="flex-1 overflow-auto">
{mockSessions.map(session => (
<div
key={session.id}
className={`px-3 py-2.5 cursor-pointer border-b border-neutral-50 hover:bg-neutral-50 transition-colors ${selectedId === session.id ? 'bg-blue-50' : ''}`}
onClick={() => setSelectedId(session.id)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="inline-block w-2 h-2 rounded-full" style={{ backgroundColor: priorityColors[session.priority] }} />
<span className="text-sm font-medium text-neutral-800">{session.name}</span>
</div>
<div className="flex items-center gap-1">
{session.unread > 0 && <Badge count={session.unread} size="small" />}
<span className="text-xs text-neutral-400">{session.time}</span>
{sessions.length === 0 ? (
<div className="flex items-center justify-center h-full text-neutral-400 text-sm"></div>
) : (
sessions.map(s => (
<div
key={s.id}
className={`px-3 py-2.5 cursor-pointer border-b border-neutral-50 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">
<span className="inline-block w-2 h-2 rounded-full" style={{ backgroundColor: priorityColors[s.priority] || '#2563eb' }} />
<span className="text-sm font-medium text-neutral-800">{s.customer_id}</span>
</div>
<span className="text-xs text-neutral-300">{new Date(s.created_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}</span>
</div>
<p className="text-xs text-neutral-400 mt-1 truncate pl-4">
<TagBadge status={s.status} priority={s.priority} />
</p>
</div>
<p className="text-xs text-neutral-400 mt-1 truncate pl-4">{session.lastMessage}</p>
</div>
))}
))
)}
</div>
</div>
{/* 中间:聊天区 */}
<div className="flex-1 flex flex-col min-w-0 bg-white">
{selectedSession ? (
{detail ? (
<>
<div className="h-12 px-4 flex items-center justify-between border-b border-neutral-100 flex-shrink-0">
<span className="text-sm font-medium text-neutral-800">{selectedSession.name}</span>
<span className="text-sm font-medium text-neutral-800">{detail.customerName}</span>
<div className="flex gap-2">
<Tooltip title="转接"><span className="text-neutral-400 cursor-pointer hover:text-neutral-600 text-sm"></span></Tooltip>
<Tooltip title="结束会话"><span className="text-neutral-400 cursor-pointer hover:text-red-500 text-sm"></span></Tooltip>
<span className="text-xs text-neutral-400 cursor-pointer hover:text-neutral-600"></span>
<span className="text-xs text-neutral-400 cursor-pointer hover:text-red-500"></span>
</div>
</div>
<div className="flex-1 overflow-auto p-4 flex flex-col gap-3">
<div className="flex flex-col items-start">
<div className="bg-neutral-100 rounded-lg px-3 py-2 text-sm text-neutral-700 max-w-[70%]">
</div>
<span className="text-xs text-neutral-400 mt-0.5">10:30</span>
</div>
<div className="flex flex-col items-end">
<div className="bg-blue-500 rounded-lg px-3 py-2 text-sm text-white max-w-[70%]">
</div>
<span className="text-xs text-neutral-400 mt-0.5">10:31</span>
</div>
<div className="flex flex-col items-start">
<div className="bg-neutral-100 rounded-lg px-3 py-2 text-sm text-neutral-700 max-w-[70%]">
AB20260714001
</div>
<span className="text-xs text-neutral-400 mt-0.5">10:31</span>
</div>
<div className="flex flex-col items-end">
<div className="bg-blue-500 rounded-lg px-3 py-2 text-sm text-white max-w-[70%]">
...
</div>
<span className="text-xs text-neutral-400 mt-0.5">10:32</span>
</div>
{detailLoading ? (
<div className="flex-1 flex items-center justify-center"><Spin /></div>
) : detail.messages.length === 0 ? (
<div className="flex-1 flex items-center justify-center text-neutral-400 text-sm"></div>
) : (
detail.messages.map((msg, i) => (
<div key={i} className={`flex ${msg.sender.startsWith('客服') ? 'justify-start' : 'justify-end'}`}>
<div className={`max-w-[70%] 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.time}</div>
</div>
</div>
))
)}
</div>
<div className="p-3 border-t border-neutral-100 flex-shrink-0">
<div className="flex items-center gap-2 bg-neutral-50 rounded-lg px-3 py-2">
<input
className="flex-1 bg-transparent outline-none text-sm text-neutral-700 placeholder:text-neutral-400"
placeholder="输入消息... (Enter 发送)"
value={message}
onChange={e => setMessage(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && message.trim()) {
setMessage('')
}
}}
value={messageInput}
onChange={e => setMessageInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') handleSend() }}
/>
<span className="text-neutral-300 cursor-pointer hover:text-neutral-500">😊</span>
<span className="text-neutral-300 cursor-pointer hover:text-neutral-500">📎</span>
</div>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-neutral-400">
</div>
<div className="flex-1 flex items-center justify-center text-neutral-400"></div>
)}
</div>
{/* 右侧:客户信息面板 */}
<div className="w-[300px] flex-shrink-0 border-l border-neutral-200 bg-white overflow-auto">
<div className="p-4 border-b border-neutral-100">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
<UserOutlined className="text-blue-500" />
{detail ? (
<div className="p-4 space-y-4">
<div className="flex items-center gap-3 pb-3 border-b border-neutral-100">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
<UserOutlined className="text-blue-500" />
</div>
<div>
<div className="text-sm font-medium text-neutral-800">{detail.customerName}</div>
<div className="text-xs text-neutral-400">ID: {detail.id}</div>
</div>
</div>
<div>
<div className="text-sm font-medium text-neutral-800">{selectedSession?.name || '访客'}</div>
<div className="text-xs text-neutral-400 mt-0.5"></div>
</div>
</div>
</div>
<div className="p-4 space-y-4">
<div>
<div className="text-xs text-neutral-400 mb-1.5"></div>
<div className="flex flex-wrap gap-1">
<span className="text-xs px-2 py-0.5 rounded bg-orange-50 text-orange-600">VIP客户</span>
<span className="text-xs px-2 py-0.5 rounded bg-blue-50 text-blue-600"></span>
</div>
</div>
<div>
<div className="text-xs text-neutral-400 mb-1.5"></div>
<div className="text-sm text-neutral-700 space-y-1">
<div>📱 138****8888</div>
<div>📧 zhang***@example.com</div>
</div>
</div>
<div>
<div className="text-xs text-neutral-400 mb-1.5"></div>
<div className="grid grid-cols-2 gap-2">
<div className="bg-neutral-50 rounded p-2 text-center">
<div className="text-lg font-semibold text-neutral-800">12</div>
<div className="text-xs text-neutral-400"></div>
<div className="text-xs text-neutral-400 mb-1.5"></div>
<div className="flex flex-wrap gap-1">
{detail.tags.map(t => <span key={t} className="text-xs px-2 py-0.5 rounded bg-blue-50 text-blue-600">{t}</span>)}
</div>
<div className="bg-neutral-50 rounded p-2 text-center">
<div className="text-lg font-semibold text-green-600 flex items-center justify-center gap-0.5">
4.8 <StarFilled className="text-xs" />
</div>
<div>
<div className="text-xs text-neutral-400 mb-1.5"></div>
<div className="grid grid-cols-2 gap-2">
<div className="bg-neutral-50 rounded p-2 text-center">
<div className="text-lg font-semibold text-neutral-800">{detail.conversationCount}</div>
<div className="text-xs text-neutral-400"></div>
</div>
<div className="bg-neutral-50 rounded p-2 text-center">
<div className="text-lg font-semibold text-green-600 flex items-center justify-center gap-0.5">
{detail.satisfaction > 0 ? detail.satisfaction : '-'} {detail.satisfaction > 0 && <StarFilled className="text-xs" />}
</div>
<div className="text-xs text-neutral-400"></div>
</div>
<div className="text-xs text-neutral-400"></div>
</div>
</div>
</div>
<div>
<div className="text-xs text-neutral-400 mb-1.5"></div>
<div className="text-sm text-neutral-500 bg-neutral-50 rounded p-2">
...
</div>
</div>
</div>
) : null}
</div>
</div>
)
}
function TagBadge({ status, priority }: { status: string; priority: string }) {
const color = status === 'ended' ? '#16a34a' : priorityColors[priority] || '#2563eb'
const text = status === 'ended' ? '已结束' : status === 'waiting' ? '等待中' : priorityLabels[priority] || status
return <span className="text-xs px-1.5 py-0.5 rounded" style={{ color, backgroundColor: `${color}15` }}>{text}</span>
}
export default Dashboard