diff --git a/web/src/pages/WidgetPreview.tsx b/web/src/pages/WidgetPreview.tsx index dee7808..e2f24e1 100644 --- a/web/src/pages/WidgetPreview.tsx +++ b/web/src/pages/WidgetPreview.tsx @@ -1,15 +1,23 @@ +import { useState } from 'react' import VisitorChat from '@/widgets/VisitorChat' const WidgetPreview = () => { + const [showWidget, setShowWidget] = useState(true) + return (

示例网站页面

-

访客聊天 Widget 已嵌入右下角,点击气泡按钮开始对话

+

Widget 在右下角,发送消息会写入数据库

+ {!showWidget && ( + + )}
- + {showWidget && }
) } diff --git a/web/src/widgets/VisitorChat.tsx b/web/src/widgets/VisitorChat.tsx index e7596d8..f137a71 100644 --- a/web/src/widgets/VisitorChat.tsx +++ b/web/src/widgets/VisitorChat.tsx @@ -9,70 +9,73 @@ interface Message { } const quickQuestions = ['产品功能介绍', '价格咨询', '售后服务', '合作咨询'] +const STORAGE_KEY = 'kefu_widget_session' -const VisitorChat = () => { - const [open, setOpen] = useState(false) - const [sessionId, setSessionId] = useState(null) - const [messages, setMessages] = useState([ - { id: 0, sender: 'agent', content: '您好!欢迎咨询客服云,请问有什么可以帮您的?', time: '' }, - ]) +const VisitorChat = ({ defaultOpen = false }: { defaultOpen?: boolean }) => { + const [open, setOpen] = useState(defaultOpen) + const [sessionId, setSessionId] = useState(() => { + const saved = localStorage.getItem(STORAGE_KEY) + return saved ? Number(saved) : null + }) + const [messages, setMessages] = useState(() => { + const saved = localStorage.getItem(STORAGE_KEY + '_msgs') + return saved ? JSON.parse(saved) : [] + }) const [input, setInput] = useState('') const [sending, setSending] = useState(false) const [showRating, setShowRating] = useState(false) const [rated, setRated] = useState(false) const pollRef = useRef(null) + const initRef = useRef(false) const initSession = async () => { + if (initRef.current && sessionId) return + initRef.current = true try { - const res = await fetch(`/api/widget/init?channel_key=WK_8a3f2e&visitor_name=测试访客`, { method: 'POST' }) + const res = await fetch(`/api/widget/init?channel_key=WK_8a3f2e&visitor_name=客服云访客`, { method: 'POST' }) const json = await res.json() if (json.code === 0) { - setSessionId(json.data.session_id) + const sid = json.data.session_id + setSessionId(sid) + localStorage.setItem(STORAGE_KEY, String(sid)) + await loadMessages(sid) } } catch (e) { - console.error('Init session failed:', e) + console.error('Init failed:', e) } } - useEffect(() => { - if (open && !sessionId) { - initSession() - } - if (!open) { - setSessionId(null) - if (pollRef.current) clearInterval(pollRef.current) - } - return () => { if (pollRef.current) clearInterval(pollRef.current) } - }, [open]) + const loadMessages = async (sid?: number) => { + const s = sid || sessionId + if (!s) return + try { + const res = await fetch(`/api/widget/messages?session_id=${s}`) + const json = await res.json() + if (json.code === 0 && json.data && json.data.length > 0) { + const msgs: Message[] = json.data.map((m: any) => ({ + id: m.id, + sender: m.sender_type === 'agent' ? 'agent' : 'visitor', + content: m.content, + time: new Date(m.sent_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + })) + setMessages(msgs) + localStorage.setItem(STORAGE_KEY + '_msgs', JSON.stringify(msgs)) + } + } catch { /* ignore */ } + } useEffect(() => { - if (sessionId) { - pollRef.current = window.setInterval(async () => { - try { - const res = await fetch(`/api/widget/messages?session_id=${sessionId}`) - const json = await res.json() - if (json.code === 0 && json.data) { - const serverMsgs: Message[] = json.data.map((m: any) => ({ - id: m.id, - sender: m.sender_type === 'agent' ? 'agent' : 'visitor', - content: m.content, - time: new Date(m.sent_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), - })) - setMessages(prev => { - const existing = new Set(prev.map(m => m.id)) - const welcome = prev[0] - // Filter out server messages that we already have; keep welcome - const serverOnly = serverMsgs.filter(m => !existing.has(m.id) && m.id > 0) - // Merge sorted - const merged = [welcome, ...serverOnly].sort((a, b) => a.id - b.id) - return merged.length > 1 ? merged : prev - }) - } - } catch { /* ignore */ } - }, 2000) + if (open && !initRef.current) { + initSession() + } + }, [open, sessionId]) + + useEffect(() => { + if (sessionId && open) { + pollRef.current = window.setInterval(() => loadMessages(), 3000) return () => { if (pollRef.current) clearInterval(pollRef.current) } } - }, [sessionId]) + }, [sessionId, open]) const sendMessage = async (text: string) => { if (!text.trim() || sending) return @@ -80,24 +83,21 @@ const VisitorChat = () => { setInput('') setSending(true) - // Add locally immediately - const localMsg: Message = { id: -Date.now(), sender: 'visitor', content, time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) } + const localMsg: Message = { + id: -Date.now(), sender: 'visitor', content, + time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }), + } setMessages(prev => [...prev, localMsg]) if (sessionId) { try { - const res = await fetch('/api/widget/message', { + await fetch('/api/widget/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, content, type: 'text' }), }) - const json = await res.json() - if (json.code !== 0) { - console.error('Send failed:', json.message) - } - } catch (e) { - console.error('Send error:', e) - } + loadMessages(sessionId) + } catch { /* ignore */ } } setSending(false) } @@ -108,6 +108,11 @@ const VisitorChat = () => { setRated(false) } + const handleClose = () => { + setOpen(false) + if (!rated) setShowRating(true) + } + return ( <> {!open && ( @@ -125,17 +130,23 @@ const VisitorChat = () => {
客服云
-
在线
+
在线 · 会话{sessionId || '...'}
- { setOpen(false); if (!rated) setShowRating(true) }} /> +
+ {messages.length === 0 && ( +
+ +
欢迎咨询,请描述您的问题
+
+ )} {messages.map(msg => (
- {msg.content} +
{msg.sender === 'agent' && 客服}{msg.content}
{msg.time &&
{msg.time}
}
@@ -144,6 +155,7 @@ const VisitorChat = () => { {messages.length <= 1 && (
+
快捷问题:
{quickQuestions.map((q, i) => ( @@ -157,13 +169,13 @@ const VisitorChat = () => { setInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') sendMessage(input) }} - disabled={sending} + disabled={sending || !sessionId} /> - sendMessage(input)} /> + sendMessage(input)} />