评论连接优化,新增私信页面,等私信后端写完

Change-Id: I63c05945c47be9bcba6113ddd299058f302cb927
diff --git a/react-ui/src/pages/Message/index.tsx b/react-ui/src/pages/Message/index.tsx
new file mode 100644
index 0000000..b07630a
--- /dev/null
+++ b/react-ui/src/pages/Message/index.tsx
@@ -0,0 +1,361 @@
+import React, { useState, useEffect } from 'react';
+import { Card, List, Avatar, Input, Button, Row, Col, message } from 'antd';
+import { SysUserMessage, ChatContact } from './data.d';
+import { getChatContactList, getChatHistory, sendMessage, getUserInfo } from './service';
+import './index.less';
+
+const MessagePage: React.FC = () => {
+    const [chatContacts, setChatContacts] = useState<ChatContact[]>([]);
+    const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
+    const [inputMessage, setInputMessage] = useState('');
+    const [chatMessages, setChatMessages] = useState<SysUserMessage[]>([]);
+    const [loading, setLoading] = useState(false);
+    const [sending, setSending] = useState(false);
+
+    // 获取聊天对象列表
+    const fetchChatContacts = async () => {
+        try {
+            setLoading(true);
+            const response = await getChatContactList();
+
+            // 按最后消息时间排序
+            const sortedContacts = response.sort((a: ChatContact, b: ChatContact) =>
+                new Date(b.lastMessageTime).getTime() - new Date(a.lastMessageTime).getTime()
+            );
+            setChatContacts(sortedContacts);
+        } catch (error) {
+            message.error('获取聊天列表失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    // 获取与特定用户的聊天记录
+    const fetchChatHistory = async (userId: number) => {
+        try {
+            setLoading(true);
+            const response = await getChatHistory({
+                userId,
+                currentUserId: 1, // 假设当前用户ID为1,实际应该从用户状态获取
+                pageSize: 100 // 获取最近100条消息
+            });
+
+            // 按时间排序
+            const sortedMessages = response.sort((a: SysUserMessage, b: SysUserMessage) =>
+                new Date(a.createTime).getTime() - new Date(b.createTime).getTime()
+            );
+            setChatMessages(sortedMessages);
+        } catch (error) {
+            message.error('获取聊天记录失败');
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    // 发送消息
+    const handleSendMessage = async () => {
+        if (!inputMessage.trim() || !selectedUserId || sending) {
+            return;
+        }
+
+        try {
+            setSending(true);
+
+            // 先在界面上显示消息(乐观更新)
+            const tempMessage: SysUserMessage = {
+                messageId: Date.now(), // 临时ID
+                senderId: 1, // 当前用户ID
+                receiverId: selectedUserId,
+                content: inputMessage,
+                createTime: new Date(),
+                delFlag: '0'
+            };
+            setChatMessages(prev => [...prev, tempMessage]);
+
+            // 清空输入框
+            const messageContent = inputMessage;
+            setInputMessage('');
+
+            // 调用API发送消息
+            await sendMessage({
+                receiverId: selectedUserId,
+                content: messageContent
+            });
+
+            // 更新聊天对象列表中的最后消息
+            setChatContacts(prevContacts => {
+                const updatedContacts = prevContacts.map(contact =>
+                    contact.userId === selectedUserId
+                        ? {
+                            ...contact,
+                            lastMessage: messageContent,
+                            lastMessageTime: new Date()
+                        }
+                        : contact
+                );
+                // 重新排序,将当前聊天对象移到最前面
+                return updatedContacts.sort((a, b) =>
+                    new Date(b.lastMessageTime).getTime() - new Date(a.lastMessageTime).getTime()
+                );
+            });
+
+            message.success('消息发送成功');
+
+        } catch (error) {
+            message.error('发送消息失败');
+            // 发送失败时,移除临时消息
+            setChatMessages(prev => prev.filter(msg => msg.messageId !== Date.now()));
+        } finally {
+            setSending(false);
+        }
+    };
+
+    // 选择聊天对象
+    const handleSelectUser = (userId: number) => {
+        setSelectedUserId(userId);
+        fetchChatHistory(userId);
+    };
+
+    // 格式化时间显示
+    const formatTime = (time: Date | string) => {
+        const date = new Date(time);
+        const now = new Date();
+        const diff = now.getTime() - date.getTime();
+        const minutes = Math.floor(diff / (1000 * 60));
+        const hours = Math.floor(diff / (1000 * 60 * 60));
+        const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+
+        if (minutes < 60) {
+            return `${minutes}分钟前`;
+        } else if (hours < 24) {
+            return `${hours}小时前`;
+        } else {
+            return `${days}天前`;
+        }
+    };
+
+    // 获取用户名
+    const getUserName = (userId: number) => {
+        const contact = chatContacts.find(c => c.userId === userId);
+        return contact?.userName || `用户${userId}`;
+    };
+
+    // 处理回车键发送
+    const handleKeyPress = (e: React.KeyboardEvent) => {
+        if (e.key === 'Enter' && !e.shiftKey) {
+            e.preventDefault();
+            handleSendMessage();
+        }
+    };
+
+    useEffect(() => {
+        fetchChatContacts();
+    }, []);
+
+    return (
+        <div className="message-page" style={{ height: '100vh', padding: '16px', display: 'flex', flexDirection: 'column' }}>
+            <Row gutter={16} style={{ flex: 1, overflow: 'hidden' }}>
+                {/* 左侧聊天对象列表 */}
+                <Col span={8}>
+                    <Card
+                        title="聊天列表"
+                        bordered={false}
+                        style={{ height: '100%' }}
+                        loading={loading && !selectedUserId}
+                    >
+                        <List
+                            style={{ height: 'calc(100vh - 140px)', overflowY: 'auto' }}
+                            dataSource={chatContacts}
+                            renderItem={(contact) => (
+                                <List.Item
+                                    onClick={() => handleSelectUser(contact.userId)}
+                                    style={{
+                                        cursor: 'pointer',
+                                        backgroundColor: selectedUserId === contact.userId ? '#f0f8ff' : 'transparent',
+                                        padding: '12px',
+                                        borderRadius: '8px',
+                                        margin: '4px 0'
+                                    }}
+                                    className={selectedUserId === contact.userId ? 'selected' : ''}
+                                >
+                                    <List.Item.Meta
+                                        avatar={
+                                            <Avatar size="large">
+                                                {contact.userName.charAt(0)}
+                                            </Avatar>
+                                        }
+                                        title={
+                                            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+                                                <span style={{
+                                                    fontWeight: 'normal',
+                                                    fontSize: '14px'
+                                                }}>
+                                                    {contact.userName}
+                                                </span>
+                                                <span style={{ fontSize: '12px', color: '#999' }}>
+                                                    {formatTime(contact.lastMessageTime)}
+                                                </span>
+                                            </div>
+                                        }
+                                        description={
+                                            <div style={{
+                                                color: '#666',
+                                                fontWeight: 'normal',
+                                                overflow: 'hidden',
+                                                textOverflow: 'ellipsis',
+                                                whiteSpace: 'nowrap',
+                                                fontSize: '13px'
+                                            }}>
+                                                {contact.lastMessage}
+                                            </div>
+                                        }
+                                    />
+                                </List.Item>
+                            )}
+                        />
+                    </Card>
+                </Col>
+
+                {/* 右侧聊天界面 */}
+                <Col span={16}>
+                    <div style={{
+                        height: '100%',
+                        display: 'flex',
+                        flexDirection: 'column',
+                        position: 'relative'
+                    }}>
+                        {/* 聊天标题 */}
+                        <Card
+                            title={selectedUserId ? `与 ${getUserName(selectedUserId)} 的对话` : '选择一个联系人开始聊天'}
+                            bordered={false}
+                            style={{
+                                marginBottom: 0,
+                                borderBottom: '1px solid #f0f0f0'
+                            }}
+                            bodyStyle={{ padding: 0 }}
+                        />
+
+                        {/* 聊天消息区域 */}
+                        <div style={{
+                            flex: 1,
+                            overflow: 'hidden',
+                            display: 'flex',
+                            flexDirection: 'column',
+                            backgroundColor: '#fff',
+                            border: '1px solid #f0f0f0',
+                            borderTop: 'none'
+                        }}>
+                            {selectedUserId ? (
+                                <>
+                                    <List
+                                        style={{
+                                            flex: 1,
+                                            overflowY: 'auto',
+                                            padding: '16px',
+                                            paddingBottom: '80px' // 为输入框预留空间
+                                        }}
+                                        dataSource={chatMessages}
+                                        loading={loading && selectedUserId !== null}
+                                        renderItem={(item) => (
+                                            <List.Item
+                                                className={`chat-message ${item.senderId === 1 ? 'sent' : 'received'}`}
+                                                style={{
+                                                    border: 'none',
+                                                    padding: '8px 0',
+                                                    display: 'flex',
+                                                    justifyContent: item.senderId === 1 ? 'flex-end' : 'flex-start'
+                                                }}
+                                            >
+                                                <div style={{
+                                                    maxWidth: '70%',
+                                                    display: 'flex',
+                                                    flexDirection: item.senderId === 1 ? 'row-reverse' : 'row',
+                                                    alignItems: 'flex-start',
+                                                    gap: '8px'
+                                                }}>
+                                                    <Avatar size="small">
+                                                        {item.senderId === 1 ? 'Me' : getUserName(item.senderId).charAt(0)}
+                                                    </Avatar>
+                                                    <div>
+                                                        <div style={{
+                                                            fontSize: '12px',
+                                                            color: '#999',
+                                                            marginBottom: '4px',
+                                                            textAlign: item.senderId === 1 ? 'right' : 'left'
+                                                        }}>
+                                                            {item.senderId === 1 ? '我' : getUserName(item.senderId)} · {new Date(item.createTime).toLocaleTimeString()}
+                                                        </div>
+                                                        <div style={{
+                                                            backgroundColor: item.senderId === 1 ? '#1890ff' : '#f0f0f0',
+                                                            color: item.senderId === 1 ? '#fff' : '#000',
+                                                            padding: '8px 12px',
+                                                            borderRadius: '12px',
+                                                            wordBreak: 'break-word',
+                                                            lineHeight: '1.4'
+                                                        }}>
+                                                            {item.content}
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                            </List.Item>
+                                        )}
+                                    />
+
+                                    {/* 输入框区域 - 固定在底部 */}
+                                    <div style={{
+                                        position: 'absolute',
+                                        bottom: 0,
+                                        left: 0,
+                                        right: 0,
+                                        backgroundColor: '#fff',
+                                        padding: '16px',
+                                        borderTop: '1px solid #f0f0f0',
+                                        display: 'flex',
+                                        gap: '8px',
+                                        zIndex: 10,
+                                        boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.1)'
+                                    }}>
+                                        <Input.TextArea
+                                            value={inputMessage}
+                                            onChange={(e) => setInputMessage(e.target.value)}
+                                            onKeyDown={handleKeyPress}
+                                            placeholder="输入消息...(按 Enter 发送,Shift+Enter 换行)"
+                                            style={{
+                                                flex: 1,
+                                                resize: 'none',
+                                                minHeight: '40px',
+                                                maxHeight: '120px'
+                                            }}
+                                            autoSize={{ minRows: 1, maxRows: 4 }}
+                                        />
+                                        <Button
+                                            type="primary"
+                                            onClick={handleSendMessage}
+                                            loading={sending}
+                                            style={{ height: '40px' }}
+                                        >
+                                            发送
+                                        </Button>
+                                    </div>
+                                </>
+                            ) : (
+                                <div style={{
+                                    flex: 1,
+                                    display: 'flex',
+                                    alignItems: 'center',
+                                    justifyContent: 'center',
+                                    color: '#999',
+                                    fontSize: '16px'
+                                }}>
+                                    请从左侧选择一个联系人开始聊天
+                                </div>
+                            )}
+                        </div>
+                    </div>
+                </Col>
+            </Row>
+        </div>
+    );
+};
+
+export default MessagePage;
\ No newline at end of file