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: ( -