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

Change-Id: I63c05945c47be9bcba6113ddd299058f302cb927
diff --git a/react-ui/src/pages/Message/data.d.ts b/react-ui/src/pages/Message/data.d.ts
new file mode 100644
index 0000000..2a537ea
--- /dev/null
+++ b/react-ui/src/pages/Message/data.d.ts
@@ -0,0 +1,63 @@
+// 消息接口
+export interface SysUserMessage {
+    messageId: number;
+    senderId: number;
+    receiverId: number;
+    content: string;
+    createTime: Date;
+    delFlag: string;
+}
+
+// 聊天对象接口
+export interface ChatContact {
+    userId: number;
+    userName: string;
+    avatar?: string;
+    lastMessage: string;
+    lastMessageTime: Date;
+}
+
+// 获取聊天对象列表参数
+export interface ChatContactListParams {
+    pageNum?: number;
+    pageSize?: number;
+    keyword?: string; // 搜索关键词
+}
+
+// 获取聊天记录参数
+export interface ChatHistoryParams {
+    userId: number; // 聊天对象用户ID
+    currentUserId?: number; // 当前用户ID
+    pageNum?: number;
+    pageSize?: number;
+}
+
+// 发送消息参数
+export interface SendMessageParams {
+    receiverId: number;
+    content: string;
+}
+
+// 用户信息接口
+export interface UserInfo {
+    userId: number;
+    userName: string;
+    avatar?: string;
+}
+
+// API 响应基础接口
+export interface ApiResponse<T = any> {
+    code: number;
+    message: string;
+    data: T;
+    success: boolean;
+}
+
+// 分页响应接口
+export interface PageResponse<T = any> {
+    list: T[];
+    total: number;
+    pageNum: number;
+    pageSize: number;
+    pages: number;
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Message/index.less b/react-ui/src/pages/Message/index.less
new file mode 100644
index 0000000..0911278
--- /dev/null
+++ b/react-ui/src/pages/Message/index.less
@@ -0,0 +1,72 @@
+.message-page {
+    .selected {
+        background-color: #f0f2f5;
+    }
+
+    .chat-message {
+        margin: 8px 0;
+        padding: 8px;
+        border-radius: 4px;
+
+        &.sent {
+            background-color: #e6f7ff;
+            margin-left: 40px;
+        }
+
+        &.received {
+            background-color: #f0f2f5;
+            margin-right: 40px;
+        }
+    }
+
+    .message-input {
+        border-top: 1px solid #f0f0f0;
+        padding-top: 16px;
+    }
+
+    .message-list {
+        height: calc(100vh - 200px);
+        overflow-y: auto;
+    }
+
+    .chat-container {
+        display: flex;
+        flex-direction: column;
+        height: calc(100vh - 100px);
+    }
+
+    .chat-messages {
+        flex: 1;
+        overflow-y: auto;
+        padding: 16px;
+    }
+
+    .ant-list-item {
+        transition: background-color 0.3s;
+
+        &:hover {
+            background-color: #f5f5f5;
+        }
+    }
+
+}
+
+.message-page {
+    .chat-message {
+        &.sent {
+            .ant-list-item-meta {
+                text-align: right;
+            }
+        }
+
+        &.received {
+            .ant-list-item-meta {
+                text-align: left;
+            }
+        }
+    }
+
+    .selected {
+        background-color: #f0f8ff;
+    }
+}
\ No newline at end of file
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
diff --git a/react-ui/src/pages/Message/service.ts b/react-ui/src/pages/Message/service.ts
new file mode 100644
index 0000000..958290e
--- /dev/null
+++ b/react-ui/src/pages/Message/service.ts
@@ -0,0 +1,155 @@
+import { request } from '@umijs/max';
+import type {
+    SysUserMessage,
+    ChatContact,
+    ChatContactListParams,
+    ChatHistoryParams,
+    SendMessageParams,
+} from './data.d';
+
+// API 路径配置 - 可根据实际后端接口调整
+const API_CONFIG = {
+    CHAT_CONTACTS: '/api/system/message/contacts',  // 可改为: '/api/message/contacts' 或其他路径
+    CHAT_HISTORY: '/api/system/message/list',    // 可改为: '/api/message/history' 或其他路径
+    SEND_MESSAGE: '/api/system/message',       // 可改为: '/api/message/send' 或其他路径
+    USER_INFO: '/api/system/user',                  // 可改为: '/api/user' 或其他路径
+};
+
+/** 获取聊天对象列表 */
+export async function getChatContactList(params?: ChatContactListParams) {
+    // 默认数据
+    const defaultData = [
+        {
+            userId: 2,
+            userName: '张三',
+            avatar: '',
+            lastMessage: '你好,请问有什么可以帮助你的吗?',
+            lastMessageTime: new Date(Date.now() - 1000 * 60 * 30),
+            unreadCount: 2
+        },
+        {
+            userId: 3,
+            userName: '李四',
+            avatar: '',
+            lastMessage: '关于最近的活动,我想了解更多细节。',
+            lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 2),
+            unreadCount: 0
+        },
+        {
+            userId: 4,
+            userName: '王五',
+            avatar: '',
+            lastMessage: '好的,我知道了,谢谢!',
+            lastMessageTime: new Date(Date.now() - 1000 * 60 * 60 * 24),
+            unreadCount: 1
+        }
+    ];
+
+    try {
+        const queryString = params
+            ? `?${new URLSearchParams(params as Record<string, any>).toString()}`
+            : '';
+        const response = await request(`${API_CONFIG.CHAT_CONTACTS}${queryString}`, {
+            method: 'get',
+        });
+
+        // 如果接口返回空数据,使用默认数据
+        if (!response || response.length === 0) {
+            return defaultData;
+        }
+        return response;
+    } catch (error) {
+        // 接口报错时返回默认数据
+        console.warn('获取聊天对象列表接口异常,使用默认数据:', error);
+        return defaultData;
+    }
+
+}
+
+/** 获取与指定用户的聊天记录 */
+export async function getChatHistory(params: ChatHistoryParams) {
+    // 默认数据
+    const defaultData = [
+        {
+            messageId: 1,
+            senderId: params.userId,
+            receiverId: params.currentUserId || 1,
+            content: `这是来自用户${params.userId}的第一条消息`,
+            createTime: new Date(Date.now() - 1000 * 60 * 60 * 2),
+            delFlag: '0'
+        },
+        {
+            messageId: 2,
+            senderId: params.currentUserId || 1,
+            receiverId: params.userId,
+            content: '收到,感谢你的消息',
+            createTime: new Date(Date.now() - 1000 * 60 * 60),
+            delFlag: '0'
+        },
+        {
+            messageId: 3,
+            senderId: params.userId,
+            receiverId: params.currentUserId || 1,
+            content: `这是来自用户${params.userId}的最新消息,包含更多详细信息`,
+            createTime: new Date(Date.now() - 1000 * 60 * 30),
+            delFlag: '0'
+        }
+    ];
+
+    // try {
+    //     const queryString = `?${new URLSearchParams(params as Record<string, any>).toString()}`;
+    //     const response = await request(`${API_CONFIG.CHAT_HISTORY}${queryString}`, {
+    //         method: 'get',
+    //     });
+
+    //     // 如果接口返回空数据,使用默认数据
+    //     if (!response || response.length === 0) {
+    //         return defaultData;
+    //     }
+    //     return response;
+    // } catch (error) {
+    //     // 接口报错时返回默认数据
+    //     console.warn('获取聊天记录接口异常,使用默认数据:', error);
+    //     return defaultData;
+    // }
+    return defaultData;
+}
+
+/** 发送消息 */
+export async function sendMessage(params: SendMessageParams) {
+    try {
+        return await request(API_CONFIG.SEND_MESSAGE, {
+            method: 'post',
+            data: params,
+        });
+    } catch (error) {
+        // 发送消息失败时记录错误但不返回默认数据,让组件处理错误
+        console.error('发送消息接口异常:', error);
+        throw error; // 重新抛出错误,让调用方处理
+    }
+}
+
+/** 获取用户信息(用于聊天对象显示) */
+export async function getUserInfo(userId: number) {
+    // 默认用户信息
+    const defaultUserInfo = {
+        userId,
+        userName: `用户${userId}`,
+        avatar: ''
+    };
+
+    try {
+        const response = await request(`${API_CONFIG.USER_INFO}/${userId}`, {
+            method: 'get',
+        });
+
+        if (!response) {
+            return defaultUserInfo;
+        }
+        return response;
+    } catch (error) {
+        // 接口报错时返回默认用户信息
+        console.warn(`获取用户${userId}信息接口异常,使用默认数据:`, error);
+        return defaultUserInfo;
+    }
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Torrent/Comments/data.d.ts b/react-ui/src/pages/Torrent/Comments/data.d.ts
index 1f3347b..bc9e517 100644
--- a/react-ui/src/pages/Torrent/Comments/data.d.ts
+++ b/react-ui/src/pages/Torrent/Comments/data.d.ts
@@ -1,5 +1,6 @@
 /** 种子评论表 */
 export interface SysTorrentComment {
+    userId: string;
     /** 评论ID */
     commentId: number;
     /** 种子ID */
diff --git a/react-ui/src/pages/Torrent/Comments/index.tsx b/react-ui/src/pages/Torrent/Comments/index.tsx
index 58167f7..05c5165 100644
--- a/react-ui/src/pages/Torrent/Comments/index.tsx
+++ b/react-ui/src/pages/Torrent/Comments/index.tsx
@@ -11,6 +11,7 @@
 
 interface CommentItem {
     id: number;
+    name: string;
     content: string;
     createTime: string;
     createBy: string;
@@ -93,6 +94,7 @@
             if (res) {
                 const formattedComments: SysTorrentComment[] = res.data.map((comment: SysTorrentComment) => ({
                     id: comment.commentId ?? 0,
+                    name: comment.userName ?? '匿名用户',
                     content: comment.content ?? '无内容',
                     createTime: comment.createTime || '',
                     createBy: getUserDisplayName(comment.userId),