Merge "新增邀请码页面,新增添加好友功能"
diff --git a/react-ui/src/pages/Invite/data.d.ts b/react-ui/src/pages/Invite/data.d.ts
new file mode 100644
index 0000000..2a07e7a
--- /dev/null
+++ b/react-ui/src/pages/Invite/data.d.ts
@@ -0,0 +1,12 @@
+// data.ts 建议的类型定义
+export interface GetInviteCodeResponse {
+ code: number;
+ msg: string;
+ data: string; // 邀请码字符串
+}
+
+export interface GetUserByInviteCodeResponse {
+ code: number;
+ msg: string;
+ data: number; // 用户ID
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Invite/index.tsx b/react-ui/src/pages/Invite/index.tsx
new file mode 100644
index 0000000..56989ff
--- /dev/null
+++ b/react-ui/src/pages/Invite/index.tsx
@@ -0,0 +1,300 @@
+import React, { useState, useEffect } from 'react';
+import { useModel } from 'umi';
+import { Card, Input, Button, Space, Typography, Row, Col, message } from 'antd';
+import { CopyOutlined, UserAddOutlined, GiftOutlined, ShareAltOutlined } from '@ant-design/icons';
+import { getUserInviteCode, getUserByInviteCode } from './service';
+
+const { Title, Text, Paragraph } = Typography;
+
+const InvitePage: React.FC = () => {
+ const [myInviteCode, setMyInviteCode] = useState<string>('');
+ const [inputInviteCode, setInputInviteCode] = useState<string>('');
+ const [loading, setLoading] = useState<boolean>(false);
+ const [submitting, setSubmitting] = useState<boolean>(false);
+
+ // 获取当前用户信息
+ const { initialState } = useModel('@@initialState');
+ const userId = initialState?.currentUser?.userId || '';
+
+ // 获取我的邀请码
+ const fetchMyInviteCode = async () => {
+ if (!userId) {
+ message.error('用户信息获取失败');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const response = await getUserInviteCode();
+ if (response.code === 200) {
+ // 根据后端返回的数据结构调整
+ setMyInviteCode(response.msg);
+ } else {
+ message.error(response.msg || '获取邀请码失败');
+ }
+ } catch (error) {
+ message.error('获取邀请码失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 复制邀请码
+ const copyInviteCode = () => {
+ if (myInviteCode) {
+ navigator.clipboard.writeText(myInviteCode).then(() => {
+ message.success('邀请码已复制到剪贴板');
+ }).catch(() => {
+ message.error('复制失败,请手动复制');
+ });
+ }
+ };
+
+ // 查询邀请码对应的用户
+ const handleSubmitInviteCode = async () => {
+ if (!inputInviteCode.trim()) {
+ message.warning('请输入邀请码');
+ return;
+ }
+
+ setSubmitting(true);
+ try {
+ const response = await getUserByInviteCode(inputInviteCode.trim());
+
+ if (response.code === 200) {
+ message.success(`邀请码有效,对应用户ID: ${response.data}`);
+ setInputInviteCode('');
+ // 这里可以根据业务需求进行后续操作,比如关注用户等
+ } else {
+ message.error(response.msg || '邀请码无效');
+ }
+ } catch (error) {
+ message.error('查询邀请码失败');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchMyInviteCode();
+ }, []);
+
+ return (
+ <div
+ style={{
+ padding: '32px 24px',
+ maxWidth: '800px',
+ margin: '0 auto',
+ minHeight: '100vh',
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+ }}
+ >
+ {/* 头部标题区域 */}
+ <div style={{ textAlign: 'center', marginBottom: '40px' }}>
+ <GiftOutlined
+ style={{
+ fontSize: '48px',
+ color: '#fff',
+ marginBottom: '16px',
+ filter: 'drop-shadow(0 4px 8px rgba(0,0,0,0.2))'
+ }}
+ />
+ <Title
+ level={1}
+ style={{
+ color: '#fff',
+ margin: 0,
+ fontSize: '32px',
+ fontWeight: 600,
+ textShadow: '0 2px 4px rgba(0,0,0,0.3)'
+ }}
+ >
+ 邀请码管理
+ </Title>
+ <Paragraph
+ style={{
+ color: 'rgba(255,255,255,0.8)',
+ fontSize: '16px',
+ margin: '8px 0 0 0'
+ }}
+ >
+ 邀请好友,共享美好体验
+ </Paragraph>
+ </div>
+
+ <Row gutter={[24, 24]}>
+ {/* 我的邀请码 */}
+ <Col xs={24} lg={12}>
+ <Card
+ title={
+ <Space>
+ <ShareAltOutlined style={{ color: '#1890ff' }} />
+ <span style={{ color: '#1890ff', fontWeight: 600 }}>我的邀请码</span>
+ </Space>
+ }
+ style={{
+ height: '100%',
+ borderRadius: '16px',
+ boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
+ border: 'none',
+ background: 'rgba(255,255,255,0.95)',
+ backdropFilter: 'blur(10px)'
+ }}
+ bodyStyle={{ padding: '24px' }}
+ >
+ <Space direction="vertical" style={{ width: '100%' }} size="large">
+ <div style={{
+ padding: '20px',
+ background: 'linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%)',
+ borderRadius: '12px',
+ textAlign: 'center'
+ }}>
+ <Text
+ type="secondary"
+ style={{
+ fontSize: '14px',
+ display: 'block',
+ marginBottom: '16px'
+ }}
+ >
+ 分享下方邀请码给好友,邀请他们加入
+ </Text>
+ <div style={{
+ background: '#fff',
+ padding: '16px',
+ borderRadius: '8px',
+ border: '2px dashed #d9d9d9',
+ marginBottom: '16px'
+ }}>
+ <Text
+ copyable={false}
+ style={{
+ fontSize: '24px',
+ fontWeight: 'bold',
+ color: '#1890ff',
+ fontFamily: 'Monaco, monospace',
+ letterSpacing: '2px'
+ }}
+ >
+ {loading ? '加载中...' : (myInviteCode || '暂无邀请码')}
+ </Text>
+ </div>
+ <Button
+ type="primary"
+ size="large"
+ icon={<CopyOutlined />}
+ onClick={copyInviteCode}
+ disabled={!myInviteCode}
+ style={{
+ borderRadius: '8px',
+ height: '44px',
+ fontSize: '16px',
+ fontWeight: 500,
+ boxShadow: '0 4px 12px rgba(24, 144, 255, 0.3)'
+ }}
+ block
+ >
+ 复制邀请码
+ </Button>
+ </div>
+ </Space>
+ </Card>
+ </Col>
+
+ {/* 输入邀请码 */}
+ <Col xs={24} lg={12}>
+ <Card
+ title={
+ <Space>
+ <UserAddOutlined style={{ color: '#52c41a' }} />
+ <span style={{ color: '#52c41a', fontWeight: 600 }}>查询邀请码</span>
+ </Space>
+ }
+ style={{
+ height: '100%',
+ borderRadius: '16px',
+ boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
+ border: 'none',
+ background: 'rgba(255,255,255,0.95)',
+ backdropFilter: 'blur(10px)'
+ }}
+ bodyStyle={{ padding: '24px' }}
+ >
+ <Space direction="vertical" style={{ width: '100%' }} size="large">
+ <div style={{
+ padding: '20px',
+ background: 'linear-gradient(135deg, #f6ffed 0%, #e6f7ff 100%)',
+ borderRadius: '12px'
+ }}>
+ <Text
+ type="secondary"
+ style={{
+ fontSize: '14px',
+ display: 'block',
+ marginBottom: '20px',
+ textAlign: 'center'
+ }}
+ >
+ 输入邀请码
+ </Text>
+ <Space direction="vertical" style={{ width: '100%' }} size="middle">
+ <Input
+ value={inputInviteCode}
+ onChange={(e) => setInputInviteCode(e.target.value)}
+ placeholder="请输入邀请码"
+ size="large"
+ style={{
+ borderRadius: '8px',
+ fontSize: '16px',
+ height: '48px',
+ textAlign: 'center',
+ fontFamily: 'Monaco, monospace',
+ letterSpacing: '1px'
+ }}
+ onPressEnter={handleSubmitInviteCode}
+ />
+ <Button
+ type="primary"
+ size="large"
+ icon={<UserAddOutlined />}
+ onClick={handleSubmitInviteCode}
+ loading={submitting}
+ disabled={!inputInviteCode.trim()}
+ style={{
+ borderRadius: '8px',
+ height: '44px',
+ fontSize: '16px',
+ fontWeight: 500,
+ background: '#52c41a',
+ borderColor: '#52c41a',
+ boxShadow: '0 4px 12px rgba(82, 196, 26, 0.3)'
+ }}
+ block
+ >
+ 邀请码
+ </Button>
+ </Space>
+ </div>
+ </Space>
+ </Card>
+ </Col>
+ </Row>
+
+ {/* 底部装饰 */}
+ <div style={{
+ textAlign: 'center',
+ marginTop: '40px',
+ padding: '20px',
+ background: 'rgba(255,255,255,0.1)',
+ borderRadius: '12px',
+ backdropFilter: 'blur(10px)'
+ }}>
+ <Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px' }}>
+ 通过邀请码连接更多朋友,一起享受精彩体验
+ </Text>
+ </div>
+ </div>
+ );
+};
+
+export default InvitePage;
\ No newline at end of file
diff --git a/react-ui/src/pages/Invite/service.ts b/react-ui/src/pages/Invite/service.ts
new file mode 100644
index 0000000..e6cf9af
--- /dev/null
+++ b/react-ui/src/pages/Invite/service.ts
@@ -0,0 +1,19 @@
+import { request } from '@umijs/max';
+import type {
+ GetInviteCodeResponse,
+ GetUserByInviteCodeResponse
+} from './data';
+
+// 获取用户邀请码
+export async function getUserInviteCode(): Promise<GetInviteCodeResponse> {
+ return request('/api/system/user/invite/code', {
+ method: 'GET', // 修改为 GET 请求
+ });
+}
+
+// 根据邀请码获取用户信息
+export async function getUserByInviteCode(code: string): Promise<GetUserByInviteCodeResponse> {
+ return request(`/api/system/user/invite/user/${code}`, {
+ method: 'GET', // 修改为 GET 请求,并使用路径参数
+ });
+}
\ No newline at end of file
diff --git a/react-ui/src/pages/Message/index.tsx b/react-ui/src/pages/Message/index.tsx
index 04f9881..9ae9467 100644
--- a/react-ui/src/pages/Message/index.tsx
+++ b/react-ui/src/pages/Message/index.tsx
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from 'react';
-import { Card, List, Avatar, Input, Button, Row, Col, message } from 'antd';
+import { Card, List, Avatar, Input, Button, Row, Col, message, Modal, Form } from 'antd';
+import { UserAddOutlined, SearchOutlined } from '@ant-design/icons';
import { SysUserMessage, ChatContact } from './data.d';
-import { getChatContactList, getChatHistory, sendMessage, getUserInfo } from './service';
+import { getChatContactList, getChatHistory, sendMessage, getUserInfo, addFriend } from './service';
import './index.less';
+import { useModel } from 'umi';
const MessagePage: React.FC = () => {
const [chatContacts, setChatContacts] = useState<ChatContact[]>([]);
@@ -11,6 +13,12 @@
const [chatMessages, setChatMessages] = useState<SysUserMessage[]>([]);
const [loading, setLoading] = useState(false);
const [sending, setSending] = useState(false);
+ const { initialState } = useModel('@@initialState');
+ const ccuserId = initialState?.currentUser?.userId || '';
+ // 添加好友相关状态
+ const [addFriendVisible, setAddFriendVisible] = useState(false);
+ const [addFriendLoading, setAddFriendLoading] = useState(false);
+ const [form] = Form.useForm();
// 获取聊天对象列表
const fetchChatContacts = async () => {
@@ -36,7 +44,7 @@
setLoading(true);
const response = await getChatHistory({
userId,
- currentUserId: 1, // 假设当前用户ID为1,实际应该从用户状态获取
+ currentUserId: Number(ccuserId), // 假设当前用户ID为1,实际应该从用户状态获取
pageSize: 100 // 获取最近100条消息
});
@@ -110,6 +118,26 @@
}
};
+ // 添加好友
+ const handleAddFriend = async (values: { username: string }) => {
+ try {
+ setAddFriendLoading(true);
+ await addFriend({ userId: Number(ccuserId), authorUsername: values.username });
+ message.success('好友添加成功');
+
+ // 重新获取聊天对象列表
+ await fetchChatContacts();
+
+ // 关闭弹窗并重置表单
+ setAddFriendVisible(false);
+ form.resetFields();
+ } catch (error) {
+ message.error('添加好友失败,请检查用户名是否正确');
+ } finally {
+ setAddFriendLoading(false);
+ }
+ };
+
// 选择聊天对象
const handleSelectUser = (userId: number) => {
setSelectedUserId(userId);
@@ -137,7 +165,6 @@
// 获取用户名
const getUserName = (userId: number) => {
const contact = chatContacts.find(c => c.userId === userId);
-
return contact?.nickName || `用户${userId}`;
};
@@ -159,7 +186,19 @@
{/* 左侧聊天对象列表 */}
<Col span={8}>
<Card
- title="聊天列表"
+ title={
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+ <span>聊天列表</span>
+ <Button
+ type="primary"
+ icon={<UserAddOutlined />}
+ size="small"
+ onClick={() => setAddFriendVisible(true)}
+ >
+ 添加好友
+ </Button>
+ </div>
+ }
bordered={false}
style={{ height: '100%' }}
loading={loading && !selectedUserId}
@@ -180,11 +219,11 @@
className={selectedUserId === contact.userId ? 'selected' : ''}
>
<List.Item.Meta
- // avatar={
- // <Avatar size="large">
- // {contact.nickName.charAt(0)}
- // </Avatar>
- // }
+ avatar={
+ <Avatar size="large">
+ {contact.nickName.charAt(0).toUpperCase()}
+ </Avatar>
+ }
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{
@@ -355,6 +394,59 @@
</div>
</Col>
</Row>
+
+ {/* 添加好友弹窗 */}
+ <Modal
+ title="添加好友"
+ open={addFriendVisible}
+ onCancel={() => {
+ setAddFriendVisible(false);
+ form.resetFields();
+ }}
+ footer={null}
+ width={400}
+ >
+ <Form
+ form={form}
+ layout="vertical"
+ onFinish={handleAddFriend}
+ style={{ marginTop: '20px' }}
+ >
+ <Form.Item
+ label="用户名"
+ name="username"
+ rules={[
+ { required: true, message: '请输入用户名' },
+ { min: 2, message: '用户名至少2个字符' },
+ { max: 20, message: '用户名最多20个字符' }
+ ]}
+ >
+ <Input
+ placeholder="请输入要添加的用户名"
+ prefix={<SearchOutlined />}
+ size="large"
+ />
+ </Form.Item>
+ <Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
+ <Button
+ onClick={() => {
+ setAddFriendVisible(false);
+ form.resetFields();
+ }}
+ style={{ marginRight: '8px' }}
+ >
+ 取消
+ </Button>
+ <Button
+ type="primary"
+ htmlType="submit"
+ loading={addFriendLoading}
+ >
+ 添加
+ </Button>
+ </Form.Item>
+ </Form>
+ </Modal>
</div>
);
};
diff --git a/react-ui/src/pages/Message/service.ts b/react-ui/src/pages/Message/service.ts
index c5bc356..4eb3840 100644
--- a/react-ui/src/pages/Message/service.ts
+++ b/react-ui/src/pages/Message/service.ts
@@ -112,6 +112,20 @@
}
}
+// 添加好友
+export async function addFriend(params: { userId: number, authorUsername: string }) {
+ try {
+ return await request('api/system/user/follow', {
+ method: 'post',
+ data: params,
+ });
+ } catch (error) {
+ // 发送消息失败时记录错误但不返回默认数据,让组件处理错误
+ console.error('发送消息接口异常:', error);
+ throw error; // 重新抛出错误,让调用方处理
+ }
+};
+
/** 获取用户信息(用于聊天对象显示) */
export async function getUserInfo(userId: number) {
// 默认用户信息
diff --git a/react-ui/src/pages/Reward/components/UpdateForm.tsx b/react-ui/src/pages/Reward/components/UpdateForm.tsx
index 526b946..a7d9444 100644
--- a/react-ui/src/pages/Reward/components/UpdateForm.tsx
+++ b/react-ui/src/pages/Reward/components/UpdateForm.tsx
@@ -85,8 +85,8 @@
</Form.Item>
<Form.Item
name="amount"
- label="悬赏金额"
- rules={[{ required: true, message: '请输入悬赏金额!' }]}
+ label="悬赏积分"
+ rules={[{ required: true, message: '请输入悬赏积分!' }]}
>
<InputNumber
style={{ width: '100%' }}
diff --git a/react-ui/src/pages/Reward/index.tsx b/react-ui/src/pages/Reward/index.tsx
index e55cfc1..42a9bac 100644
--- a/react-ui/src/pages/Reward/index.tsx
+++ b/react-ui/src/pages/Reward/index.tsx
@@ -1,4 +1,4 @@
-import { ExclamationCircleOutlined, PlusOutlined, DeleteOutlined, UploadOutlined } from '@ant-design/icons';
+import { ExclamationCircleOutlined, PlusOutlined, DeleteOutlined, UploadOutlined, TrophyOutlined } from '@ant-design/icons';
import { Button, message, Modal, Switch, Upload, Form } from 'antd';
import React, { useRef, useState, useEffect } from 'react';
import { FormattedMessage, useIntl } from 'umi';
@@ -223,10 +223,21 @@
valueType: 'text',
},
{
- title: '悬赏金额',
+ title: (
+ <span>
+ <TrophyOutlined style={{ marginRight: 4, color: '#faad14' }} />
+ 悬赏积分
+ </span>
+ ),
dataIndex: 'amount',
- valueType: 'money',
+ valueType: 'digit',
hideInSearch: true,
+ render: (_, record) => (
+ <span style={{ color: '#faad14', fontWeight: 'bold' }}>
+ <TrophyOutlined style={{ marginRight: 4 }} />
+ {record.amount}
+ </span>
+ ),
},
// {
// title: '悬赏状态',
@@ -283,6 +294,7 @@
size="small"
key="accept"
style={{ color: '#52c41a' }}
+ icon={<TrophyOutlined />}
onClick={() => {
setCurrentAcceptReward(record);
setAcceptModalVisible(true);
@@ -468,7 +480,12 @@
{/* 接悬赏模态框 */}
<Modal
- title={`接悬赏 - ${currentAcceptReward?.title || ''}`}
+ title={
+ <span>
+ <TrophyOutlined style={{ color: '#faad14', marginRight: 8 }} />
+ 接悬赏 - {currentAcceptReward?.title || ''}
+ </span>
+ }
open={acceptModalVisible}
onOk={handleAcceptSubmit}
onCancel={() => {
@@ -482,7 +499,13 @@
>
<div style={{ marginBottom: 16 }}>
<p><strong>悬赏标题:</strong>{currentAcceptReward?.title}</p>
- <p><strong>悬赏金额:</strong>¥{currentAcceptReward?.amount}</p>
+ <p>
+ <strong>悬赏积分:</strong>
+ <span style={{ color: '#faad14', fontWeight: 'bold' }}>
+ <TrophyOutlined style={{ marginRight: 4 }} />
+ {currentAcceptReward?.amount}
+ </span>
+ </p>
<p><strong>备注:</strong>{currentAcceptReward?.remark || '无'}</p>
</div>
diff --git a/react-ui/src/pages/Torrent/Comments/index.tsx b/react-ui/src/pages/Torrent/Comments/index.tsx
index 05c5165..3dd3c68 100644
--- a/react-ui/src/pages/Torrent/Comments/index.tsx
+++ b/react-ui/src/pages/Torrent/Comments/index.tsx
@@ -5,7 +5,6 @@
import { Layout } from 'antd';
import { listComments, addComment } from './service';
import { responseSysTorrentComment, SysTorrentComment } from './data';
-
const { Content } = Layout;
const { TextArea } = Input;