新增访客聊天Widget组件、Admin端布局框架及路由

This commit is contained in:
yml2213
2026-07-14 11:05:22 +08:00
parent 7f72e4b46d
commit 2492daae3b
9 changed files with 272 additions and 1 deletions
+151
View File
@@ -0,0 +1,151 @@
interface Message {
id: string
sender: 'visitor' | 'agent'
content: string
time: string
}
const initialMessages: Message[] = [
{ id: '1', sender: 'agent', content: '您好!欢迎咨询客服云,请问有什么可以帮您的?', time: '10:30' },
]
const quickQuestions = ['产品功能介绍', '价格咨询', '售后服务', '合作咨询']
import { useState } from 'react'
import { CloseOutlined, MessageOutlined, SmileOutlined, PaperClipOutlined, SendOutlined, StarFilled } from '@ant-design/icons'
const VisitorChat = () => {
const [open, setOpen] = useState(false)
const [messages, setMessages] = useState<Message[]>(initialMessages)
const [input, setInput] = useState('')
const [typing, setTyping] = useState(false)
const [showRating, setShowRating] = useState(false)
const [rated, setRated] = useState(false)
const sendMessage = (text: string) => {
if (!text.trim()) return
const msg: Message = { id: Date.now().toString(), sender: 'visitor', content: text, time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) }
setMessages(prev => [...prev, msg])
setInput('')
setTyping(true)
setTimeout(() => {
setTyping(false)
const reply: Message = { id: (Date.now() + 1).toString(), sender: 'agent', content: '感谢您的咨询,客服正在为您处理中...', time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) }
setMessages(prev => [...prev, reply])
}, 1500)
}
const handleEnd = () => {
if (!rated) {
setShowRating(true)
}
setOpen(false)
}
return (
<>
{/* 悬浮按钮 */}
{!open && (
<button
onClick={() => { setOpen(true); setShowRating(false) }}
className="fixed right-5 bottom-5 w-14 h-14 rounded-full bg-blue-500 hover:bg-blue-600 text-white shadow-lg flex items-center justify-center transition-all hover:scale-110 z-50"
>
<MessageOutlined className="text-xl" />
</button>
)}
{/* 聊天窗口 */}
{open && (
<div className="fixed right-5 bottom-5 w-[400px] h-[600px] bg-white rounded-xl shadow-2xl border border-neutral-200 flex flex-col z-50 overflow-hidden">
{/* 顶部 */}
<div className="h-14 px-4 flex items-center justify-between bg-gradient-to-r from-blue-500 to-blue-600 flex-shrink-0">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center">
<span className="text-white text-sm font-medium"></span>
</div>
<div>
<div className="text-sm font-medium text-white"></div>
<div className="text-xs text-white/70">{typing ? '正在输入...' : '在线'}</div>
</div>
</div>
<CloseOutlined className="text-white cursor-pointer hover:text-white/80" onClick={handleEnd} />
</div>
{/* 消息区域 */}
<div className="flex-1 overflow-auto p-4 space-y-3 bg-neutral-50">
{messages.map(msg => (
<div key={msg.id} className={`flex ${msg.sender === 'visitor' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[75%] rounded-lg px-3 py-2 text-sm ${msg.sender === 'visitor' ? 'bg-blue-500 text-white' : 'bg-white text-neutral-700 border border-neutral-200'}`}>
{msg.content}
<div className={`text-xs mt-1 ${msg.sender === 'visitor' ? 'text-white/60' : 'text-neutral-400'}`}>{msg.time}</div>
</div>
</div>
))}
{typing && (
<div className="flex justify-start">
<div className="bg-white rounded-lg px-3 py-2 border border-neutral-200">
<div className="flex gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-neutral-300 animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-1.5 h-1.5 rounded-full bg-neutral-300 animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-1.5 h-1.5 rounded-full bg-neutral-300 animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
)}
</div>
{/* 快捷问题 */}
{messages.length <= 1 && (
<div className="px-4 py-2 border-t border-neutral-100 flex-shrink-0">
<div className="flex flex-wrap gap-1.5">
{quickQuestions.map((q, i) => (
<button key={i} onClick={() => sendMessage(q)} className="text-xs px-2.5 py-1 rounded-full bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors border border-blue-100">
{q}
</button>
))}
</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-1.5">
<SmileOutlined className="text-neutral-300 cursor-pointer hover:text-neutral-500" />
<PaperClipOutlined className="text-neutral-300 cursor-pointer hover:text-neutral-500" />
<input
className="flex-1 bg-transparent outline-none text-sm text-neutral-700 placeholder:text-neutral-400"
placeholder="输入您的问题..."
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { sendMessage(input) } }}
/>
<SendOutlined className="text-blue-500 cursor-pointer hover:text-blue-600" onClick={() => sendMessage(input)} />
</div>
</div>
{/* 满意度弹窗 */}
{showRating && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center z-10">
<div className="bg-white rounded-xl p-6 mx-8 w-full max-w-xs text-center">
<div className="text-lg font-semibold text-neutral-800 mb-1"></div>
<div className="text-sm text-neutral-400 mb-4"></div>
<div className="flex justify-center gap-1.5 mb-4">
{[1, 2, 3, 4, 5].map(star => (
<StarFilled
key={star}
className="text-2xl cursor-pointer text-neutral-200 hover:text-yellow-400 transition-colors"
onClick={() => { setRated(true); setShowRating(false) }}
/>
))}
</div>
<button onClick={() => setShowRating(false)} className="text-sm text-neutral-400 hover:text-neutral-600"></button>
</div>
</div>
)}
</div>
)}
</>
)
}
export default VisitorChat