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