diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 7d3b1ce..8e74515 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -25,8 +25,8 @@ jobs: # 2. 停止并移除 Docker 容器及镜像 echo -e "\n===== 停止并清理 Docker =====" - # 合并停止容器和删除镜像的操作,避免重复执行导致"already in progress"错误 - echo $SUDO_PASSWORD | sudo -S docker compose down --rmi all + # 移除 --rmi all,保留镜像缓存,加快构建速度,同时避免误删基础镜像 + echo $SUDO_PASSWORD | sudo -S docker compose down # 3. 拉取 Git 最新代码 echo -e "\n===== 拉取 Git 代码 =====" @@ -42,6 +42,6 @@ jobs: # 4. 重新启动 Docker 容器 echo -e "\n===== 启动 Docker 容器 =====" - echo $SUDO_PASSWORD | sudo -S docker compose up -d + echo $SUDO_PASSWORD | sudo -S docker compose up -d --build echo -e "\n===== 操作完成!=====" diff --git a/backend/Dockerfile b/backend/Dockerfile index dfdd83c..5911a06 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -18,5 +18,8 @@ COPY . /app/ # Expose port EXPOSE 8000 +# Volume for media files +VOLUME ["/app/media"] + # Run the application with gunicorn CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"] diff --git a/docker-compose.yml b/docker-compose.yml index 6f42fec..73bdca8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,7 @@ services: command: sh -c "python manage.py collectstatic --noinput && python manage.py migrate && gunicorn --bind 0.0.0.0:8000 --access-logfile - --error-logfile - config.wsgi:application" volumes: - ./backend:/app + - ./backend/media:/app/media ports: - "8000:8000" environment: diff --git a/frontend/src/components/competition/CompetitionDetail.jsx b/frontend/src/components/competition/CompetitionDetail.jsx index 8721372..9368e75 100644 --- a/frontend/src/components/competition/CompetitionDetail.jsx +++ b/frontend/src/components/competition/CompetitionDetail.jsx @@ -1,7 +1,7 @@ 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 } from 'antd'; +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'; @@ -14,6 +14,12 @@ 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; @@ -24,7 +30,11 @@ const getImageUrl = (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 || ''); @@ -76,6 +86,11 @@ const CodeBlock = ({ inline, className, children, ...props }) => { ); }; +/** + * 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(); @@ -88,6 +103,9 @@ const CompetitionDetail = () => { 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], @@ -117,6 +135,10 @@ const CompetitionDetail = () => { const myProject = myProjects?.results?.[0]; + /** + * Handle competition enrollment. + * Checks login status and submits enrollment request. + */ const handleEnroll = async () => { if (!user) { showLoginModal(); @@ -131,6 +153,10 @@ const CompetitionDetail = () => { } }; + /** + * Fetch and display judge comments for a project. + * @param {Object} project - The project object + */ const handleViewComments = async (project) => { if (!project) return; setCommentsLoading(true); @@ -157,8 +183,8 @@ const CompetitionDetail = () => { key: 'details', label: '比赛详情', children: ( -
- +
+ {competition.status_display} @@ -172,21 +198,21 @@ const CompetitionDetail = () => { - 比赛简介 -
+ 比赛简介 +
, - h1: (props) =>

, - h2: (props) =>

, - h3: (props) =>

, + h1: (props) =>

, + h2: (props) =>

, + h3: (props) =>

, a: (props) => , blockquote: (props) =>
, - table: (props) => , - th: (props) =>
, + table: (props) => , + th: (props) =>
, td: (props) => , }} > @@ -194,21 +220,21 @@ const CompetitionDetail = () => { - 规则说明 -
+ 规则说明 +
, - h1: (props) =>

, - h2: (props) =>

, - h3: (props) =>

, + h1: (props) =>

, + h2: (props) =>

, + h3: (props) =>

, a: (props) => , blockquote: (props) =>
, - table: (props) => , - th: (props) =>
, + table: (props) => , + th: (props) =>
, td: (props) => , }} > @@ -216,21 +242,21 @@ const CompetitionDetail = () => { - 参赛条件 -
+ 参赛条件 +
, - h1: (props) =>

, - h2: (props) =>

, - h3: (props) =>

, + h1: (props) =>

, + h2: (props) =>

, + h3: (props) =>

, a: (props) => , blockquote: (props) =>
, - table: (props) => , - th: (props) =>
, + table: (props) => , + th: (props) => { 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.title}
{project.contestant_info?.nickname}
-
+
{enrollment && project.contestant === enrollment.id ? project.final_score : '**'}
@@ -299,30 +325,31 @@ const CompetitionDetail = () => { ]; return ( -
+
- {competition.title} -
+ {competition.title} +
{enrollment ? ( - + ) : ( - )} @@ -331,6 +358,7 @@ const CompetitionDetail = () => {
, td: (props) => , }} > @@ -244,7 +270,7 @@ const CompetitionDetail = () => { key: 'projects', label: '参赛项目', children: ( - + {projects?.results?.map(project => (