blob: 79f976a56b978e55143c7f88de44833056eed505 [file] [log] [blame]
223010144ce05872025-06-08 22:33:28 +08001import React, { useState } from 'react';
2import { Card, Typography, Tag, Flex, Table, Collapse, List, Spin, Alert, Button, Input, Form, message } from 'antd';
22301014356527a2025-06-09 17:46:56 +08003import { SendOutlined } from '@ant-design/icons';
223010144ce05872025-06-08 22:33:28 +08004import ReactMarkdown from 'react-markdown';
5import type { ColumnsType } from 'antd/es/table';
6import type { PaginationConfig } from 'antd/es/pagination';
7import type { FormInstance } from 'antd/es/form';
8import type { Comment, Version, User, HistoryUser } from './types';
9import { parseUploadSize } from './types';
10
11const { Title, Paragraph } = Typography;
12const { Panel } = Collapse;
13const { TextArea } = Input;
14
15// 作品封面组件
16export const ArtworkCover: React.FC<{ coverUrl: string }> = ({ coverUrl }) => (
17 <Card cover={<img alt="作品封面" src={coverUrl} style={{ height: 250, objectFit: 'cover' }} />} />
18);
19
20// 当前做种用户组件
21export const CurrentSeedingUsers: React.FC<{ users: User[] }> = ({ users }) => (
22 <Card>
23 <Title level={4} style={{ marginBottom: 12 }}>当前做种用户</Title>
24 <Flex wrap="wrap" gap={8}>
25 {users.map((user) => (
26 <Tag key={user.userId} color="green">{user.username}</Tag>
27 ))}
28 </Flex>
29 </Card>
30);
31
32// 历史做种用户组件
33export const HistorySeedingUsers: React.FC<{ users: HistoryUser[] }> = ({ users }) => {
34 const sortedUsers = [...users].sort((a, b) => parseUploadSize(b.uploadTotal) - parseUploadSize(a.uploadTotal));
35
36 const columns: ColumnsType<HistoryUser> = [
37 { title: '用户名', dataIndex: 'username', key: 'username' },
38 {
39 title: '上传总量',
40 dataIndex: 'uploadTotal',
41 key: 'uploadTotal',
42 sorter: (a: HistoryUser, b: HistoryUser) => parseUploadSize(a.uploadTotal) - parseUploadSize(b.uploadTotal),
43 },
44 ];
45
46 return (
47 <Card>
48 <Title level={4} style={{ marginBottom: 12 }}>历史做种用户</Title>
49 <Table columns={columns} dataSource={sortedUsers} rowKey="username" pagination={false} size="small" />
50 </Card>
51 );
52};
53
54// 作品描述组件
55export const ArtworkDescription: React.FC<{
56 name: string;
57 author: string;
58 category: string;
59 description: string;
60 isAuthor?: boolean;
61 onEdit?: () => void;
22301014356527a2025-06-09 17:46:56 +080062}> = ({ name, author, category, description }) => (
223010144ce05872025-06-08 22:33:28 +080063 <Card style={{ marginBottom: 20 }}>
64 <Flex justify="space-between" align="flex-start">
65 <div style={{ flex: 1 }}>
66 <Title level={2} style={{ marginBottom: 8 }}>{name}</Title>
67 <Paragraph style={{ marginBottom: 8, fontSize: 16 }}>
68 <strong>作者:</strong>{author}
69 </Paragraph>
70 <div style={{ marginBottom: 16 }}>
71 <Tag color="blue">{category}</Tag>
72 </div>
73 <div style={{ lineHeight: 1.6 }}>
74 <ReactMarkdown>{description}</ReactMarkdown>
75 </div>
76 </div>
223010144ce05872025-06-08 22:33:28 +080077 </Flex>
78 </Card>
79);
80
81// 版本历史组件
82export const VersionHistory: React.FC<{ versions: Version[] }> = ({ versions }) => (
83 <Card title="版本历史" style={{ marginBottom: 20 }}>
84 <Collapse>
85 {versions.map((version, index) => (
86 <Panel
87 header={`版本 ${version.version}`}
88 key={`version-${index}`}
89 extra={<Tag color="blue">v{version.version}</Tag>}
90 >
91 <div style={{ marginBottom: 16 }}>
92 <strong>版本描述:</strong>
93 <div style={{ marginTop: 8, lineHeight: 1.6 }}>
94 <ReactMarkdown>{version.versionDescription}</ReactMarkdown>
95 </div>
96 </div>
97 <div>
98 <strong>种子文件:</strong>
99 <a href={version.seedFile} target="_blank" rel="noopener noreferrer" style={{ marginLeft: 8 }}>
100 下载链接
101 </a>
102 </div>
103 </Panel>
104 ))}
105 </Collapse>
106 </Card>
107);
108
109// 评论项组件(递归)
110export const CommentItem: React.FC<{
111 comment: Comment;
112 level?: number;
113 onReply?: (parentId: string, parentAuthor: string) => void;
114}> = ({ comment, level = 0, onReply }) => (
115 <div style={{ marginLeft: level * 20 }}>
116 <div style={{ marginBottom: 8 }}>
117 <Paragraph style={{ marginBottom: 4 }}>
118 <strong>{comment.author}:</strong>{comment.content}
119 </Paragraph>
120 <div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}>
121 {comment.createdAt && <span style={{ marginRight: 16 }}>{comment.createdAt}</span>}
122 {onReply && (
123 <Button
124 type="link"
125 size="small"
126 style={{ padding: 0, height: 'auto', fontSize: 12 }}
127 onClick={() => onReply(comment.id || comment.author, comment.author)}
128 >
129 回复
130 </Button>
131 )}
132 </div>
133 </div>
134 {comment.child && comment.child.length > 0 && (
135 <div style={{
136 borderLeft: level === 0 ? '2px solid #f0f0f0' : 'none',
137 paddingLeft: level === 0 ? 12 : 0
138 }}>
139 {comment.child.map((child, index) => (
140 <CommentItem
141 key={child.id || `child-${level}-${index}`}
142 comment={child}
143 level={level + 1}
144 onReply={onReply}
145 />
146 ))}
147 </div>
148 )}
149 </div>
150);
151
152// 发表评论组件
153export const CommentForm: React.FC<{
154 loading?: boolean;
155 onSubmit: (content: string, parentId?: string) => void;
156 replyTo?: { id: string; author: string } | null;
157 onCancelReply?: () => void;
158}> = ({ loading = false, onSubmit, replyTo, onCancelReply }) => {
159 const [form]: [FormInstance] = Form.useForm();
160 const [content, setContent] = useState('');
161
162 const handleSubmit = (): void => {
163 if (!content.trim()) {
164 message.warning('请输入评论内容');
165 return;
166 }
167 onSubmit(content.trim(), replyTo?.id);
168 setContent('');
169 form.resetFields();
170 };
171
172 const placeholder = replyTo ? `回复 @${replyTo.author}:` : "发表你的看法...";
173
174 return (
175 <Card
176 size="small"
177 style={{ marginBottom: 16 }}
178 title={replyTo ? (
179 <div style={{ fontSize: 14 }}>
180 <span>回复 @{replyTo.author}</span>
181 <Button type="link" size="small" onClick={onCancelReply} style={{ padding: '0 0 0 8px', fontSize: 12 }}>
182 取消
183 </Button>
184 </div>
185 ) : undefined}
186 >
187 <Form form={form} layout="vertical">
188 <Form.Item>
189 <TextArea
190 value={content}
191 onChange={(e) => setContent(e.target.value)}
192 placeholder={placeholder}
193 rows={3}
194 maxLength={500}
195 showCount
196 />
197 </Form.Item>
198 <Form.Item style={{ marginBottom: 0 }}>
199 <Flex justify="flex-end" gap={8}>
200 {replyTo && <Button onClick={onCancelReply}>取消</Button>}
201 <Button
202 type="primary"
203 icon={<SendOutlined />}
204 loading={loading}
205 onClick={handleSubmit}
206 disabled={!content.trim()}
207 >
208 {replyTo ? '发表回复' : '发表评论'}
209 </Button>
210 </Flex>
211 </Form.Item>
212 </Form>
213 </Card>
214 );
215};
216
217// 评论区组件
218export const CommentSection: React.FC<{
219 comments: Comment[];
220 total: number;
221 loading?: boolean;
222 error?: string | null;
223 addCommentLoading?: boolean;
224 onPageChange: (page: number, pageSize: number) => void;
225 onAddComment: (content: string, parentId?: string) => void;
226 currentPage: number;
227 pageSize: number;
228}> = ({ comments, total, loading, error, addCommentLoading, onPageChange, onAddComment, currentPage, pageSize }) => {
229 const [replyTo, setReplyTo] = useState<{ id: string; author: string } | null>(null);
230
231 const handleReply = (parentId: string, parentAuthor: string): void => {
232 setReplyTo({ id: parentId, author: parentAuthor });
233 };
234
235 const handleCancelReply = (): void => {
236 setReplyTo(null);
237 };
238
239 const handleSubmitComment = (content: string, parentId?: string): void => {
240 onAddComment(content, parentId);
241 setReplyTo(null);
242 };
243
244 const paginationConfig: PaginationConfig = {
245 current: currentPage,
246 pageSize,
247 total,
248 showSizeChanger: true,
249 showQuickJumper: true,
250 showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条评论`,
251 pageSizeOptions: ['5', '10', '20'],
252 onChange: onPageChange,
253 onShowSizeChange: onPageChange,
254 };
255
256 return (
257 <Card title={`评论 (${total})`} style={{ marginBottom: 20 }}>
258 <CommentForm
259 loading={addCommentLoading}
260 onSubmit={handleSubmitComment}
261 replyTo={replyTo}
262 onCancelReply={handleCancelReply}
263 />
264
265 {error ? (
266 <Alert message="加载评论失败" description={error} type="error" showIcon />
267 ) : loading ? (
268 <Flex justify="center" align="center" style={{ minHeight: 200 }}>
269 <Spin size="large" />
270 </Flex>
271 ) : comments.length > 0 ? (
272 <List
273 dataSource={comments}
274 pagination={paginationConfig}
275 renderItem={(comment, index) => (
276 <List.Item
277 key={comment.id || `comment-${index}`}
278 style={{ border: 'none', padding: '16px 0', borderBottom: '1px solid #f0f0f0' }}
279 >
280 <CommentItem comment={comment} onReply={handleReply} />
281 </List.Item>
282 )}
283 />
284 ) : (
285 <Paragraph style={{ textAlign: 'center', color: '#999', margin: '20px 0' }}>
286 暂无评论,来发表第一条评论吧!
287 </Paragraph>
288 )}
289 </Card>
290 );
291};
292
293// 侧边栏组合组件
294export const Sidebar: React.FC<{
295 coverUrl: string;
296 currentUsers: User[];
297 historyUsers: HistoryUser[];
298 loading?: boolean;
299 error?: string | null;
300}> = ({ coverUrl, currentUsers, historyUsers, loading, error }) => (
301 <Flex flex={1} vertical gap={20}>
302 <ArtworkCover coverUrl={coverUrl} />
303 {loading ? (
304 <Flex justify="center" align="center" style={{ minHeight: 200 }}>
305 <Spin size="large" />
306 </Flex>
307 ) : error ? (
308 <Alert message="加载用户信息失败" description={error} type="error" showIcon />
309 ) : (
310 <>
311 <CurrentSeedingUsers users={currentUsers} />
312 <HistorySeedingUsers users={historyUsers} />
313 </>
314 )}
315 </Flex>
316);
317
318// 主内容区组合组件
319export const MainContent: React.FC<{
320 artworkName: string;
321 author: string;
322 category: string;
323 description: string;
324 versions: Version[];
325 comments: Comment[];
326 commentsTotal: number;
327 commentsLoading?: boolean;
328 commentsError?: string | null;
329 addCommentLoading?: boolean;
330 onCommentsPageChange: (page: number, pageSize: number) => void;
331 onAddComment: (content: string, parentId?: string) => void;
332 currentPage: number;
333 pageSize: number;
334 isAuthor?: boolean;
335 onEditArtwork?: () => void;
336}> = ({
337 artworkName, author, category, description, versions, comments, commentsTotal,
338 commentsLoading, commentsError, addCommentLoading, onCommentsPageChange, onAddComment,
339 currentPage, pageSize, isAuthor, onEditArtwork
340}) => (
341 <Flex flex={4} vertical>
342 <ArtworkDescription
343 name={artworkName}
344 author={author}
345 category={category}
346 description={description}
347 isAuthor={isAuthor}
348 onEdit={onEditArtwork}
349 />
350 <VersionHistory versions={versions} />
351 <CommentSection
352 comments={comments}
353 total={commentsTotal}
354 loading={commentsLoading}
355 error={commentsError}
356 addCommentLoading={addCommentLoading}
357 onPageChange={onCommentsPageChange}
358 onAddComment={onAddComment}
359 currentPage={currentPage}
360 pageSize={pageSize}
361 />
362 </Flex>
363 );