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