blob: 04f9881238e910ad54ad1991afa8bea2ee72d8da [file] [log] [blame]
BirdNETM632c0612025-05-27 17:41:40 +08001import React, { useState, useEffect } from 'react';
2import { Card, List, Avatar, Input, Button, Row, Col, message } from 'antd';
3import { SysUserMessage, ChatContact } from './data.d';
4import { getChatContactList, getChatHistory, sendMessage, getUserInfo } from './service';
5import './index.less';
6
7const MessagePage: React.FC = () => {
8 const [chatContacts, setChatContacts] = useState<ChatContact[]>([]);
9 const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
10 const [inputMessage, setInputMessage] = useState('');
11 const [chatMessages, setChatMessages] = useState<SysUserMessage[]>([]);
12 const [loading, setLoading] = useState(false);
13 const [sending, setSending] = useState(false);
14
15 // 获取聊天对象列表
16 const fetchChatContacts = async () => {
17 try {
18 setLoading(true);
19 const response = await getChatContactList();
20
21 // 按最后消息时间排序
22 const sortedContacts = response.sort((a: ChatContact, b: ChatContact) =>
23 new Date(b.lastMessageTime).getTime() - new Date(a.lastMessageTime).getTime()
24 );
25 setChatContacts(sortedContacts);
26 } catch (error) {
27 message.error('获取聊天列表失败');
28 } finally {
29 setLoading(false);
30 }
31 };
32
33 // 获取与特定用户的聊天记录
34 const fetchChatHistory = async (userId: number) => {
35 try {
36 setLoading(true);
37 const response = await getChatHistory({
38 userId,
39 currentUserId: 1, // 假设当前用户ID为1,实际应该从用户状态获取
40 pageSize: 100 // 获取最近100条消息
41 });
42
43 // 按时间排序
44 const sortedMessages = response.sort((a: SysUserMessage, b: SysUserMessage) =>
45 new Date(a.createTime).getTime() - new Date(b.createTime).getTime()
46 );
47 setChatMessages(sortedMessages);
48 } catch (error) {
49 message.error('获取聊天记录失败');
50 } finally {
51 setLoading(false);
52 }
53 };
54
55 // 发送消息
56 const handleSendMessage = async () => {
57 if (!inputMessage.trim() || !selectedUserId || sending) {
58 return;
59 }
60
61 try {
62 setSending(true);
63
64 // 先在界面上显示消息(乐观更新)
65 const tempMessage: SysUserMessage = {
66 messageId: Date.now(), // 临时ID
67 senderId: 1, // 当前用户ID
68 receiverId: selectedUserId,
69 content: inputMessage,
70 createTime: new Date(),
71 delFlag: '0'
72 };
73 setChatMessages(prev => [...prev, tempMessage]);
74
75 // 清空输入框
76 const messageContent = inputMessage;
77 setInputMessage('');
78
79 // 调用API发送消息
80 await sendMessage({
81 receiverId: selectedUserId,
82 content: messageContent
83 });
84
85 // 更新聊天对象列表中的最后消息
86 setChatContacts(prevContacts => {
87 const updatedContacts = prevContacts.map(contact =>
88 contact.userId === selectedUserId
89 ? {
90 ...contact,
91 lastMessage: messageContent,
92 lastMessageTime: new Date()
93 }
94 : contact
95 );
96 // 重新排序,将当前聊天对象移到最前面
97 return updatedContacts.sort((a, b) =>
98 new Date(b.lastMessageTime).getTime() - new Date(a.lastMessageTime).getTime()
99 );
100 });
101
102 message.success('消息发送成功');
103
104 } catch (error) {
105 message.error('发送消息失败');
106 // 发送失败时,移除临时消息
107 setChatMessages(prev => prev.filter(msg => msg.messageId !== Date.now()));
108 } finally {
109 setSending(false);
110 }
111 };
112
113 // 选择聊天对象
114 const handleSelectUser = (userId: number) => {
115 setSelectedUserId(userId);
116 fetchChatHistory(userId);
117 };
118
119 // 格式化时间显示
120 const formatTime = (time: Date | string) => {
121 const date = new Date(time);
122 const now = new Date();
123 const diff = now.getTime() - date.getTime();
124 const minutes = Math.floor(diff / (1000 * 60));
125 const hours = Math.floor(diff / (1000 * 60 * 60));
126 const days = Math.floor(diff / (1000 * 60 * 60 * 24));
127
128 if (minutes < 60) {
129 return `${minutes}分钟前`;
130 } else if (hours < 24) {
131 return `${hours}小时前`;
132 } else {
133 return `${days}天前`;
134 }
135 };
136
137 // 获取用户名
138 const getUserName = (userId: number) => {
139 const contact = chatContacts.find(c => c.userId === userId);
BirdNETM2b789252025-06-03 18:08:04 +0800140
141 return contact?.nickName || `用户${userId}`;
BirdNETM632c0612025-05-27 17:41:40 +0800142 };
143
144 // 处理回车键发送
145 const handleKeyPress = (e: React.KeyboardEvent) => {
146 if (e.key === 'Enter' && !e.shiftKey) {
147 e.preventDefault();
148 handleSendMessage();
149 }
150 };
151
152 useEffect(() => {
153 fetchChatContacts();
154 }, []);
155
156 return (
157 <div className="message-page" style={{ height: '100vh', padding: '16px', display: 'flex', flexDirection: 'column' }}>
158 <Row gutter={16} style={{ flex: 1, overflow: 'hidden' }}>
159 {/* 左侧聊天对象列表 */}
160 <Col span={8}>
161 <Card
162 title="聊天列表"
163 bordered={false}
164 style={{ height: '100%' }}
165 loading={loading && !selectedUserId}
166 >
167 <List
168 style={{ height: 'calc(100vh - 140px)', overflowY: 'auto' }}
169 dataSource={chatContacts}
170 renderItem={(contact) => (
171 <List.Item
172 onClick={() => handleSelectUser(contact.userId)}
173 style={{
174 cursor: 'pointer',
175 backgroundColor: selectedUserId === contact.userId ? '#f0f8ff' : 'transparent',
176 padding: '12px',
177 borderRadius: '8px',
178 margin: '4px 0'
179 }}
180 className={selectedUserId === contact.userId ? 'selected' : ''}
181 >
182 <List.Item.Meta
BirdNETM2b789252025-06-03 18:08:04 +0800183 // avatar={
184 // <Avatar size="large">
185 // {contact.nickName.charAt(0)}
186 // </Avatar>
187 // }
BirdNETM632c0612025-05-27 17:41:40 +0800188 title={
189 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
190 <span style={{
191 fontWeight: 'normal',
192 fontSize: '14px'
193 }}>
BirdNETM2b789252025-06-03 18:08:04 +0800194 {contact.nickName}
BirdNETM632c0612025-05-27 17:41:40 +0800195 </span>
196 <span style={{ fontSize: '12px', color: '#999' }}>
197 {formatTime(contact.lastMessageTime)}
198 </span>
199 </div>
200 }
201 description={
202 <div style={{
203 color: '#666',
204 fontWeight: 'normal',
205 overflow: 'hidden',
206 textOverflow: 'ellipsis',
207 whiteSpace: 'nowrap',
208 fontSize: '13px'
209 }}>
210 {contact.lastMessage}
211 </div>
212 }
213 />
214 </List.Item>
215 )}
216 />
217 </Card>
218 </Col>
219
220 {/* 右侧聊天界面 */}
221 <Col span={16}>
222 <div style={{
223 height: '100%',
224 display: 'flex',
225 flexDirection: 'column',
226 position: 'relative'
227 }}>
228 {/* 聊天标题 */}
229 <Card
230 title={selectedUserId ? `与 ${getUserName(selectedUserId)} 的对话` : '选择一个联系人开始聊天'}
231 bordered={false}
232 style={{
233 marginBottom: 0,
234 borderBottom: '1px solid #f0f0f0'
235 }}
236 bodyStyle={{ padding: 0 }}
237 />
238
239 {/* 聊天消息区域 */}
240 <div style={{
241 flex: 1,
242 overflow: 'hidden',
243 display: 'flex',
244 flexDirection: 'column',
245 backgroundColor: '#fff',
246 border: '1px solid #f0f0f0',
247 borderTop: 'none'
248 }}>
249 {selectedUserId ? (
250 <>
251 <List
252 style={{
253 flex: 1,
254 overflowY: 'auto',
255 padding: '16px',
256 paddingBottom: '80px' // 为输入框预留空间
257 }}
258 dataSource={chatMessages}
259 loading={loading && selectedUserId !== null}
260 renderItem={(item) => (
261 <List.Item
262 className={`chat-message ${item.senderId === 1 ? 'sent' : 'received'}`}
263 style={{
264 border: 'none',
265 padding: '8px 0',
266 display: 'flex',
267 justifyContent: item.senderId === 1 ? 'flex-end' : 'flex-start'
268 }}
269 >
270 <div style={{
271 maxWidth: '70%',
272 display: 'flex',
273 flexDirection: item.senderId === 1 ? 'row-reverse' : 'row',
274 alignItems: 'flex-start',
275 gap: '8px'
276 }}>
277 <Avatar size="small">
278 {item.senderId === 1 ? 'Me' : getUserName(item.senderId).charAt(0)}
279 </Avatar>
280 <div>
281 <div style={{
282 fontSize: '12px',
283 color: '#999',
284 marginBottom: '4px',
285 textAlign: item.senderId === 1 ? 'right' : 'left'
286 }}>
287 {item.senderId === 1 ? '我' : getUserName(item.senderId)} · {new Date(item.createTime).toLocaleTimeString()}
288 </div>
289 <div style={{
290 backgroundColor: item.senderId === 1 ? '#1890ff' : '#f0f0f0',
291 color: item.senderId === 1 ? '#fff' : '#000',
292 padding: '8px 12px',
293 borderRadius: '12px',
294 wordBreak: 'break-word',
295 lineHeight: '1.4'
296 }}>
297 {item.content}
298 </div>
299 </div>
300 </div>
301 </List.Item>
302 )}
303 />
304
305 {/* 输入框区域 - 固定在底部 */}
306 <div style={{
307 position: 'absolute',
308 bottom: 0,
309 left: 0,
310 right: 0,
311 backgroundColor: '#fff',
312 padding: '16px',
313 borderTop: '1px solid #f0f0f0',
314 display: 'flex',
315 gap: '8px',
316 zIndex: 10,
317 boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.1)'
318 }}>
319 <Input.TextArea
320 value={inputMessage}
321 onChange={(e) => setInputMessage(e.target.value)}
322 onKeyDown={handleKeyPress}
323 placeholder="输入消息...(按 Enter 发送,Shift+Enter 换行)"
324 style={{
325 flex: 1,
326 resize: 'none',
327 minHeight: '40px',
328 maxHeight: '120px'
329 }}
330 autoSize={{ minRows: 1, maxRows: 4 }}
331 />
332 <Button
333 type="primary"
334 onClick={handleSendMessage}
335 loading={sending}
336 style={{ height: '40px' }}
337 >
338 发送
339 </Button>
340 </div>
341 </>
342 ) : (
343 <div style={{
344 flex: 1,
345 display: 'flex',
346 alignItems: 'center',
347 justifyContent: 'center',
348 color: '#999',
349 fontSize: '16px'
350 }}>
351 请从左侧选择一个联系人开始聊天
352 </div>
353 )}
354 </div>
355 </div>
356 </Col>
357 </Row>
358 </div>
359 );
360};
361
362export default MessagePage;