This commit is contained in:
jeremygan2021
2026-03-10 14:11:43 +08:00
parent b74d0826ee
commit 03297f3d07

View File

@@ -0,0 +1,188 @@
import React, { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { Typography, Card, Button, Row, Col, Tag, Descriptions, Empty, Spin, Avatar, List, Image, Grid } from 'antd';
import { UserOutlined, ArrowLeftOutlined, LinkOutlined, FileTextOutlined, TrophyOutlined, MessageOutlined } from '@ant-design/icons';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import dayjs from 'dayjs';
import { getProjectDetail, getComments } from '../../api';
import 'github-markdown-css/github-markdown-dark.css';
const { Title, Paragraph, Text } = Typography;
const { useBreakpoint } = Grid;
const getImageUrl = (url) => {
if (!url) return '';
if (url.startsWith('http') || url.startsWith('//')) return url;
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
const baseUrl = apiUrl.replace(/\/api\/?$/, '');
return `${baseUrl}${url}`;
};
const CodeBlock = ({ inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={vscDarkPlus}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
};
const ProjectDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const screens = useBreakpoint();
const isMobile = !screens.md;
const { data: project, isLoading } = useQuery({
queryKey: ['project', id],
queryFn: () => getProjectDetail(id).then(res => res.data)
});
const { data: comments } = useQuery({
queryKey: ['comments', id],
queryFn: () => getComments({ project: id }).then(res => res.data?.results || res.data || []),
enabled: !!project
});
if (isLoading) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
if (!project) return <Empty description="项目不存在" />;
return (
<div style={{ maxWidth: 1200, margin: '0 auto', padding: isMobile ? '12px' : '24px' }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate(-1)}
style={{ marginBottom: 16 }}
>
返回
</Button>
<Card bordered={false} style={{ background: '#1f1f1f', borderRadius: 8 }}>
<Row gutter={[24, 24]}>
<Col xs={24} md={8}>
<Image
src={getImageUrl(project.display_cover_image) || 'placeholder.jpg'}
alt={project.title}
style={{ width: '100%', borderRadius: 8, objectFit: 'cover', aspectRatio: '16/9' }}
/>
<Card style={{ marginTop: 24, background: '#141414', border: '1px solid #303030' }}>
<Descriptions title="项目信息" column={1} size="small">
<Descriptions.Item label="参赛者">
<Avatar icon={<UserOutlined />} size="small" style={{ marginRight: 8 }} />
{project.contestant_info?.nickname || '匿名用户'}
</Descriptions.Item>
<Descriptions.Item label="提交时间">
{dayjs(project.created_at).format('YYYY-MM-DD HH:mm')}
</Descriptions.Item>
<Descriptions.Item label="最终得分">
<span style={{ color: '#00b96b', fontSize: 18, fontWeight: 'bold' }}>
{project.final_score ?? '待定'}
</span>
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={project.status === 'submitted' ? 'green' : 'default'}>
{project.status === 'submitted' ? '已提交' : '草稿'}
</Tag>
</Descriptions.Item>
</Descriptions>
</Card>
{project.link && (
<Button
type="primary"
block
icon={<LinkOutlined />}
href={project.link}
target="_blank"
style={{ marginTop: 16 }}
>
访问项目链接
</Button>
)}
{project.file && (
<Button
block
icon={<FileTextOutlined />}
href={getImageUrl(project.file)}
target="_blank"
style={{ marginTop: 16 }}
>
下载项目文件
</Button>
)}
</Col>
<Col xs={24} md={16}>
<Title level={isMobile ? 3 : 2} style={{ color: '#fff', marginTop: 0 }}>{project.title}</Title>
<Text type="secondary" style={{ fontSize: 16 }}>{project.subtitle}</Text>
<div style={{ marginTop: 24 }}>
<Title level={4} style={{ color: '#fff' }}>项目详情</Title>
<div style={{ color: '#ccc', lineHeight: '1.8', fontSize: isMobile ? '14px' : '16px', background: '#141414', padding: 16, borderRadius: 8 }} className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
code: CodeBlock,
img: (props) => <img {...props} src={getImageUrl(props.src)} style={{ maxWidth: '100%', borderRadius: '8px' }} />,
h1: (props) => <h1 {...props} style={{ color: '#fff', borderBottom: '1px solid #333', paddingBottom: '0.3em', fontSize: isMobile ? '1.5em' : '2em' }} />,
h2: (props) => <h2 {...props} style={{ color: '#fff', borderBottom: '1px solid #333', paddingBottom: '0.3em', fontSize: isMobile ? '1.3em' : '1.5em' }} />,
h3: (props) => <h3 {...props} style={{ color: '#eee', fontSize: isMobile ? '1.1em' : '1.25em' }} />,
a: (props) => <a {...props} style={{ color: '#00b96b' }} target="_blank" rel="noopener noreferrer" />,
}}
>
{project.description || '暂无描述'}
</ReactMarkdown>
</div>
</div>
{comments && comments.length > 0 && (
<div style={{ marginTop: 32 }}>
<Title level={4} style={{ color: '#fff' }}>评委评语</Title>
<List
itemLayout="horizontal"
dataSource={comments}
renderItem={item => (
<List.Item style={{ background: '#141414', padding: 16, borderRadius: 8, marginBottom: 12, border: '1px solid #303030' }}>
<List.Item.Meta
avatar={<Avatar icon={<UserOutlined />} style={{ backgroundColor: '#00b96b' }} />}
title={<span style={{ color: '#fff' }}>{item.judge_name || '评委'}</span>}
description={
<div>
<div style={{ color: '#ccc', marginTop: 8 }}>{item.content}</div>
<div style={{ fontSize: 12, color: '#666', marginTop: 8 }}>
{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}
</div>
</div>
}
/>
</List.Item>
)}
/>
</div>
)}
</Col>
</Row>
</Card>
</div>
);
};
export default ProjectDetail;