import React, { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Typography, Tabs, Button, Row, Col, Card, Statistic, Tag, Descriptions, Empty, message, Spin, Modal, List, Avatar, Grid } from 'antd'; import { CalendarOutlined, TrophyOutlined, UserOutlined, FileTextOutlined, CloudUploadOutlined, CopyOutlined, CheckOutlined, 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 { getCompetitionDetail, getProjects, getMyCompetitionEnrollment, enrollCompetition, getComments } from '../../api'; import ProjectSubmission from './ProjectSubmission'; import { useAuth } from '../../context/AuthContext'; import 'github-markdown-css/github-markdown-dark.css'; /** * Get the full URL for an image. * Handles relative paths and ensures correct API base URL is used. * @param {string} url - The image URL path * @returns {string} The full absolute URL */ 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'; // Remove /api suffix if present to get the root URL for media files const baseUrl = apiUrl.replace(/\/api\/?$/, ''); return `${baseUrl}${url}`; }; const { Title, Paragraph } = Typography; const { useBreakpoint } = Grid; /** * Code block component for markdown rendering with syntax highlighting and copy functionality. */ const CodeBlock = ({ inline, className, children, ...props }) => { const [copied, setCopied] = useState(false); const match = /language-(\w+)/.exec(className || ''); const codeString = String(children).replace(/\n$/, ''); const handleCopy = () => { navigator.clipboard.writeText(codeString); setCopied(true); message.success('代码已复制'); setTimeout(() => setCopied(false), 2000); }; return !inline && match ? (
{copied ? : } {copied ? '已复制' : '复制'}
{codeString}
) : ( {children} ); }; /** * Main component for displaying competition details. * Includes tabs for overview, projects, and leaderboard. * Responsive design for mobile and desktop. */ const CompetitionDetail = () => { const { id } = useParams(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { user, showLoginModal } = useAuth(); const [activeTab, setActiveTab] = useState('details'); const [submissionModalVisible, setSubmissionModalVisible] = useState(false); const [editingProject, setEditingProject] = useState(null); const [commentsModalVisible, setCommentsModalVisible] = useState(false); const [currentProjectComments, setCurrentProjectComments] = useState([]); const [commentsLoading, setCommentsLoading] = useState(false); const screens = useBreakpoint(); const isMobile = !screens.md; // Fetch competition details const { data: competition, isLoading: loadingDetail } = useQuery({ queryKey: ['competition', id], queryFn: () => getCompetitionDetail(id).then(res => res.data) }); // Fetch projects (for leaderboard/display) const { data: projects } = useQuery({ queryKey: ['projects', id], queryFn: () => getProjects({ competition: id, status: 'submitted', page_size: 100 }).then(res => res.data) }); // Check enrollment status const { data: enrollment, refetch: refetchEnrollment } = useQuery({ queryKey: ['enrollment', id], queryFn: () => getMyCompetitionEnrollment(id).then(res => res.data), enabled: !!user, retry: false }); // Fetch my project if enrolled const { data: myProjects, isLoading: loadingMyProject } = useQuery({ queryKey: ['myProject', id, enrollment?.id], queryFn: () => getProjects({ competition: id, contestant: enrollment.id }).then(res => res.data), enabled: !!enrollment?.id }); const myProject = myProjects?.results?.[0]; /** * Handle competition enrollment. * Checks login status and submits enrollment request. */ const handleEnroll = async () => { if (!user) { showLoginModal(); return; } try { await enrollCompetition(id, { role: 'contestant' }); message.success('报名申请已提交,请等待审核'); refetchEnrollment(); } catch (error) { message.error(error.response?.data?.detail || '报名失败'); } }; /** * Fetch and display judge comments for a project. * @param {Object} project - The project object */ const handleViewComments = async (project) => { if (!project) return; setCommentsLoading(true); setCommentsModalVisible(true); try { const res = await getComments({ project: project.id }); // Support pagination result or list result setCurrentProjectComments(res.data?.results || res.data || []); } catch (error) { console.error(error); message.error('获取评语失败'); } finally { setCommentsLoading(false); } }; if (loadingDetail) return ; if (!competition) return ; const isContestant = enrollment?.role === 'contestant' && enrollment?.status === 'approved'; const items = [ { key: 'details', label: '比赛详情', children: (
{competition.status_display} {dayjs(competition.start_time).format('YYYY-MM-DD')} {dayjs(competition.end_time).format('YYYY-MM-DD')} 比赛简介
, h1: (props) =>

, h2: (props) =>

, h3: (props) =>

, a: (props) => , blockquote: (props) =>
, table: (props) => , th: (props) =>
, td: (props) => , }} > {competition.description} 规则说明
, h1: (props) =>

, h2: (props) =>

, h3: (props) =>

, a: (props) => , blockquote: (props) =>
, table: (props) => , th: (props) =>
, td: (props) => , }} > {competition.rule_description} 参赛条件
, h1: (props) =>

, h2: (props) =>

, h3: (props) =>

, a: (props) => , blockquote: (props) =>
, table: (props) => , th: (props) => } actions={[ , ]} > } /> ))} {(!projects?.results || projects.results.length === 0) && ( )} ) }, { key: 'leaderboard', label: '排行榜', children: ( {/* Leaderboard Logic: sort by final_score descending */} {[...(projects?.results || [])].sort((a, b) => b.final_score - a.final_score).map((project, index) => (
#{index + 1}
{project.title}
{project.contestant_info?.nickname}
{project.final_score || 0}
))}
) } ]; return (
{competition.title}
{enrollment ? ( ) : ( )} {isContestant && ( <> {myProject && ( )} )}
{submissionModalVisible && ( { setSubmissionModalVisible(false); setEditingProject(null); }} onSuccess={() => { setSubmissionModalVisible(false); setEditingProject(null); // Refetch projects queryClient.invalidateQueries(['projects']); queryClient.invalidateQueries(['myProject']); }} /> )} setCommentsModalVisible(false)} footer={null} > ( } />} title={item.judge_name || '评委'} description={
{item.content}
{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}
} />
)} locale={{ emptyText: '暂无评语' }} />
); }; export default CompetitionDetail;
, td: (props) => , }} > {competition.condition_description} ) }, { key: 'projects', label: '参赛项目', children: ( {projects?.results?.map(project => (