blob: 9ae94671f77f141702f244b25e07354f29492d7a [file] [log] [blame]
BirdNETM632c0612025-05-27 17:41:40 +08001import React, { useState, useEffect } from 'react';
BirdNETM11aacb92025-06-07 23:17:03 +08002import { Card, List, Avatar, Input, Button, Row, Col, message, Modal, Form } from 'antd';
3import { UserAddOutlined, SearchOutlined } from '@ant-design/icons';
BirdNETM632c0612025-05-27 17:41:40 +08004import { SysUserMessage, ChatContact } from './data.d';
BirdNETM11aacb92025-06-07 23:17:03 +08005import { getChatContactList, getChatHistory, sendMessage, getUserInfo, addFriend } from './service';
BirdNETM632c0612025-05-27 17:41:40 +08006import './index.less';
BirdNETM11aacb92025-06-07 23:17:03 +08007import { useModel } from 'umi';
BirdNETM632c0612025-05-27 17:41:40 +08008
9const MessagePage: React.FC = () => {
10 const [chatContacts, setChatContacts] = useState<ChatContact[]>([]);
11 const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
12 const [inputMessage, setInputMessage] = useState('');
13 const [chatMessages, setChatMessages] = useState<SysUserMessage[]>([]);
14 const [loading, setLoading] = useState(false);
15 const [sending, setSending] = useState(false);
BirdNETM11aacb92025-06-07 23:17:03 +080016 const { initialState } = useModel('@@initialState');
17 const ccuserId = initialState?.currentUser?.userId || '';
18 // 添加好友相关状态
19 const [addFriendVisible, setAddFriendVisible] = useState(false);
20 const [addFriendLoading, setAddFriendLoading] = useState(false);
21 const [form] = Form.useForm();
BirdNETM632c0612025-05-27 17:41:40 +080022
23 // 获取聊天对象列表
24 const fetchChatContacts = async () => {
25 try {
26 setLoading(true);
27 const response = await getChatContactList();
28
29 // 按最后消息时间排序
30 const sortedContacts = response.sort((a: ChatContact, b: ChatContact) =>
31 new Date(b.lastMessageTime).getTime() - new Date(a.lastMessageTime).getTime()
32 );
33 setChatContacts(sortedContacts);
34 } catch (error) {
35 message.error('获取聊天列表失败');
36 } finally {
37 setLoading(false);
38 }
39 };
40
41 // 获取与特定用户的聊天记录
42 const fetchChatHistory = async (userId: number) => {
43 try {
44 setLoading(true);
45 const response = await getChatHistory({
46 userId,
BirdNETM11aacb92025-06-07 23:17:03 +080047 currentUserId: Number(ccuserId), // 假设当前用户ID为1,实际应该从用户状态获取
BirdNETM632c0612025-05-27 17:41:40 +080048 pageSize: 100 // 获取最近100条消息
49 });
50
51 // 按时间排序
52 const sortedMessages = response.sort((a: SysUserMessage, b: SysUserMessage) =>
53 new Date(a.createTime).getTime() - new Date(b.createTime).getTime()
54 );
55 setChatMessages(sortedMessages);
56 } catch (error) {
57 message.error('获取聊天记录失败');
58 } finally {
59 setLoading(false);
60 }
61 };
62
63 // 发送消息
64 const handleSendMessage = async () => {
65 if (!inputMessage.trim() || !selectedUserId || sending) {
66 return;
67 }
68
69 try {
70 setSending(true);
71
72 // 先在界面上显示消息(乐观更新)
73 const tempMessage: SysUserMessage = {
74 messageId: Date.now(), // 临时ID
75 senderId: 1, // 当前用户ID
76 receiverId: selectedUserId,
77 content: inputMessage,
78 createTime: new Date(),
79 delFlag: '0'
80 };
81 setChatMessages(prev => [...prev, tempMessage]);
82
83 // 清空输入框
84 const messageContent = inputMessage;
85 setInputMessage('');
86
87 // 调用API发送消息
88 await sendMessage({
89 receiverId: selectedUserId,
90 content: messageContent
91 });
92
93 // 更新聊天对象列表中的最后消息
94 setChatContacts(prevContacts => {
95 const updatedContacts = prevContacts.map(contact =>
96 contact.userId === selectedUserId
97 ? {
98 ...contact,
99 lastMessage: messageContent,
100 lastMessageTime: new Date()
101 }
102 : contact
103 );
104 // 重新排序,将当前聊天对象移到最前面
105 return updatedContacts.sort((a, b) =>
106 new Date(b.lastMessageTime).getTime() - new Date(a.lastMessageTime).getTime()
107 );
108 });
109
110 message.success('消息发送成功');
111
112 } catch (error) {
113 message.error('发送消息失败');
114 // 发送失败时,移除临时消息
115 setChatMessages(prev => prev.filter(msg => msg.messageId !== Date.now()));
116 } finally {
117 setSending(false);
118 }
119 };
120
BirdNETM11aacb92025-06-07 23:17:03 +0800121 // 添加好友
122 const handleAddFriend = async (values: { username: string }) => {
123 try {
124 setAddFriendLoading(true);
125 await addFriend({ userId: Number(ccuserId), authorUsername: values.username });
126 message.success('好友添加成功');
127
128 // 重新获取聊天对象列表
129 await fetchChatContacts();
130
131 // 关闭弹窗并重置表单
132 setAddFriendVisible(false);
133 form.resetFields();
134 } catch (error) {
135 message.error('添加好友失败,请检查用户名是否正确');
136 } finally {
137 setAddFriendLoading(false);
138 }
139 };
140
BirdNETM632c0612025-05-27 17:41:40 +0800141 // 选择聊天对象
142 const handleSelectUser = (userId: number) => {
143 setSelectedUserId(userId);
144 fetchChatHistory(userId);
145 };
146
147 // 格式化时间显示
148 const formatTime = (time: Date | string) => {
149 const date = new Date(time);
150 const now = new Date();
151 const diff = now.getTime() - date.getTime();
152 const minutes = Math.floor(diff / (1000 * 60));
153 const hours = Math.floor(diff / (1000 * 60 * 60));
154 const days = Math.floor(diff / (1000 * 60 * 60 * 24));
155
156 if (minutes < 60) {
157 return `${minutes}分钟前`;
158 } else if (hours < 24) {
159 return `${hours}小时前`;
160 } else {
161 return `${days}天前`;
162 }
163 };
164
165 // 获取用户名
166 const getUserName = (userId: number) => {
167 const contact = chatContacts.find(c => c.userId === userId);
BirdNETM2b789252025-06-03 18:08:04 +0800168 return contact?.nickName || `用户${userId}`;
BirdNETM632c0612025-05-27 17:41:40 +0800169 };
170
171 // 处理回车键发送
172 const handleKeyPress = (e: React.KeyboardEvent) => {
173 if (e.key === 'Enter' && !e.shiftKey) {
174 e.preventDefault();
175 handleSendMessage();
176 }
177 };
178
179 useEffect(() => {
180 fetchChatContacts();
181 }, []);
182
183 return (
184 <div className="message-page" style={{ height: '100vh', padding: '16px', display: 'flex', flexDirection: 'column' }}>
185 <Row gutter={16} style={{ flex: 1, overflow: 'hidden' }}>
186 {/* 左侧聊天对象列表 */}
187 <Col span={8}>
188 <Card
BirdNETM11aacb92025-06-07 23:17:03 +0800189 title={
190 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
191 <span>聊天列表</span>
192 <Button
193 type="primary"
194 icon={<UserAddOutlined />}
195 size="small"
196 onClick={() => setAddFriendVisible(true)}
197 >
198 添加好友
199 </Button>
200 </div>
201 }
BirdNETM632c0612025-05-27 17:41:40 +0800202 bordered={false}
203 style={{ height: '100%' }}
204 loading={loading && !selectedUserId}
205 >
206 <List
207 style={{ height: 'calc(100vh - 140px)', overflowY: 'auto' }}
208 dataSource={chatContacts}
209 renderItem={(contact) => (
210 <List.Item
211 onClick={() => handleSelectUser(contact.userId)}
212 style={{
213 cursor: 'pointer',
214 backgroundColor: selectedUserId === contact.userId ? '#f0f8ff' : 'transparent',
215 padding: '12px',
216 borderRadius: '8px',
217 margin: '4px 0'
218 }}
219 className={selectedUserId === contact.userId ? 'selected' : ''}
220 >
221 <List.Item.Meta
BirdNETM11aacb92025-06-07 23:17:03 +0800222 avatar={
223 <Avatar size="large">
224 {contact.nickName.charAt(0).toUpperCase()}
225 </Avatar>
226 }
BirdNETM632c0612025-05-27 17:41:40 +0800227 title={
228 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
229 <span style={{
230 fontWeight: 'normal',
231 fontSize: '14px'
232 }}>
BirdNETM2b789252025-06-03 18:08:04 +0800233 {contact.nickName}
BirdNETM632c0612025-05-27 17:41:40 +0800234 </span>
235 <span style={{ fontSize: '12px', color: '#999' }}>
236 {formatTime(contact.lastMessageTime)}
237 </span>
238 </div>
239 }
240 description={
241 <div style={{
242 color: '#666',
243 fontWeight: 'normal',
244 overflow: 'hidden',
245 textOverflow: 'ellipsis',
246 whiteSpace: 'nowrap',
247 fontSize: '13px'
248 }}>
249 {contact.lastMessage}
250 </div>
251 }
252 />
253 </List.Item>
254 )}
255 />
256 </Card>
257 </Col>
258
259 {/* 右侧聊天界面 */}
260 <Col span={16}>
261 <div style={{
262 height: '100%',
263 display: 'flex',
264 flexDirection: 'column',
265 position: 'relative'
266 }}>
267 {/* 聊天标题 */}
268 <Card
269 title={selectedUserId ? `与 ${getUserName(selectedUserId)} 的对话` : '选择一个联系人开始聊天'}
270 bordered={false}
271 style={{
272 marginBottom: 0,
273 borderBottom: '1px solid #f0f0f0'
274 }}
275 bodyStyle={{ padding: 0 }}
276 />
277
278 {/* 聊天消息区域 */}
279 <div style={{
280 flex: 1,
281 overflow: 'hidden',
282 display: 'flex',
283 flexDirection: 'column',
284 backgroundColor: '#fff',
285 border: '1px solid #f0f0f0',
286 borderTop: 'none'
287 }}>
288 {selectedUserId ? (
289 <>
290 <List
291 style={{
292 flex: 1,
293 overflowY: 'auto',
294 padding: '16px',
295 paddingBottom: '80px' // 为输入框预留空间
296 }}
297 dataSource={chatMessages}
298 loading={loading && selectedUserId !== null}
299 renderItem={(item) => (
300 <List.Item
301 className={`chat-message ${item.senderId === 1 ? 'sent' : 'received'}`}
302 style={{
303 border: 'none',
304 padding: '8px 0',
305 display: 'flex',
306 justifyContent: item.senderId === 1 ? 'flex-end' : 'flex-start'
307 }}
308 >
309 <div style={{
310 maxWidth: '70%',
311 display: 'flex',
312 flexDirection: item.senderId === 1 ? 'row-reverse' : 'row',
313 alignItems: 'flex-start',
314 gap: '8px'
315 }}>
316 <Avatar size="small">
317 {item.senderId === 1 ? 'Me' : getUserName(item.senderId).charAt(0)}
318 </Avatar>
319 <div>
320 <div style={{
321 fontSize: '12px',
322 color: '#999',
323 marginBottom: '4px',
324 textAlign: item.senderId === 1 ? 'right' : 'left'
325 }}>
326 {item.senderId === 1 ? '我' : getUserName(item.senderId)} · {new Date(item.createTime).toLocaleTimeString()}
327 </div>
328 <div style={{
329 backgroundColor: item.senderId === 1 ? '#1890ff' : '#f0f0f0',
330 color: item.senderId === 1 ? '#fff' : '#000',
331 padding: '8px 12px',
332 borderRadius: '12px',
333 wordBreak: 'break-word',
334 lineHeight: '1.4'
335 }}>
336 {item.content}
337 </div>
338 </div>
339 </div>
340 </List.Item>
341 )}
342 />
343
344 {/* 输入框区域 - 固定在底部 */}
345 <div style={{
346 position: 'absolute',
347 bottom: 0,
348 left: 0,
349 right: 0,
350 backgroundColor: '#fff',
351 padding: '16px',
352 borderTop: '1px solid #f0f0f0',
353 display: 'flex',
354 gap: '8px',
355 zIndex: 10,
356 boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.1)'
357 }}>
358 <Input.TextArea
359 value={inputMessage}
360 onChange={(e) => setInputMessage(e.target.value)}
361 onKeyDown={handleKeyPress}
362 placeholder="输入消息...(按 Enter 发送,Shift+Enter 换行)"
363 style={{
364 flex: 1,
365 resize: 'none',
366 minHeight: '40px',
367 maxHeight: '120px'
368 }}
369 autoSize={{ minRows: 1, maxRows: 4 }}
370 />
371 <Button
372 type="primary"
373 onClick={handleSendMessage}
374 loading={sending}
375 style={{ height: '40px' }}
376 >
377 发送
378 </Button>
379 </div>
380 </>
381 ) : (
382 <div style={{
383 flex: 1,
384 display: 'flex',
385 alignItems: 'center',
386 justifyContent: 'center',
387 color: '#999',
388 fontSize: '16px'
389 }}>
390 请从左侧选择一个联系人开始聊天
391 </div>
392 )}
393 </div>
394 </div>
395 </Col>
396 </Row>
BirdNETM11aacb92025-06-07 23:17:03 +0800397
398 {/* 添加好友弹窗 */}
399 <Modal
400 title="添加好友"
401 open={addFriendVisible}
402 onCancel={() => {
403 setAddFriendVisible(false);
404 form.resetFields();
405 }}
406 footer={null}
407 width={400}
408 >
409 <Form
410 form={form}
411 layout="vertical"
412 onFinish={handleAddFriend}
413 style={{ marginTop: '20px' }}
414 >
415 <Form.Item
416 label="用户名"
417 name="username"
418 rules={[
419 { required: true, message: '请输入用户名' },
420 { min: 2, message: '用户名至少2个字符' },
421 { max: 20, message: '用户名最多20个字符' }
422 ]}
423 >
424 <Input
425 placeholder="请输入要添加的用户名"
426 prefix={<SearchOutlined />}
427 size="large"
428 />
429 </Form.Item>
430 <Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
431 <Button
432 onClick={() => {
433 setAddFriendVisible(false);
434 form.resetFields();
435 }}
436 style={{ marginRight: '8px' }}
437 >
438 取消
439 </Button>
440 <Button
441 type="primary"
442 htmlType="submit"
443 loading={addFriendLoading}
444 >
445 添加
446 </Button>
447 </Form.Item>
448 </Form>
449 </Modal>
BirdNETM632c0612025-05-27 17:41:40 +0800450 </div>
451 );
452};
453
454export default MessagePage;