blob: 05c516531699d52931e65adc366f193419c8eafa [file] [log] [blame]
BirdNETMed9f2f92025-05-26 20:12:30 +08001import React, { useState, useEffect, useCallback } from 'react';
BirdNETMb0f71532025-05-26 17:37:33 +08002import { useParams, useNavigate } from 'react-router-dom';
BirdNETMed9f2f92025-05-26 20:12:30 +08003import { Button, Card, Form, Input, List, message, Avatar, Spin, Empty } from 'antd';
4import { ArrowLeftOutlined, UserOutlined } from '@ant-design/icons';
BirdNETMb0f71532025-05-26 17:37:33 +08005import { Layout } from 'antd';
BirdNETMed9f2f92025-05-26 20:12:30 +08006import { listComments, addComment } from './service';
7import { responseSysTorrentComment, SysTorrentComment } from './data';
BirdNETMb0f71532025-05-26 17:37:33 +08008
9const { Content } = Layout;
10const { TextArea } = Input;
11
12interface CommentItem {
13 id: number;
BirdNETM632c0612025-05-27 17:41:40 +080014 name: string;
BirdNETMb0f71532025-05-26 17:37:33 +080015 content: string;
16 createTime: string;
17 createBy: string;
18 torrentId: number;
19}
20
21const TorrentComments: React.FC = () => {
22 const { torrentId } = useParams<{ torrentId: string }>();
23 const navigate = useNavigate();
24 const [form] = Form.useForm();
25 const [comments, setComments] = useState<CommentItem[]>([]);
26 const [submitting, setSubmitting] = useState(false);
BirdNETMed9f2f92025-05-26 20:12:30 +080027 const [loading, setLoading] = useState(false);
BirdNETMb0f71532025-05-26 17:37:33 +080028
BirdNETMed9f2f92025-05-26 20:12:30 +080029 // 格式化时间
30 const formatTime = (time: string | Date | null | undefined): string => {
31 if (!time) return '未知时间';
32
BirdNETMb0f71532025-05-26 17:37:33 +080033 try {
BirdNETMed9f2f92025-05-26 20:12:30 +080034 const date = new Date(time);
35 const now = new Date();
36 const diff = now.getTime() - date.getTime();
37
38 // 小于1分钟
39 if (diff < 60 * 1000) {
40 return '刚刚';
41 }
42 // 小于1小时
43 if (diff < 60 * 60 * 1000) {
44 return `${Math.floor(diff / (60 * 1000))}分钟前`;
45 }
46 // 小于24小时
47 if (diff < 24 * 60 * 60 * 1000) {
48 return `${Math.floor(diff / (60 * 60 * 1000))}小时前`;
49 }
50 // 小于7天
51 if (diff < 7 * 24 * 60 * 60 * 1000) {
52 return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`;
53 }
54
55 // 否则显示具体日期
56 return date.toLocaleDateString('zh-CN', {
57 year: 'numeric',
58 month: '2-digit',
59 day: '2-digit',
60 hour: '2-digit',
61 minute: '2-digit'
62 });
BirdNETMb0f71532025-05-26 17:37:33 +080063 } catch (error) {
BirdNETMed9f2f92025-05-26 20:12:30 +080064 return '未知时间';
BirdNETMb0f71532025-05-26 17:37:33 +080065 }
66 };
67
BirdNETMed9f2f92025-05-26 20:12:30 +080068 // 获取用户显示名称
69 const getUserDisplayName = (userId: number | null | undefined): string => {
70 if (!userId) return '匿名用户';
71 return `用户${userId}`;
72 };
73
74 // 获取用户头像字符
75 const getAvatarChar = (userName: string): string => {
76 if (!userName || userName === '匿名用户') return '?';
77 // 如果是"用户123"格式,取数字的最后一位
78 const match = userName.match(/\d+$/);
79 if (match) {
80 return match[0].slice(-1);
81 }
82 return userName[0].toUpperCase();
83 };
84
85 // 获取评论列表
86 const fetchComments = useCallback(async () => {
87 if (!torrentId) return;
88
89 setLoading(true);
90 try {
91
92 const res = await listComments(Number(torrentId));
93 console.log('获取评论列表:', res);
94 if (res) {
95 const formattedComments: SysTorrentComment[] = res.data.map((comment: SysTorrentComment) => ({
96 id: comment.commentId ?? 0,
BirdNETM632c0612025-05-27 17:41:40 +080097 name: comment.userName ?? '匿名用户',
BirdNETMed9f2f92025-05-26 20:12:30 +080098 content: comment.content ?? '无内容',
99 createTime: comment.createTime || '',
100 createBy: getUserDisplayName(comment.userId),
101 torrentId: comment.torrentId ?? 0,
102 }));
103
104 // 按时间倒序排列,最新的在前面
105 formattedComments.sort((a, b) => {
106 const timeA = a.createTime ? new Date(a.createTime).getTime() : 0;
107 const timeB = b.createTime ? new Date(b.createTime).getTime() : 0;
108 return timeB - timeA;
109 });
110
111 setComments(formattedComments);
112 } else {
113 message.error(res?.msg || '获取评论列表失败');
114 setComments([]);
115 }
116 } catch (error) {
117 console.error('获取评论失败:', error);
118 message.error('获取评论失败,请稍后重试');
119 setComments([]);
120 } finally {
121 setLoading(false);
122 }
123 }, [torrentId]);
124
BirdNETMb0f71532025-05-26 17:37:33 +0800125 useEffect(() => {
126 if (torrentId) {
127 fetchComments();
128 }
BirdNETMed9f2f92025-05-26 20:12:30 +0800129 }, [torrentId, fetchComments]);
BirdNETMb0f71532025-05-26 17:37:33 +0800130
131 // 提交评论
132 const handleSubmit = async () => {
133 try {
134 const values = await form.validateFields();
135 setSubmitting(true);
136
BirdNETMed9f2f92025-05-26 20:12:30 +0800137 // 调用添加评论的API
138 const response = await addComment({
139 torrentId: Number(torrentId),
140 content: values.content.trim(),
141 });
BirdNETMb0f71532025-05-26 17:37:33 +0800142
BirdNETMed9f2f92025-05-26 20:12:30 +0800143 if (response?.code === 200) {
144 message.success('评论成功');
145 form.resetFields();
146 // 刷新评论列表
147 await fetchComments();
148 } else {
149 message.error(response?.msg || '评论失败,请稍后重试');
150 }
BirdNETMb0f71532025-05-26 17:37:33 +0800151 } catch (error) {
BirdNETMed9f2f92025-05-26 20:12:30 +0800152 console.error('评论失败:', error);
153 message.error('评论失败,请稍后重试');
BirdNETMb0f71532025-05-26 17:37:33 +0800154 } finally {
155 setSubmitting(false);
156 }
157 };
158
BirdNETMed9f2f92025-05-26 20:12:30 +0800159 // 处理按下 Ctrl+Enter 提交
160 const handleKeyDown = (e: React.KeyboardEvent) => {
161 if (e.ctrlKey && e.key === 'Enter' && !submitting) {
162 handleSubmit();
163 }
164 };
165
BirdNETMb0f71532025-05-26 17:37:33 +0800166 return (
167 <Content style={{
168 height: '100vh',
169 display: 'flex',
170 flexDirection: 'column',
BirdNETMed9f2f92025-05-26 20:12:30 +0800171 overflow: 'hidden',
172 backgroundColor: '#f5f5f5'
BirdNETMb0f71532025-05-26 17:37:33 +0800173 }}>
174 {/* 顶部标题栏 */}
175 <div style={{
176 padding: '16px',
177 borderBottom: '1px solid #f0f0f0',
178 display: 'flex',
179 alignItems: 'center',
180 backgroundColor: '#fff',
BirdNETMed9f2f92025-05-26 20:12:30 +0800181 zIndex: 10,
182 boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)'
BirdNETMb0f71532025-05-26 17:37:33 +0800183 }}>
184 <Button
185 type="link"
186 icon={<ArrowLeftOutlined />}
187 onClick={() => navigate(-1)}
188 style={{ marginRight: '10px', padding: 0 }}
189 />
BirdNETMed9f2f92025-05-26 20:12:30 +0800190 <span style={{ fontSize: '16px', fontWeight: 'bold' }}>
191 种子评论 {comments.length > 0 && `(${comments.length})`}
192 </span>
BirdNETMb0f71532025-05-26 17:37:33 +0800193 </div>
194
195 {/* 评论列表区域 - 可滚动 */}
196 <div style={{
197 flex: 1,
198 overflowY: 'auto',
BirdNETMed9f2f92025-05-26 20:12:30 +0800199 padding: '16px',
200 paddingBottom: '8px'
BirdNETMb0f71532025-05-26 17:37:33 +0800201 }}>
BirdNETMed9f2f92025-05-26 20:12:30 +0800202 <Spin spinning={loading}>
203 {comments.length === 0 && !loading ? (
204 <Empty
205 description="暂无评论"
206 style={{ marginTop: '60px' }}
207 />
208 ) : (
209 <List
210 className="comment-list"
211 itemLayout="horizontal"
212 dataSource={comments}
213 renderItem={(item) => (
214 <Card
215 style={{
216 marginBottom: '12px',
217 borderRadius: '8px',
218 boxShadow: '0 1px 2px rgba(0, 0, 0, 0.03)'
219 }}
220 bodyStyle={{ padding: '12px 16px' }}
221 >
222 <List.Item style={{ border: 'none', padding: 0 }}>
223 <List.Item.Meta
224 avatar={
225 <Avatar
226 style={{
227 backgroundColor: '#1890ff',
228 verticalAlign: 'middle'
229 }}
230 size="default"
231 icon={<UserOutlined />}
232 >
233 {getAvatarChar(item.createBy)}
234 </Avatar>
235 }
236 title={
237 <div style={{
238 display: 'flex',
239 justifyContent: 'space-between',
240 alignItems: 'center'
241 }}>
242 <span style={{ fontWeight: 500 }}>{item.createBy}</span>
243 <span style={{
244 color: '#8c8c8c',
245 fontSize: '12px',
246 fontWeight: 'normal'
247 }}>
248 {formatTime(item.createTime)}
249 </span>
250 </div>
251 }
252 description={
253 <div style={{
254 marginTop: '8px',
255 color: '#262626',
256 fontSize: '14px',
257 lineHeight: '22px',
258 wordBreak: 'break-word'
259 }}>
260 {item.content}
261 </div>
262 }
263 />
264 </List.Item>
265 </Card>
266 )}
267 />
BirdNETMb0f71532025-05-26 17:37:33 +0800268 )}
BirdNETMed9f2f92025-05-26 20:12:30 +0800269 </Spin>
BirdNETMb0f71532025-05-26 17:37:33 +0800270 </div>
271
BirdNETMed9f2f92025-05-26 20:12:30 +0800272 {/* 评论输入框 - 固定在底部 */}
BirdNETMb0f71532025-05-26 17:37:33 +0800273 <div style={{
BirdNETMb0f71532025-05-26 17:37:33 +0800274 padding: '16px',
275 backgroundColor: '#fff',
276 borderTop: '1px solid #f0f0f0',
277 boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.06)'
278 }}>
BirdNETMed9f2f92025-05-26 20:12:30 +0800279 <Form form={form} onFinish={handleSubmit}>
BirdNETMb0f71532025-05-26 17:37:33 +0800280 <Form.Item
281 name="content"
BirdNETMed9f2f92025-05-26 20:12:30 +0800282 rules={[
283 { required: true, message: '请输入评论内容' },
284 { whitespace: true, message: '评论内容不能为空' },
285 { max: 500, message: '评论内容不能超过500个字符' }
286 ]}
BirdNETMb0f71532025-05-26 17:37:33 +0800287 style={{ marginBottom: '12px' }}
288 >
BirdNETMed9f2f92025-05-26 20:12:30 +0800289 <TextArea
290 rows={3}
291 placeholder="请输入您的评论(Ctrl+Enter 快速提交)"
292 maxLength={500}
293 showCount
294 onKeyDown={handleKeyDown}
295 disabled={submitting}
296 />
BirdNETMb0f71532025-05-26 17:37:33 +0800297 </Form.Item>
298 <Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
299 <Button
300 htmlType="submit"
301 loading={submitting}
BirdNETMb0f71532025-05-26 17:37:33 +0800302 type="primary"
BirdNETMed9f2f92025-05-26 20:12:30 +0800303 disabled={loading}
BirdNETMb0f71532025-05-26 17:37:33 +0800304 >
305 提交评论
306 </Button>
307 </Form.Item>
308 </Form>
309 </div>
310 </Content>
311 );
312};
313
314export default TorrentComments;