blob: 79f976a56b978e55143c7f88de44833056eed505 [file] [log] [blame] [edit]
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>
);