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 && (
- } onClick={() => setSubmissionModalVisible(true)}>
- 提交/管理作品
+ }
+ loading={loadingMyProject}
+ onClick={() => {
+ setEditingProject(myProject || null);
+ setSubmissionModalVisible(true);
+ }}
+ >
+ {myProject ? '管理/修改作品' : '提交作品'}
)}
@@ -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 && (