修复会话安全与实时消息

This commit is contained in:
yml2213
2026-07-14 15:06:01 +08:00
parent 064af29d3b
commit 8cff2a5824
29 changed files with 1961 additions and 546 deletions
+52 -25
View File
@@ -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">