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

This commit is contained in:
jeremygan2021
2026-03-22 22:04:13 +08:00
parent 2e05322909
commit 2104e7b7dc
8 changed files with 525 additions and 112 deletions

View File

@@ -2,7 +2,7 @@ import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api',
timeout: 8000, // 增加超时时间到 10秒
timeout: 120000, // 大文件上传需要更长超时时间 2分钟
headers: {
'Content-Type': 'application/json',
}

View File

@@ -1,19 +1,63 @@
import React, { useState } from 'react';
import { Card, Button, Form, Input, Upload, App, Modal, Select } from 'antd';
import { UploadOutlined, CloudUploadOutlined, LinkOutlined } from '@ant-design/icons';
import React, { useState, useEffect } from 'react';
import { Button, Form, Input, Upload, App, Modal, Progress, Space } from 'antd';
import { CloudUploadOutlined, LinkOutlined, FileTextOutlined, DownloadOutlined, FilePdfOutlined, FilePptOutlined, VideoCameraOutlined, PictureOutlined } from '@ant-design/icons';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createProject, updateProject, submitProject, uploadProjectFile } from '../../api';
import { createProject, updateProject, submitProject, uploadProjectFile, getProjects } from '../../api';
const { TextArea } = Input;
const { Option } = Select;
const getFileIcon = (fileType) => {
switch (fileType) {
case 'pdf':
return <FilePdfOutlined style={{ color: '#ff4d4f' }} />;
case 'ppt':
case 'pptx':
return <FilePptOutlined style={{ color: '#fa8c16' }} />;
case 'video':
return <VideoCameraOutlined style={{ color: '#722ed1' }} />;
case 'image':
return <PictureOutlined style={{ color: '#52c41a' }} />;
default:
return <FileTextOutlined style={{ color: '#1890ff' }} />;
}
};
const getFileUrl = (file) => {
return file.file_url_display || file.file_url || (file.file ? URL.createObjectURL(file.file) : null);
};
const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }) => {
const { message } = App.useApp();
const { message, modal } = App.useApp();
const [form] = Form.useForm();
const [fileList, setFileList] = useState([]);
const [uploadedFiles, setUploadedFiles] = useState([]);
const [uploadingFiles, setUploadingFiles] = useState({});
const queryClient = useQueryClient();
// Reset form when initialValues changes (important for switching between create/edit)
useEffect(() => {
if (initialValues?.id) {
getProjects({ competition: competitionId, contestant: initialValues.id })
.then(res => {
const project = res.data?.results?.[0];
if (project?.files && project.files.length > 0) {
const files = project.files.map(file => ({
uid: file.id,
id: file.id,
name: file.name || '未命名文件',
url: file.file_url_display || file.file_url,
fileType: file.file_type,
status: 'done'
}));
setUploadedFiles(files);
} else {
setUploadedFiles([]);
}
})
.catch(err => {
console.error('获取项目文件失败:', err);
});
}
}, [initialValues?.id, competitionId]);
React.useEffect(() => {
if (initialValues) {
form.setFieldsValue(initialValues);
@@ -30,7 +74,7 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
onSuccess();
},
onError: (error) => {
message.error(`创建失败: ${error.message}`);
message.error(`创建失败: ${error.response?.data?.detail || error.message}`);
}
});
@@ -42,26 +86,68 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
onSuccess();
},
onError: (error) => {
message.error(`更新失败: ${error.message}`);
message.error(`更新失败: ${error.response?.data?.detail || error.message}`);
}
});
const uploadMutation = useMutation({
mutationFn: uploadProjectFile,
onSuccess: (data) => {
message.success('文件上传成功');
setFileList([...fileList, data]); // Add file to list (assuming response format)
},
onError: (error) => {
message.error(`上传失败: ${error.message}`);
const handleUpload = ({ file, onSuccess, onError }) => {
console.log('handleUpload called', file.name);
if (!initialValues?.id) {
message.warning('请先保存项目基本信息再上传文件');
onError(new Error('请先保存项目'));
return;
}
});
const fileUid = file.uid || Date.now().toString();
console.log('fileUid:', fileUid);
setUploadingFiles(prev => ({
...prev,
[fileUid]: { percent: 0, status: 'uploading' }
}));
const formData = new FormData();
formData.append('file', file);
formData.append('project', initialValues.id);
console.log('Sending upload request for project:', initialValues.id);
uploadProjectFile(formData)
.then(res => {
console.log('Upload success:', res);
setUploadingFiles(prev => ({
...prev,
[fileUid]: { percent: 100, status: 'done' }
}));
const newFile = {
uid: res.data.id,
id: res.data.id,
name: res.data.name || file.name,
url: res.data.file_url_display || res.data.file_url,
fileType: res.data.file_type,
status: 'done'
};
setUploadedFiles(prev => [...prev, newFile]);
message.success('文件上传成功');
onSuccess(res.data);
})
.catch(err => {
console.error('Upload error:', err);
setUploadingFiles(prev => ({
...prev,
[fileUid]: { percent: 0, status: 'error' }
}));
message.error(`上传失败: ${err.response?.data?.detail || err.message}`);
onError(err);
});
};
const onFinish = (values) => {
const data = {
...values,
competition: competitionId,
// Handle file URLs/IDs if needed in create/update
};
if (initialValues?.id) {
@@ -71,28 +157,6 @@ 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
uploadMutation.mutate(formData, {
onSuccess: (data) => {
onSuccess(data);
},
onError: (error) => {
onError(error);
}
});
};
return (
<Modal
title={initialValues?.id ? "修改已提交项目" : "提交新项目"}
@@ -144,49 +208,84 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
<Input prefix={<LinkOutlined />} placeholder="https://example.com/image.jpg" />
</Form.Item>
{/* File Upload Section - Only visible if project exists */}
{initialValues?.id && (
<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>
</Form.Item>
<Form.Item label="项目附件 (PPT/PDF/视频/图片)">
<div style={{ marginBottom: 16 }}>
{uploadedFiles.length === 0 ? (
<div style={{ color: '#999', fontStyle: 'italic' }}>暂无上传文件</div>
) : (
<Space orientation="vertical" style={{ width: '100%' }} size="middle">
{uploadedFiles.map((file) => (
<div key={file.uid} style={{
display: 'flex',
alignItems: 'center',
padding: '12px 16px',
background: '#f5f5f5',
borderRadius: 8,
border: '1px solid #e8e8e8'
}}>
<span style={{ fontSize: 20, marginRight: 12 }}>
{getFileIcon(file.fileType)}
</span>
<span style={{ flex: 1, fontWeight: 500 }}>{file.name}</span>
{file.url && (
<Button
type="link"
icon={<DownloadOutlined />}
href={file.url}
target="_blank"
>
下载/查看
</Button>
)}
</div>
))}
</Space>
)}
</div>
{Object.keys(uploadingFiles).length > 0 && (
<div style={{ marginBottom: 16 }}>
<Space orientation="vertical" style={{ width: '100%' }} size="small">
{Object.entries(uploadingFiles).map(([uid, info]) => (
<Progress
key={uid}
percent={info.percent}
status={info.status === 'error' ? 'exception' : 'active'}
size="small"
/>
))}
</Space>
</div>
)}
<Upload
showUploadList={false}
maxCount={5}
accept=".ppt,.pptx,.pdf,.mp4,.mov,.avi,.webm,.jpg,.jpeg,.png,.gif,.webp,.doc,.docx"
beforeUpload={(file) => {
console.log('beforeUpload triggered for:', file.name);
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;
}
console.log('beforeUpload passed, manually calling handleUpload');
handleUpload({ file, onSuccess: () => {}, onError: () => {} });
return Upload.LIST_IGNORE;
}}
>
<Button icon={<CloudUploadOutlined />} onClick={() => console.log('Upload button clicked')}>继续上传文件 (最大50MB)</Button>
</Upload>
</Form.Item>
)}
<Form.Item>
@@ -200,7 +299,7 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
type="primary"
danger
onClick={() => {
Modal.confirm({
modal.confirm({
title: '确认提交?',
content: '提交后将无法修改,确认提交吗?',
onOk: () => submitProject(initialValues.id).then(() => {
@@ -220,4 +319,4 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
);
};
export default ProjectSubmission;
export default ProjectSubmission;