pdf upload
All checks were successful
Deploy to Server / deploy (push) Successful in 32s

This commit is contained in:
jeremygan2021
2026-03-22 21:15:34 +08:00
parent 0274e59fd9
commit 2e05322909
5 changed files with 130 additions and 5 deletions

View File

@@ -435,6 +435,19 @@ def project_detail_api(request, project_id):
# 判断是否为选手查看自己的项目 # 判断是否为选手查看自己的项目
is_own_project = role == 'contestant' and project.contestant.user == user 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 = { data = {
'id': project.id, 'id': project.id,
'title': project.title, 'title': project.title,
@@ -445,6 +458,7 @@ def project_detail_api(request, project_id):
'current_comment': current_comment, 'current_comment': current_comment,
'ai_result': ai_data, 'ai_result': ai_data,
'audio_url': audio_url, 'audio_url': audio_url,
'files': files_data,
'can_grade': role == 'judge' or (role == 'contestant' and project.contestant.user != user), 'can_grade': role == 'judge' or (role == 'contestant' and project.contestant.user != user),
'is_own_project': is_own_project, 'is_own_project': is_own_project,
# 评分细项(评委、嘉宾可见,选手查看自己的项目时也可见) # 评分细项(评委、嘉宾可见,选手查看自己的项目时也可见)

View File

@@ -33,19 +33,44 @@ class CompetitionEnrollmentSerializer(serializers.ModelSerializer):
read_only_fields = ['status'] read_only_fields = ['status']
class ProjectFileSerializer(serializers.ModelSerializer): class ProjectFileSerializer(serializers.ModelSerializer):
file_url_display = serializers.SerializerMethodField()
class Meta: class Meta:
model = ProjectFile 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): def validate_file(self, value):
if not value: if not value:
return value return value
# 50MB limit
limit_mb = 50 limit_mb = 50
if value.size > limit_mb * 1024 * 1024: if value.size > limit_mb * 1024 * 1024:
raise serializers.ValidationError(f"文件大小不能超过 {limit_mb}MB") raise serializers.ValidationError(f"文件大小不能超过 {limit_mb}MB")
return value 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): class ProjectSerializer(serializers.ModelSerializer):
files = ProjectFileSerializer(many=True, read_only=True) files = ProjectFileSerializer(many=True, read_only=True)
contestant_info = serializers.SerializerMethodField() contestant_info = serializers.SerializerMethodField()

View File

@@ -116,6 +116,13 @@
</div> </div>
</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 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"> <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> <i class="fas fa-robot text-indigo-600 mr-2"></i>
@@ -548,6 +555,39 @@ async function viewProject(id) {
audioSection.classList.add('justify-center'); 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 // AI Result
const aiSection = document.getElementById('aiResultSection'); const aiSection = document.getElementById('aiResultSection');
if (data.ai_result) { if (data.ai_result) {

View File

@@ -10,6 +10,7 @@ from .serializers import (
ProjectSerializer, ProjectFileSerializer, ProjectSerializer, ProjectFileSerializer,
ScoreSerializer, CommentSerializer, ScoreDimensionSerializer ScoreSerializer, CommentSerializer, ScoreDimensionSerializer
) )
import uuid
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
@@ -200,13 +201,26 @@ class ProjectFileViewSet(viewsets.ModelViewSet):
return ProjectFile.objects.all() return ProjectFile.objects.all()
def perform_create(self, serializer): def perform_create(self, serializer):
# 简单权限控制:只有项目拥有者可以上传
project = serializer.validated_data['project'] project = serializer.validated_data['project']
user = get_current_wechat_user(self.request) user = get_current_wechat_user(self.request)
if not user or project.contestant.user != user: if not user or project.contestant.user != user:
raise serializers.ValidationError("无权上传文件") 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() serializer.save()

View File

@@ -146,11 +146,43 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
{/* File Upload Section - Only visible if project exists */} {/* File Upload Section - Only visible if project exists */}
{initialValues?.id && ( {initialValues?.id && (
<Form.Item label="项目附件 (PPT/PDF/视频)"> <Form.Item label="项目附件 (PPT/PDF/视频/图片)">
<Upload <Upload
customRequest={handleUpload} customRequest={handleUpload}
listType="picture" listType="picture"
maxCount={5} 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> <Button icon={<CloudUploadOutlined />}>上传文件 (最大50MB)</Button>
</Upload> </Upload>