| import React, { useState } from 'react'; |
| import { Card, Typography, Tag, Flex, Table, Collapse, List, Spin, Alert, Button, Input, Form, message } from 'antd'; |
| import { SendOutlined } from '@ant-design/icons'; |
| import ReactMarkdown from 'react-markdown'; |
| import type { ColumnsType } from 'antd/es/table'; |
| import type { PaginationConfig } from 'antd/es/pagination'; |
| import type { FormInstance } from 'antd/es/form'; |
| import type { Comment, Version, User, HistoryUser } from './types'; |
| import { parseUploadSize } from './types'; |
| |
| const { Title, Paragraph } = Typography; |
| const { Panel } = Collapse; |
| const { TextArea } = Input; |
| |
| // 作品封面组件 |
| export const ArtworkCover: React.FC<{ coverUrl: string }> = ({ coverUrl }) => ( |
| <Card cover={<img alt="作品封面" src={coverUrl} style={{ height: 250, objectFit: 'cover' }} />} /> |
| ); |
| |
| // 当前做种用户组件 |
| export const CurrentSeedingUsers: React.FC<{ users: User[] }> = ({ users }) => ( |
| <Card> |
| <Title level={4} style={{ marginBottom: 12 }}>当前做种用户</Title> |
| <Flex wrap="wrap" gap={8}> |
| {users.map((user) => ( |
| <Tag key={user.userId} color="green">{user.username}</Tag> |
| ))} |
| </Flex> |
| </Card> |
| ); |
| |
| // 历史做种用户组件 |
| export const HistorySeedingUsers: React.FC<{ users: HistoryUser[] }> = ({ users }) => { |
| const sortedUsers = [...users].sort((a, b) => parseUploadSize(b.uploadTotal) - parseUploadSize(a.uploadTotal)); |
| |
| const columns: ColumnsType<HistoryUser> = [ |
| { title: '用户名', dataIndex: 'username', key: 'username' }, |
| { |
| title: '上传总量', |
| dataIndex: 'uploadTotal', |
| key: 'uploadTotal', |
| sorter: (a: HistoryUser, b: HistoryUser) => parseUploadSize(a.uploadTotal) - parseUploadSize(b.uploadTotal), |
| }, |
| ]; |
| |
| return ( |
| <Card> |
| <Title level={4} style={{ marginBottom: 12 }}>历史做种用户</Title> |
| <Table columns={columns} dataSource={sortedUsers} rowKey="username" pagination={false} size="small" /> |
| </Card> |
| ); |
| }; |
| |
| // 作品描述组件 |
| export const ArtworkDescription: React.FC<{ |
| name: string; |
| author: string; |
| category: string; |
| description: string; |
| isAuthor?: boolean; |
| onEdit?: () => void; |
| }> = ({ name, author, category, description }) => ( |
| <Card style={{ marginBottom: 20 }}> |
| <Flex justify="space-between" align="flex-start"> |
| <div style={{ flex: 1 }}> |
| <Title level={2} style={{ marginBottom: 8 }}>{name}</Title> |
| <Paragraph style={{ marginBottom: 8, fontSize: 16 }}> |
| <strong>作者:</strong>{author} |
| </Paragraph> |
| <div style={{ marginBottom: 16 }}> |
| <Tag color="blue">{category}</Tag> |
| </div> |
| <div style={{ lineHeight: 1.6 }}> |
| <ReactMarkdown>{description}</ReactMarkdown> |
| </div> |
| </div> |
| </Flex> |
| </Card> |
| ); |
| |
| // 版本历史组件 |
| export const VersionHistory: React.FC<{ versions: Version[] }> = ({ versions }) => ( |
| <Card title="版本历史" style={{ marginBottom: 20 }}> |
| <Collapse> |
| {versions.map((version, index) => ( |
| <Panel |
| header={`版本 ${version.version}`} |
| key={`version-${index}`} |
| extra={<Tag color="blue">v{version.version}</Tag>} |
| > |
| <div style={{ marginBottom: 16 }}> |
| <strong>版本描述:</strong> |
| <div style={{ marginTop: 8, lineHeight: 1.6 }}> |
| <ReactMarkdown>{version.versionDescription}</ReactMarkdown> |
| </div> |
| </div> |
| <div> |
| <strong>种子文件:</strong> |
| <a href={version.seedFile} target="_blank" rel="noopener noreferrer" style={{ marginLeft: 8 }}> |
| 下载链接 |
| </a> |
| </div> |
| </Panel> |
| ))} |
| </Collapse> |
| </Card> |
| ); |
| |
| // 评论项组件(递归) |
| export const CommentItem: React.FC<{ |
| comment: Comment; |
| level?: number; |
| onReply?: (parentId: string, parentAuthor: string) => void; |
| }> = ({ comment, level = 0, onReply }) => ( |
| <div style={{ marginLeft: level * 20 }}> |
| <div style={{ marginBottom: 8 }}> |
| <Paragraph style={{ marginBottom: 4 }}> |
| <strong>{comment.author}:</strong>{comment.content} |
| </Paragraph> |
| <div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}> |
| {comment.createdAt && <span style={{ marginRight: 16 }}>{comment.createdAt}</span>} |
| {onReply && ( |
| <Button |
| type="link" |
| size="small" |
| style={{ padding: 0, height: 'auto', fontSize: 12 }} |
| onClick={() => onReply(comment.id || comment.author, comment.author)} |
| > |
| 回复 |
| </Button> |
| )} |
| </div> |
| </div> |
| {comment.child && comment.child.length > 0 && ( |
| <div style={{ |
| borderLeft: level === 0 ? '2px solid #f0f0f0' : 'none', |
| paddingLeft: level === 0 ? 12 : 0 |
| }}> |
| {comment.child.map((child, index) => ( |
| <CommentItem |
| key={child.id || `child-${level}-${index}`} |
| comment={child} |
| level={level + 1} |
| onReply={onReply} |
| /> |
| ))} |
| </div> |
| )} |
| </div> |
| ); |
| |
| // 发表评论组件 |
| export const CommentForm: React.FC<{ |
| loading?: boolean; |
| onSubmit: (content: string, parentId?: string) => void; |
| replyTo?: { id: string; author: string } | null; |
| onCancelReply?: () => void; |
| }> = ({ loading = false, onSubmit, replyTo, onCancelReply }) => { |
| const [form]: [FormInstance] = Form.useForm(); |
| const [content, setContent] = useState(''); |
| |
| const handleSubmit = (): void => { |
| if (!content.trim()) { |
| message.warning('请输入评论内容'); |
| return; |
| } |
| onSubmit(content.trim(), replyTo?.id); |
| setContent(''); |
| form.resetFields(); |
| }; |
| |
| const placeholder = replyTo ? `回复 @${replyTo.author}:` : "发表你的看法..."; |
| |
| return ( |
| <Card |
| size="small" |
| style={{ marginBottom: 16 }} |
| title={replyTo ? ( |
| <div style={{ fontSize: 14 }}> |
| <span>回复 @{replyTo.author}</span> |
| <Button type="link" size="small" onClick={onCancelReply} style={{ padding: '0 0 0 8px', fontSize: 12 }}> |
| 取消 |
| </Button> |
| </div> |
| ) : undefined} |
| > |
| <Form form={form} layout="vertical"> |
| <Form.Item> |
| <TextArea |
| value={content} |
| onChange={(e) => setContent(e.target.value)} |
| placeholder={placeholder} |
| rows={3} |
| maxLength={500} |
| showCount |
| /> |
| </Form.Item> |
| <Form.Item style={{ marginBottom: 0 }}> |
| <Flex justify="flex-end" gap={8}> |
| {replyTo && <Button onClick={onCancelReply}>取消</Button>} |
| <Button |
| type="primary" |
| icon={<SendOutlined />} |
| loading={loading} |
| onClick={handleSubmit} |
| disabled={!content.trim()} |
| > |
| {replyTo ? '发表回复' : '发表评论'} |
| </Button> |
| </Flex> |
| </Form.Item> |
| </Form> |
| </Card> |
| ); |
| }; |
| |
| // 评论区组件 |
| export const CommentSection: React.FC<{ |
| comments: Comment[]; |
| total: number; |
| loading?: boolean; |
| error?: string | null; |
| addCommentLoading?: boolean; |
| onPageChange: (page: number, pageSize: number) => void; |
| onAddComment: (content: string, parentId?: string) => void; |
| currentPage: number; |
| pageSize: number; |
| }> = ({ comments, total, loading, error, addCommentLoading, onPageChange, onAddComment, currentPage, pageSize }) => { |
| const [replyTo, setReplyTo] = useState<{ id: string; author: string } | null>(null); |
| |
| const handleReply = (parentId: string, parentAuthor: string): void => { |
| setReplyTo({ id: parentId, author: parentAuthor }); |
| }; |
| |
| const handleCancelReply = (): void => { |
| setReplyTo(null); |
| }; |
| |
| const handleSubmitComment = (content: string, parentId?: string): void => { |
| onAddComment(content, parentId); |
| setReplyTo(null); |
| }; |
| |
| const paginationConfig: PaginationConfig = { |
| current: currentPage, |
| pageSize, |
| total, |
| showSizeChanger: true, |
| showQuickJumper: true, |
| showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条评论`, |
| pageSizeOptions: ['5', '10', '20'], |
| onChange: onPageChange, |
| onShowSizeChange: onPageChange, |
| }; |
| |
| return ( |
| <Card title={`评论 (${total})`} style={{ marginBottom: 20 }}> |
| <CommentForm |
| loading={addCommentLoading} |
| onSubmit={handleSubmitComment} |
| replyTo={replyTo} |
| onCancelReply={handleCancelReply} |
| /> |
| |
| {error ? ( |
| <Alert message="加载评论失败" description={error} type="error" showIcon /> |
| ) : loading ? ( |
| <Flex justify="center" align="center" style={{ minHeight: 200 }}> |
| <Spin size="large" /> |
| </Flex> |
| ) : comments.length > 0 ? ( |
| <List |
| dataSource={comments} |
| pagination={paginationConfig} |
| renderItem={(comment, index) => ( |
| <List.Item |
| key={comment.id || `comment-${index}`} |
| style={{ border: 'none', padding: '16px 0', borderBottom: '1px solid #f0f0f0' }} |
| > |
| <CommentItem comment={comment} onReply={handleReply} /> |
| </List.Item> |
| )} |
| /> |
| ) : ( |
| <Paragraph style={{ textAlign: 'center', color: '#999', margin: '20px 0' }}> |
| 暂无评论,来发表第一条评论吧! |
| </Paragraph> |
| )} |
| </Card> |
| ); |
| }; |
| |
| // 侧边栏组合组件 |
| export const Sidebar: React.FC<{ |
| coverUrl: string; |
| currentUsers: User[]; |
| historyUsers: HistoryUser[]; |
| loading?: boolean; |
| error?: string | null; |
| }> = ({ coverUrl, currentUsers, historyUsers, loading, error }) => ( |
| <Flex flex={1} vertical gap={20}> |
| <ArtworkCover coverUrl={coverUrl} /> |
| {loading ? ( |
| <Flex justify="center" align="center" style={{ minHeight: 200 }}> |
| <Spin size="large" /> |
| </Flex> |
| ) : error ? ( |
| <Alert message="加载用户信息失败" description={error} type="error" showIcon /> |
| ) : ( |
| <> |
| <CurrentSeedingUsers users={currentUsers} /> |
| <HistorySeedingUsers users={historyUsers} /> |
| </> |
| )} |
| </Flex> |
| ); |
| |
| // 主内容区组合组件 |
| export const MainContent: React.FC<{ |
| artworkName: string; |
| author: string; |
| category: string; |
| description: string; |
| versions: Version[]; |
| comments: Comment[]; |
| commentsTotal: number; |
| commentsLoading?: boolean; |
| commentsError?: string | null; |
| addCommentLoading?: boolean; |
| onCommentsPageChange: (page: number, pageSize: number) => void; |
| onAddComment: (content: string, parentId?: string) => void; |
| currentPage: number; |
| pageSize: number; |
| isAuthor?: boolean; |
| onEditArtwork?: () => void; |
| }> = ({ |
| artworkName, author, category, description, versions, comments, commentsTotal, |
| commentsLoading, commentsError, addCommentLoading, onCommentsPageChange, onAddComment, |
| currentPage, pageSize, isAuthor, onEditArtwork |
| }) => ( |
| <Flex flex={4} vertical> |
| <ArtworkDescription |
| name={artworkName} |
| author={author} |
| category={category} |
| description={description} |
| isAuthor={isAuthor} |
| onEdit={onEditArtwork} |
| /> |
| <VersionHistory versions={versions} /> |
| <CommentSection |
| comments={comments} |
| total={commentsTotal} |
| loading={commentsLoading} |
| error={commentsError} |
| addCommentLoading={addCommentLoading} |
| onPageChange={onCommentsPageChange} |
| onAddComment={onAddComment} |
| currentPage={currentPage} |
| pageSize={pageSize} |
| /> |
| </Flex> |
| ); |