cnmksjs's picture
Upload 49 files
40e991e verified
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;