This commit is contained in:
@@ -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,
|
||||
# 评分细项(评委、嘉宾可见,选手查看自己的项目时也可见)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -116,6 +116,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modalFilesSection" style="display: none;">
|
||||
<h4 class="text-lg font-semibold text-gray-900 mb-3 flex items-center"><i class="fas fa-file-powerpoint mr-2 text-orange-500"></i>项目文件</h4>
|
||||
<div id="modalFilesList" class="bg-gray-50 p-4 rounded-lg border border-gray-100 space-y-2">
|
||||
<!-- Files will be injected here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aiResultSection" style="display:none;" class="border border-indigo-100 rounded-xl overflow-hidden">
|
||||
<div class="bg-indigo-50 px-4 py-3 border-b border-indigo-100 flex items-center">
|
||||
<i class="fas fa-robot text-indigo-600 mr-2"></i>
|
||||
@@ -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 `
|
||||
<a href="${file.file_url}" target="_blank" class="flex items-center p-3 bg-white rounded-lg border border-gray-200 hover:bg-gray-50 hover:border-blue-300 transition-colors group">
|
||||
<i class="${iconClass} ${iconColor} text-xl mr-3 group-hover:scale-110 transition-transform"></i>
|
||||
<span class="flex-1 text-sm font-medium text-gray-700 truncate">${file.name || '未命名文件'}</span>
|
||||
<i class="fas fa-external-link-alt text-gray-400 group-hover:text-blue-500"></i>
|
||||
</a>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
filesSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// AI Result
|
||||
const aiSection = document.getElementById('aiResultSection');
|
||||
if (data.ai_result) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -146,11 +146,43 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
|
||||
|
||||
{/* File Upload Section - Only visible if project exists */}
|
||||
{initialValues?.id && (
|
||||
<Form.Item label="项目附件 (PPT/PDF/视频)">
|
||||
<Form.Item label="项目附件 (PPT/PDF/视频/图片)">
|
||||
<Upload
|
||||
customRequest={handleUpload}
|
||||
listType="picture"
|
||||
maxCount={5}
|
||||
accept=".ppt,.pptx,.pdf,.mp4,.mov,.avi,.webm,.jpg,.jpeg,.png,.gif,.webp,.doc,.docx"
|
||||
beforeUpload={(file) => {
|
||||
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;
|
||||
}}
|
||||
>
|
||||
<Button icon={<CloudUploadOutlined />}>上传文件 (最大50MB)</Button>
|
||||
</Upload>
|
||||
|
||||
Reference in New Issue
Block a user