diff --git a/backend/competition/views.py b/backend/competition/views.py index d01cedd..d3cde5f 100644 --- a/backend/competition/views.py +++ b/backend/competition/views.py @@ -1,4 +1,4 @@ -from rest_framework import viewsets, permissions, status, filters +from rest_framework import viewsets, permissions, status, filters, serializers from rest_framework.decorators import action from rest_framework.response import Response from django.db.models import Q @@ -107,6 +107,10 @@ class ProjectViewSet(viewsets.ModelViewSet): if competition_id: queryset = queryset.filter(competition_id=competition_id) + contestant_id = self.request.query_params.get('contestant') + if contestant_id: + queryset = queryset.filter(contestant_id=contestant_id) + # 如果是普通用户,只能看到已提交的项目,或者自己草稿的项目 user = get_current_wechat_user(self.request) if user: @@ -152,6 +156,10 @@ class ProjectViewSet(viewsets.ModelViewSet): except CompetitionEnrollment.DoesNotExist: raise serializers.ValidationError("您没有参赛资格或审核未通过") + # 检查是否已提交过项目 + if Project.objects.filter(competition=competition, contestant=enrollment).exists(): + raise serializers.ValidationError("您已提交过该比赛的项目,请勿重复提交") + serializer.save(contestant=enrollment) @action(detail=True, methods=['post']) diff --git a/frontend/src/components/competition/CompetitionDetail.jsx b/frontend/src/components/competition/CompetitionDetail.jsx index 8f2d251..d1f31d4 100644 --- a/frontend/src/components/competition/CompetitionDetail.jsx +++ b/frontend/src/components/competition/CompetitionDetail.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Typography, Tabs, Button, Row, Col, Card, Statistic, Tag, Descriptions, Empty, message, Spin } from 'antd'; import { CalendarOutlined, TrophyOutlined, UserOutlined, FileTextOutlined, CloudUploadOutlined, CopyOutlined, CheckOutlined } from '@ant-design/icons'; import ReactMarkdown from 'react-markdown'; @@ -79,6 +79,7 @@ const CodeBlock = ({ inline, className, children, ...props }) => { 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); @@ -104,6 +105,15 @@ const CompetitionDetail = () => { 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]; + const handleEnroll = async () => { if (!user) { showLoginModal(); @@ -297,8 +307,15 @@ const CompetitionDetail = () => { )} {isContestant && ( - )} @@ -325,6 +342,8 @@ const CompetitionDetail = () => { setSubmissionModalVisible(false); setEditingProject(null); // Refetch projects + queryClient.invalidateQueries(['projects']); + queryClient.invalidateQueries(['myProject']); }} /> )} diff --git a/frontend/src/components/competition/ProjectSubmission.jsx b/frontend/src/components/competition/ProjectSubmission.jsx index 45062bd..2d366f7 100644 --- a/frontend/src/components/competition/ProjectSubmission.jsx +++ b/frontend/src/components/competition/ProjectSubmission.jsx @@ -12,6 +12,15 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess } const [fileList, setFileList] = useState([]); const queryClient = useQueryClient(); + // Reset form when initialValues changes (important for switching between create/edit) + React.useEffect(() => { + if (initialValues) { + form.setFieldsValue(initialValues); + } else { + form.resetFields(); + } + }, [initialValues, form]); + const createMutation = useMutation({ mutationFn: createProject, onSuccess: () => { @@ -62,6 +71,13 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess } }; const handleUpload = ({ file, onSuccess, onError }) => { + if (!initialValues?.id) { + message.warning('请先保存项目基本信息再上传文件'); + // Prevent default upload + onError(new Error('请先保存项目')); + return; + } + const formData = new FormData(); formData.append('file', file); formData.append('project', initialValues?.id || ''); // Need project ID first usually @@ -70,7 +86,7 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess } // Or upload to temp storage then link? // For simplicity, let's assume we create project first if not exists if (!initialValues?.id) { - message.warning('请先保存项目基本信息再上传文件'); + // Already handled above return; } @@ -79,7 +95,7 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess } return ( + {initialValues?.status === 'submitted' && ( +
+ 注意:您已正式提交该项目,修改后需要重新审核(如适用)。 +
+ )} + {initialValues?.id && ( + myProject ? ( + + ) : ( + enrollment.status === 'approved' ? ( + + ) : ( + + ) + ) ) : (