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
|
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,
|
||||||
# 评分细项(评委、嘉宾可见,选手查看自己的项目时也可见)
|
# 评分细项(评委、嘉宾可见,选手查看自己的项目时也可见)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user