BirdNETM | 632c061 | 2025-05-27 17:41:40 +0800 | [diff] [blame] | 1 | import React, { useState, useEffect } from 'react'; |
| 2 | import { Card, List, Avatar, Input, Button, Row, Col, message } from 'antd'; |
| 3 | import { SysUserMessage, ChatContact } from './data.d'; |
| 4 | import { getChatContactList, getChatHistory, sendMessage, getUserInfo } from './service'; |
| 5 | import './index.less'; |
| 6 | |
| 7 | const 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); |
| 140 | return contact?.userName || `用户${userId}`; |
| 141 | }; |
| 142 | |
| 143 | // 处理回车键发送 |
| 144 | const handleKeyPress = (e: React.KeyboardEvent) => { |
| 145 | if (e.key === 'Enter' && !e.shiftKey) { |
| 146 | e.preventDefault(); |
| 147 | handleSendMessage(); |
| 148 | } |
| 149 | }; |
| 150 | |
| 151 | useEffect(() => { |
| 152 | fetchChatContacts(); |
| 153 | }, []); |
| 154 | |
| 155 | return ( |
| 156 | <div className="message-page" style={{ height: '100vh', padding: '16px', display: 'flex', flexDirection: 'column' }}> |
| 157 | <Row gutter={16} style={{ flex: 1, overflow: 'hidden' }}> |
| 158 | {/* 左侧聊天对象列表 */} |
| 159 | <Col span={8}> |
| 160 | <Card |
| 161 | title="聊天列表" |
| 162 | bordered={false} |
| 163 | style={{ height: '100%' }} |
| 164 | loading={loading && !selectedUserId} |
| 165 | > |
| 166 | <List |
| 167 | style={{ height: 'calc(100vh - 140px)', overflowY: 'auto' }} |
| 168 | dataSource={chatContacts} |
| 169 | renderItem={(contact) => ( |
| 170 | <List.Item |
| 171 | onClick={() => handleSelectUser(contact.userId)} |
| 172 | style={{ |
| 173 | cursor: 'pointer', |
| 174 | backgroundColor: selectedUserId === contact.userId ? '#f0f8ff' : 'transparent', |
| 175 | padding: '12px', |
| 176 | borderRadius: '8px', |
| 177 | margin: '4px 0' |
| 178 | }} |
| 179 | className={selectedUserId === contact.userId ? 'selected' : ''} |
| 180 | > |
| 181 | <List.Item.Meta |
| 182 | avatar={ |
| 183 | <Avatar size="large"> |
| 184 | {contact.userName.charAt(0)} |
| 185 | </Avatar> |
| 186 | } |
| 187 | title={ |
| 188 | <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> |
| 189 | <span style={{ |
| 190 | fontWeight: 'normal', |
| 191 | fontSize: '14px' |
| 192 | }}> |
| 193 | {contact.userName} |
| 194 | </span> |
| 195 | <span style={{ fontSize: '12px', color: '#999' }}> |
| 196 | {formatTime(contact.lastMessageTime)} |
| 197 | </span> |
| 198 | </div> |
| 199 | } |
| 200 | description={ |
| 201 | <div style={{ |
| 202 | color: '#666', |
| 203 | fontWeight: 'normal', |
| 204 | overflow: 'hidden', |
| 205 | textOverflow: 'ellipsis', |
| 206 | whiteSpace: 'nowrap', |
| 207 | fontSize: '13px' |
| 208 | }}> |
| 209 | {contact.lastMessage} |
| 210 | </div> |
| 211 | } |
| 212 | /> |
| 213 | </List.Item> |
| 214 | )} |
| 215 | /> |
| 216 | </Card> |
| 217 | </Col> |
| 218 | |
| 219 | {/* 右侧聊天界面 */} |
| 220 | <Col span={16}> |
| 221 | <div style={{ |
| 222 | height: '100%', |
| 223 | display: 'flex', |
| 224 | flexDirection: 'column', |
| 225 | position: 'relative' |
| 226 | }}> |
| 227 | {/* 聊天标题 */} |
| 228 | <Card |
| 229 | title={selectedUserId ? `与 ${getUserName(selectedUserId)} 的对话` : '选择一个联系人开始聊天'} |
| 230 | bordered={false} |
| 231 | style={{ |
| 232 | marginBottom: 0, |
| 233 | borderBottom: '1px solid #f0f0f0' |
| 234 | }} |
| 235 | bodyStyle={{ padding: 0 }} |
| 236 | /> |
| 237 | |
| 238 | {/* 聊天消息区域 */} |
| 239 | <div style={{ |
| 240 | flex: 1, |
| 241 | overflow: 'hidden', |
| 242 | display: 'flex', |
| 243 | flexDirection: 'column', |
| 244 | backgroundColor: '#fff', |
| 245 | border: '1px solid #f0f0f0', |
| 246 | borderTop: 'none' |
| 247 | }}> |
| 248 | {selectedUserId ? ( |
| 249 | <> |
| 250 | <List |
| 251 | style={{ |
| 252 | flex: 1, |
| 253 | overflowY: 'auto', |
| 254 | padding: '16px', |
| 255 | paddingBottom: '80px' // 为输入框预留空间 |
| 256 | }} |
| 257 | dataSource={chatMessages} |
| 258 | loading={loading && selectedUserId !== null} |
| 259 | renderItem={(item) => ( |
| 260 | <List.Item |
| 261 | className={`chat-message ${item.senderId === 1 ? 'sent' : 'received'}`} |
| 262 | style={{ |
| 263 | border: 'none', |
| 264 | padding: '8px 0', |
| 265 | display: 'flex', |
| 266 | justifyContent: item.senderId === 1 ? 'flex-end' : 'flex-start' |
| 267 | }} |
| 268 | > |
| 269 | <div style={{ |
| 270 | maxWidth: '70%', |
| 271 | display: 'flex', |
| 272 | flexDirection: item.senderId === 1 ? 'row-reverse' : 'row', |
| 273 | alignItems: 'flex-start', |
| 274 | gap: '8px' |
| 275 | }}> |
| 276 | <Avatar size="small"> |
| 277 | {item.senderId === 1 ? 'Me' : getUserName(item.senderId).charAt(0)} |
| 278 | </Avatar> |
| 279 | <div> |
| 280 | <div style={{ |
| 281 | fontSize: '12px', |
| 282 | color: '#999', |
| 283 | marginBottom: '4px', |
| 284 | textAlign: item.senderId === 1 ? 'right' : 'left' |
| 285 | }}> |
| 286 | {item.senderId === 1 ? '我' : getUserName(item.senderId)} · {new Date(item.createTime).toLocaleTimeString()} |
| 287 | </div> |
| 288 | <div style={{ |
| 289 | backgroundColor: item.senderId === 1 ? '#1890ff' : '#f0f0f0', |
| 290 | color: item.senderId === 1 ? '#fff' : '#000', |
| 291 | padding: '8px 12px', |
| 292 | borderRadius: '12px', |
| 293 | wordBreak: 'break-word', |
| 294 | lineHeight: '1.4' |
| 295 | }}> |
| 296 | {item.content} |
| 297 | </div> |
| 298 | </div> |
| 299 | </div> |
| 300 | </List.Item> |
| 301 | )} |
| 302 | /> |
| 303 | |
| 304 | {/* 输入框区域 - 固定在底部 */} |
| 305 | <div style={{ |
| 306 | position: 'absolute', |
| 307 | bottom: 0, |
| 308 | left: 0, |
| 309 | right: 0, |
| 310 | backgroundColor: '#fff', |
| 311 | padding: '16px', |
| 312 | borderTop: '1px solid #f0f0f0', |
| 313 | display: 'flex', |
| 314 | gap: '8px', |
| 315 | zIndex: 10, |
| 316 | boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.1)' |
| 317 | }}> |
| 318 | <Input.TextArea |
| 319 | value={inputMessage} |
| 320 | onChange={(e) => setInputMessage(e.target.value)} |
| 321 | onKeyDown={handleKeyPress} |
| 322 | placeholder="输入消息...(按 Enter 发送,Shift+Enter 换行)" |
| 323 | style={{ |
| 324 | flex: 1, |
| 325 | resize: 'none', |
| 326 | minHeight: '40px', |
| 327 | maxHeight: '120px' |
| 328 | }} |
| 329 | autoSize={{ minRows: 1, maxRows: 4 }} |
| 330 | /> |
| 331 | <Button |
| 332 | type="primary" |
| 333 | onClick={handleSendMessage} |
| 334 | loading={sending} |
| 335 | style={{ height: '40px' }} |
| 336 | > |
| 337 | 发送 |
| 338 | </Button> |
| 339 | </div> |
| 340 | </> |
| 341 | ) : ( |
| 342 | <div style={{ |
| 343 | flex: 1, |
| 344 | display: 'flex', |
| 345 | alignItems: 'center', |
| 346 | justifyContent: 'center', |
| 347 | color: '#999', |
| 348 | fontSize: '16px' |
| 349 | }}> |
| 350 | 请从左侧选择一个联系人开始聊天 |
| 351 | </div> |
| 352 | )} |
| 353 | </div> |
| 354 | </div> |
| 355 | </Col> |
| 356 | </Row> |
| 357 | </div> |
| 358 | ); |
| 359 | }; |
| 360 | |
| 361 | export default MessagePage; |