比赛
This commit is contained in:
188
frontend/src/components/competition/ProjectDetail.jsx
Normal file
188
frontend/src/components/competition/ProjectDetail.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user