This commit is contained in:
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user