|
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); |
|
|
|
|
|
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; |
|
|