BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 1 | import React, { useState, useEffect } from 'react'; |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 2 | import { Card, List, Avatar, Input, Button, Row, Col, message, Modal, Form } from 'antd'; |
| 3 | import { UserAddOutlined, SearchOutlined } from '@ant-design/icons'; |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 4 | import { SysUserMessage, ChatContact } from './data.d'; |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 5 | import { getChatContactList, getChatHistory, sendMessage, getUserInfo, addFriend } from './service'; |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 6 | import './index.less'; |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 7 | import { useModel } from 'umi'; |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 8 | |
| 9 | const 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); |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 16 | 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(); |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 22 | |
| 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, |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 47 | currentUserId: Number(ccuserId), // 假设当前用户ID为1,实际应该从用户状态获取 |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 48 | 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 | |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 121 | // 添加好友 |
| 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 | |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 141 | // 选择聊天对象 |
| 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); |
BirdNETM | 2b78925 | 2025-06-03 18:08:04 +0800 | [diff] [blame] | 168 | return contact?.nickName || `用户${userId}`; |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 169 | }; |
| 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 |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 189 | 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 | } |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 202 | 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 |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 222 | avatar={ |
| 223 | <Avatar size="large"> |
| 224 | {contact.nickName.charAt(0).toUpperCase()} |
| 225 | </Avatar> |
| 226 | } |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 227 | title={ |
| 228 | <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> |
| 229 | <span style={{ |
| 230 | fontWeight: 'normal', |
| 231 | fontSize: '14px' |
| 232 | }}> |
BirdNETM | 2b78925 | 2025-06-03 18:08:04 +0800 | [diff] [blame] | 233 | {contact.nickName} |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 234 | </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> |
BirdNETM | 11aacb9 | 2025-06-07 23:17:03 +0800 | [diff] [blame^] | 397 | |
| 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> |
BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 450 | </div> |
| 451 | ); |
| 452 | }; |
| 453 | |
| 454 | export default MessagePage; |