diff --git a/backend/competition/judge_views.py b/backend/competition/judge_views.py
index fc248d0..47fbf90 100644
--- a/backend/competition/judge_views.py
+++ b/backend/competition/judge_views.py
@@ -435,6 +435,19 @@ def project_detail_api(request, project_id):
# 判断是否为选手查看自己的项目
is_own_project = role == 'contestant' and project.contestant.user == user
+ # 获取项目文件(PPT、PDF等)
+ project_files = ProjectFile.objects.filter(project=project)
+ files_data = []
+ for f in project_files:
+ file_url = f.file.url if f.file else f.file_url
+ if file_url:
+ files_data.append({
+ 'id': f.id,
+ 'name': f.name,
+ 'file_type': f.file_type,
+ 'file_url': file_url
+ })
+
data = {
'id': project.id,
'title': project.title,
@@ -445,6 +458,7 @@ def project_detail_api(request, project_id):
'current_comment': current_comment,
'ai_result': ai_data,
'audio_url': audio_url,
+ 'files': files_data,
'can_grade': role == 'judge' or (role == 'contestant' and project.contestant.user != user),
'is_own_project': is_own_project,
# 评分细项(评委、嘉宾可见,选手查看自己的项目时也可见)
diff --git a/backend/competition/serializers.py b/backend/competition/serializers.py
index 689884a..e220480 100644
--- a/backend/competition/serializers.py
+++ b/backend/competition/serializers.py
@@ -33,19 +33,44 @@ class CompetitionEnrollmentSerializer(serializers.ModelSerializer):
read_only_fields = ['status']
class ProjectFileSerializer(serializers.ModelSerializer):
+ file_url_display = serializers.SerializerMethodField()
+
class Meta:
model = ProjectFile
- fields = ['id', 'project', 'file_type', 'file', 'file_url', 'name', 'created_at']
+ fields = ['id', 'project', 'file_type', 'file', 'file_url', 'name', 'created_at', 'file_url_display']
+
+ def get_file_url_display(self, obj):
+ if obj.file:
+ return obj.file.url
+ return obj.file_url
def validate_file(self, value):
if not value:
return value
- # 50MB limit
limit_mb = 50
if value.size > limit_mb * 1024 * 1024:
raise serializers.ValidationError(f"文件大小不能超过 {limit_mb}MB")
return value
+ def create(self, validated_data):
+ file_obj = validated_data.get('file')
+ if file_obj:
+ ext = file_obj.name.split('.')[-1].lower() if '.' in file_obj.name else ''
+ if ext in ['ppt', 'pptx']:
+ validated_data['file_type'] = 'ppt'
+ elif ext == 'pdf':
+ validated_data['file_type'] = 'pdf'
+ elif ext in ['jpg', 'jpeg', 'png', 'gif', 'webp']:
+ validated_data['file_type'] = 'image'
+ elif ext in ['mp4', 'mov', 'avi', 'webm']:
+ validated_data['file_type'] = 'video'
+ elif ext in ['doc', 'docx']:
+ validated_data['file_type'] = 'doc'
+
+ if not validated_data.get('name'):
+ validated_data['name'] = file_obj.name
+ return super().create(validated_data)
+
class ProjectSerializer(serializers.ModelSerializer):
files = ProjectFileSerializer(many=True, read_only=True)
contestant_info = serializers.SerializerMethodField()
diff --git a/backend/competition/templates/judge/dashboard.html b/backend/competition/templates/judge/dashboard.html
index d2112fb..efc61b5 100644
--- a/backend/competition/templates/judge/dashboard.html
+++ b/backend/competition/templates/judge/dashboard.html
@@ -116,6 +116,13 @@
+
+
@@ -548,6 +555,39 @@ async function viewProject(id) {
audioSection.classList.add('justify-center');
}
+ // Render Project Files (PPT/PDF)
+ const filesSection = document.getElementById('modalFilesSection');
+ const filesList = document.getElementById('modalFilesList');
+ if (data.files && data.files.length > 0) {
+ filesSection.style.display = 'block';
+ filesList.innerHTML = data.files.map(file => {
+ let iconClass = 'fas fa-file';
+ let iconColor = 'text-gray-500';
+ if (file.file_type === 'ppt' || file.file_type === 'pptx') {
+ iconClass = 'fas fa-file-powerpoint';
+ iconColor = 'text-orange-500';
+ } else if (file.file_type === 'pdf') {
+ iconClass = 'fas fa-file-pdf';
+ iconColor = 'text-red-500';
+ } else if (file.file_type === 'video') {
+ iconClass = 'fas fa-video';
+ iconColor = 'text-purple-500';
+ } else if (file.file_type === 'image') {
+ iconClass = 'fas fa-image';
+ iconColor = 'text-green-500';
+ }
+ return `
+
+
+ ${file.name || '未命名文件'}
+
+
+ `;
+ }).join('');
+ } else {
+ filesSection.style.display = 'none';
+ }
+
// AI Result
const aiSection = document.getElementById('aiResultSection');
if (data.ai_result) {
diff --git a/backend/competition/views.py b/backend/competition/views.py
index 86096bf..bf8ed7c 100644
--- a/backend/competition/views.py
+++ b/backend/competition/views.py
@@ -10,6 +10,7 @@ from .serializers import (
ProjectSerializer, ProjectFileSerializer,
ScoreSerializer, CommentSerializer, ScoreDimensionSerializer
)
+import uuid
from rest_framework.pagination import PageNumberPagination
@@ -200,13 +201,26 @@ class ProjectFileViewSet(viewsets.ModelViewSet):
return ProjectFile.objects.all()
def perform_create(self, serializer):
- # 简单权限控制:只有项目拥有者可以上传
project = serializer.validated_data['project']
user = get_current_wechat_user(self.request)
if not user or project.contestant.user != user:
raise serializers.ValidationError("无权上传文件")
-
+
+ file_obj = serializer.validated_data.get('file')
+ if file_obj:
+ try:
+ from ai_services.services import AliyunTingwuService
+ service = AliyunTingwuService()
+ if service.bucket:
+ ext = file_obj.name.split('.')[-1] if '.' in file_obj.name else ''
+ file_name = f"competitions/projects/{project.id}/{uuid.uuid4()}.{ext}"
+ oss_url = service.upload_to_oss(file_obj, file_name, day=30)
+ serializer.save(file_url=oss_url, file=None)
+ return
+ except Exception as e:
+ print(f"OSS upload failed, using local storage: {e}")
+
serializer.save()
diff --git a/frontend/src/components/competition/ProjectSubmission.jsx b/frontend/src/components/competition/ProjectSubmission.jsx
index f641ea7..101bd76 100644
--- a/frontend/src/components/competition/ProjectSubmission.jsx
+++ b/frontend/src/components/competition/ProjectSubmission.jsx
@@ -146,11 +146,43 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
{/* File Upload Section - Only visible if project exists */}
{initialValues?.id && (
-
+
{
+ const allowedTypes = [
+ 'application/vnd.ms-powerpoint',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'application/pdf',
+ 'video/mp4',
+ 'video/quicktime',
+ 'video/x-msvideo',
+ 'video/webm',
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif',
+ 'image/webp',
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+ ];
+ const allowedExtensions = ['ppt', 'pptx', 'pdf', 'mp4', 'mov', 'avi', 'webm', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'doc', 'docx'];
+ const fileExt = file.name.split('.').pop()?.toLowerCase();
+
+ if (!allowedExtensions.includes(fileExt)) {
+ message.error('不支持的文件格式!请上传 PPT、PDF、视频或图片文件');
+ return Upload.LIST_IGNORE;
+ }
+
+ const isLt50M = file.size / 1024 / 1024 < 50;
+ if (!isLt50M) {
+ message.error('文件大小不能超过 50MB');
+ return Upload.LIST_IGNORE;
+ }
+ return false;
+ }}
>
}>上传文件 (最大50MB)