评论连接优化,新增私信页面,等私信后端写完
Change-Id: I63c05945c47be9bcba6113ddd299058f302cb927
diff --git a/react-ui/src/components/Footer/index.tsx b/react-ui/src/components/Footer/index.tsx
index f204ac2..162587e 100644
--- a/react-ui/src/components/Footer/index.tsx
+++ b/react-ui/src/components/Footer/index.tsx
@@ -4,31 +4,32 @@
const Footer: React.FC = () => {
return (
- <DefaultFooter
- style={{
- background: 'none',
- }}
- links={[
- {
- key: 'Ant Design Pro',
- title: 'Ant Design Pro',
- href: 'https://pro.ant.design',
- blankTarget: true,
- },
- {
- key: 'github',
- title: <GithubOutlined />,
- href: 'https://github.com/ant-design/ant-design-pro',
- blankTarget: true,
- },
- {
- key: 'Ant Design',
- title: 'Ant Design',
- href: 'https://ant.design',
- blankTarget: true,
- },
- ]}
- />
+ // <DefaultFooter
+ // style={{
+ // background: 'none',
+ // }}
+ // links={[
+ // {
+ // key: 'Ant Design Pro',
+ // title: 'Ant Design Pro',
+ // href: 'https://pro.ant.design',
+ // blankTarget: true,
+ // },
+ // {
+ // key: 'github',
+ // title: <GithubOutlined />,
+ // href: 'https://github.com/ant-design/ant-design-pro',
+ // blankTarget: true,
+ // },
+ // {
+ // key: 'Ant Design',
+ // title: 'Ant Design',
+ // href: 'https://ant.design',
+ // blankTarget: true,
+ // },
+ // ]}
+ // />
+ <></>
);
};
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),
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysTorrentCommentMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysTorrentCommentMapper.xml
index 139e0e1..00cc579 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysTorrentCommentMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysTorrentCommentMapper.xml
@@ -1,11 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysTorrentCommentMapper">
+ <resultMap id="SysTorrentCommentMap" type="com.ruoyi.system.domain.SysTorrentComment">
+ <id property="commentId" column="comment_id"/>
+ <result property="torrentId" column="torrent_id"/>
+ <result property="userId" column="user_id"/>
+ <result property="content" column="content"/>
+ <result property="parentId" column="parent_id"/>
+ <result property="createTime" column="create_time"/>
+ </resultMap>
+
<insert id="insertComment" parameterType="com.ruoyi.system.domain.SysTorrentComment">
insert into sys_torrent_comment (torrent_id, user_id, content, parent_id, create_time)
values (#{torrentId}, #{userId}, #{content}, #{parentId}, sysdate())
</insert>
- <select id="selectCommentListByTorrentId" resultType="com.ruoyi.system.domain.SysTorrentComment">
+ <select id="selectCommentListByTorrentId" resultMap="SysTorrentCommentMap">
select comment_id, torrent_id, user_id, content, parent_id, create_time
from sys_torrent_comment
where torrent_id = #{torrentId}