diff --git a/frontend/src/components/competition/ProjectDetail.jsx b/frontend/src/components/competition/ProjectDetail.jsx new file mode 100644 index 0000000..0674322 --- /dev/null +++ b/frontend/src/components/competition/ProjectDetail.jsx @@ -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 ? ( + + {String(children).replace(/\n$/, '')} + + ) : ( + + {children} + + ); +}; + +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 ; + if (!project) return ; + + return ( +
+ + + + + + {project.title} + + + + + } size="small" style={{ marginRight: 8 }} /> + {project.contestant_info?.nickname || '匿名用户'} + + + {dayjs(project.created_at).format('YYYY-MM-DD HH:mm')} + + + + {project.final_score ?? '待定'} + + + + + {project.status === 'submitted' ? '已提交' : '草稿'} + + + + + + {project.link && ( + + )} + {project.file && ( + + )} + + + + + {project.title} + {project.subtitle} + +
+ 项目详情 +
+ , + h1: (props) =>

, + h2: (props) =>

, + h3: (props) =>

, + a: (props) => , + }} + > + {project.description || '暂无描述'} + +

+
+ + {comments && comments.length > 0 && ( +
+ 评委评语 + ( + + } style={{ backgroundColor: '#00b96b' }} />} + title={{item.judge_name || '评委'}} + description={ +
+
{item.content}
+
+ {dayjs(item.created_at).format('YYYY-MM-DD HH:mm')} +
+
+ } + /> +
+ )} + /> +
+ )} + + +
+
+
+ ); +}; + +export default ProjectDetail;