Spaces:
Configuration error
Configuration error
import React, { useState, useEffect, useRef } from 'react'; | |
import { Send, LogOut, Users, MessageCircle } from 'lucide-react'; | |
import { User, Message, OnlineUser } from '../types'; | |
import { messageAPI } from '../utils/api'; | |
import { socketService } from '../utils/socket'; | |
interface ChatProps { | |
user: User; | |
onLogout: () => void; | |
} | |
const Chat: React.FC<ChatProps> = ({ user, onLogout }) => { | |
const [messages, setMessages] = useState<Message[]>([]); | |
const [newMessage, setNewMessage] = useState(''); | |
const [onlineUsers, setOnlineUsers] = useState<OnlineUser[]>([]); | |
const [loading, setLoading] = useState(true); | |
const messagesEndRef = useRef<HTMLDivElement>(null); | |
const scrollToBottom = () => { | |
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
}; | |
useEffect(() => { | |
scrollToBottom(); | |
}, [messages]); | |
useEffect(() => { | |
const initializeChat = async () => { | |
try { | |
// 获取历史消息 | |
const historyMessages = await messageAPI.getMessages(); | |
setMessages(historyMessages); | |
// 连接Socket | |
const token = localStorage.getItem('token'); | |
if (token) { | |
const socket = socketService.connect(token); | |
// 监听新消息 | |
socketService.onNewMessage((message: Message) => { | |
setMessages(prev => [...prev, message]); | |
}); | |
// 监听用户上线 | |
socketService.onUserJoined((userData) => { | |
console.log(`${userData.username} 加入了聊天室`); | |
}); | |
// 监听用户下线 | |
socketService.onUserLeft((userData) => { | |
console.log(`${userData.username} 离开了聊天室`); | |
}); | |
// 监听在线用户列表 | |
socketService.onOnlineUsers((users: OnlineUser[]) => { | |
setOnlineUsers(users); | |
}); | |
} | |
} catch (error) { | |
console.error('初始化聊天失败:', error); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
initializeChat(); | |
// 清理函数 | |
return () => { | |
socketService.offAllListeners(); | |
socketService.disconnect(); | |
}; | |
}, []); | |
const handleSendMessage = (e: React.FormEvent) => { | |
e.preventDefault(); | |
if (newMessage.trim()) { | |
socketService.sendMessage(newMessage.trim()); | |
setNewMessage(''); | |
} | |
}; | |
const handleLogout = () => { | |
socketService.disconnect(); | |
onLogout(); | |
}; | |
const formatTime = (timestamp: Date) => { | |
return new Date(timestamp).toLocaleTimeString('zh-CN', { | |
hour: '2-digit', | |
minute: '2-digit', | |
}); | |
}; | |
if (loading) { | |
return ( | |
<div className="min-h-screen flex items-center justify-center bg-gray-50"> | |
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div> | |
</div> | |
); | |
} | |
return ( | |
<div className="flex h-screen bg-gray-50"> | |
{/* 侧边栏 */} | |
<div className="w-64 bg-white border-r border-gray-200 flex flex-col"> | |
{/* 用户信息 */} | |
<div className="p-4 border-b border-gray-200"> | |
<div className="flex items-center space-x-3"> | |
<div className="w-10 h-10 bg-primary-600 rounded-full flex items-center justify-center"> | |
<span className="text-white font-medium"> | |
{user.username.charAt(0).toUpperCase()} | |
</span> | |
</div> | |
<div className="flex-1"> | |
<h3 className="font-medium text-gray-900">{user.username}</h3> | |
<p className="text-sm text-gray-500">{user.email}</p> | |
</div> | |
<button | |
onClick={handleLogout} | |
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100" | |
title="退出登录" | |
> | |
<LogOut className="h-5 w-5" /> | |
</button> | |
</div> | |
</div> | |
{/* 在线用户 */} | |
<div className="flex-1 p-4"> | |
<div className="flex items-center space-x-2 mb-4"> | |
<Users className="h-5 w-5 text-gray-500" /> | |
<h4 className="font-medium text-gray-900"> | |
在线用户 ({onlineUsers.length}) | |
</h4> | |
</div> | |
<div className="space-y-2"> | |
{onlineUsers.map((onlineUser) => ( | |
<div key={onlineUser.userId} className="flex items-center space-x-3"> | |
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center"> | |
<span className="text-white text-sm font-medium"> | |
{onlineUser.username.charAt(0).toUpperCase()} | |
</span> | |
</div> | |
<span className="text-sm text-gray-700">{onlineUser.username}</span> | |
{onlineUser.userId === user.id && ( | |
<span className="text-xs text-gray-500">(你)</span> | |
)} | |
</div> | |
))} | |
</div> | |
</div> | |
</div> | |
{/* 主聊天区域 */} | |
<div className="flex-1 flex flex-col"> | |
{/* 聊天头部 */} | |
<div className="bg-white border-b border-gray-200 p-4"> | |
<div className="flex items-center space-x-2"> | |
<MessageCircle className="h-6 w-6 text-primary-600" /> | |
<h2 className="text-lg font-semibold text-gray-900">聊天室</h2> | |
</div> | |
</div> | |
{/* 消息列表 */} | |
<div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar"> | |
{messages.map((message) => ( | |
<div | |
key={message.id} | |
className={`flex ${ | |
message.sender.id === user.id ? 'justify-end' : 'justify-start' | |
}`} | |
> | |
<div | |
className={`message-bubble ${ | |
message.sender.id === user.id ? 'message-own' : 'message-other' | |
}`} | |
> | |
{message.sender.id !== user.id && ( | |
<div className="text-xs font-medium mb-1 text-gray-600"> | |
{message.sender.username} | |
</div> | |
)} | |
<div className="text-sm">{message.content}</div> | |
<div | |
className={`text-xs mt-1 ${ | |
message.sender.id === user.id | |
? 'text-primary-200' | |
: 'text-gray-500' | |
}`} | |
> | |
{formatTime(message.timestamp)} | |
</div> | |
</div> | |
</div> | |
))} | |
<div ref={messagesEndRef} /> | |
</div> | |
{/* 消息输入 */} | |
<div className="bg-white border-t border-gray-200 p-4"> | |
<form onSubmit={handleSendMessage} className="flex space-x-4"> | |
<input | |
type="text" | |
value={newMessage} | |
onChange={(e) => setNewMessage(e.target.value)} | |
placeholder="输入消息..." | |
className="flex-1 input-field" | |
/> | |
<button | |
type="submit" | |
disabled={!newMessage.trim()} | |
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2" | |
> | |
<Send className="h-4 w-4" /> | |
<span>发送</span> | |
</button> | |
</form> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default Chat; | |