修复会话安全与实时消息
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Input, Spin, message as antMsg } from 'antd'
|
||||
import { SearchOutlined, StarFilled } from '@ant-design/icons'
|
||||
import { useAuth } from '@/stores/auth'
|
||||
import { getSessions, getSession, getCustomers, type Session as SessionType, type Customer } from '@/services/api'
|
||||
import { endSession, getSessions, getSession, getCustomers, type Session as SessionType, type Customer } from '@/services/api'
|
||||
|
||||
const priorityColors: Record<string, string> = { urgent: '#dc2626', normal: '#d97706', waiting: '#d97706' }
|
||||
const priorityLabels: Record<string, string> = { urgent: '紧急', waiting: '等待中', active: '进行中', normal: '进行中' }
|
||||
@@ -21,21 +21,21 @@ const Dashboard = () => {
|
||||
const initialLoad = useRef(true)
|
||||
const chatEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => { loadAll() }, [])
|
||||
|
||||
const loadAll = async () => {
|
||||
const loadAll = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [sRes, cRes] = await Promise.all([
|
||||
getSessions(),
|
||||
getCustomers({ page: 1 }),
|
||||
])
|
||||
setSessions(sRes.list)
|
||||
const sessionList = Array.isArray(sRes.list) ? sRes.list : []
|
||||
const customerList = Array.isArray(cRes.list) ? cRes.list : []
|
||||
setSessions(sessionList)
|
||||
const map: Record<number, Customer> = {}
|
||||
cRes.list.forEach(c => { map[c.id] = c })
|
||||
customerList.forEach(c => { map[c.id] = c })
|
||||
setCustomers(map)
|
||||
if (sRes.list.length > 0 && initialLoad.current) {
|
||||
setSelectedId(sRes.list[0].id)
|
||||
if (sessionList.length > 0 && initialLoad.current) {
|
||||
setSelectedId(sessionList[0].id)
|
||||
initialLoad.current = false
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -43,20 +43,16 @@ const Dashboard = () => {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedId) loadDetail(selectedId)
|
||||
}, [selectedId])
|
||||
|
||||
const loadDetail = async (id: number) => {
|
||||
const loadDetail = useCallback(async (id: number) => {
|
||||
setDetailLoading(true)
|
||||
try {
|
||||
const res = await getSession(id)
|
||||
const msgs: any = res.data as any
|
||||
setDetail({
|
||||
messages: (msgs.messages || []).map((m: any) => ({
|
||||
sender: m.sender_type === 'agent' ? '客服' : getCustomerName(id),
|
||||
sender: m.sender_type === 'agent' ? '客服' : '访客',
|
||||
content: m.content,
|
||||
time: new Date(m.sent_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
})),
|
||||
@@ -67,14 +63,33 @@ const Dashboard = () => {
|
||||
setDetailLoading(false)
|
||||
}
|
||||
setTimeout(() => chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 200)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getCustomerName = (sessionId: number): string => {
|
||||
const s = sessions.find(s => s.id === sessionId)
|
||||
if (!s) return '访客'
|
||||
const c = customers[s.customer_id]
|
||||
return c ? c.name : `客户${s.customer_id}`
|
||||
}
|
||||
useEffect(() => { loadAll() }, [loadAll])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.token) return
|
||||
const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const socket = new WebSocket(`${scheme}//${window.location.host}/api/ws`, ['kefu-v1', user.token])
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event.data)
|
||||
if (payload.type === 'message' && payload.session_id === selectedId) {
|
||||
loadDetail(payload.session_id)
|
||||
}
|
||||
if (payload.type === 'session_created' || payload.type === 'session_updated') {
|
||||
loadAll()
|
||||
}
|
||||
} catch {
|
||||
// 忽略格式错误的实时消息
|
||||
}
|
||||
}
|
||||
return () => socket.close()
|
||||
}, [user?.token, selectedId, loadAll, loadDetail])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedId) loadDetail(selectedId)
|
||||
}, [selectedId, loadDetail])
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!messageInput.trim() || !selectedId || sending) return
|
||||
@@ -110,6 +125,18 @@ const Dashboard = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnd = async () => {
|
||||
if (!selectedId || sending) return
|
||||
try {
|
||||
await endSession(selectedId, 'resolved')
|
||||
antMsg.success('会话已结束')
|
||||
await loadAll()
|
||||
await loadDetail(selectedId)
|
||||
} catch {
|
||||
antMsg.error('结束会话失败')
|
||||
}
|
||||
}
|
||||
|
||||
const selected = sessions.find(s => s.id === selectedId)
|
||||
const selectedCustomer = selected ? customers[selected.customer_id] : null
|
||||
|
||||
@@ -170,8 +197,8 @@ const Dashboard = () => {
|
||||
<span className="text-xs text-neutral-400">{statusLabels[selected.status]}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<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>
|
||||
<span className="text-xs text-neutral-400 cursor-not-allowed">转接</span>
|
||||
<span className="text-xs text-neutral-400 cursor-pointer hover:text-red-500" onClick={handleEnd}>结束</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4 flex flex-col gap-3">
|
||||
|
||||
Reference in New Issue
Block a user