比赛
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import React from 'react'
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { App as AntdApp } from 'antd';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import Layout from './components/Layout';
|
||||
import Home from './pages/Home';
|
||||
@@ -25,9 +26,10 @@ const queryClient = new QueryClient();
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Layout>
|
||||
<AntdApp>
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/services" element={<AIServices />} />
|
||||
@@ -44,8 +46,9 @@ function App() {
|
||||
<Route path="/payment/:orderId" element={<Payment />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
</AntdApp>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,5 +94,6 @@ export const uploadProjectFile = (data) => {
|
||||
|
||||
export const createScore = (data) => api.post('/competition/scores/', data);
|
||||
export const createComment = (data) => api.post('/competition/comments/', data);
|
||||
export const getComments = (params) => api.get('/competition/comments/', { params });
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Typography, Tabs, Button, Row, Col, Card, Statistic, Tag, Descriptions, Empty, message, Spin } from 'antd';
|
||||
import { CalendarOutlined, TrophyOutlined, UserOutlined, FileTextOutlined, CloudUploadOutlined, CopyOutlined, CheckOutlined } from '@ant-design/icons';
|
||||
import { Typography, Tabs, Button, Row, Col, Card, Statistic, Tag, Descriptions, Empty, message, Spin, Modal, List, Avatar } from 'antd';
|
||||
import { CalendarOutlined, TrophyOutlined, UserOutlined, FileTextOutlined, CloudUploadOutlined, CopyOutlined, CheckOutlined, MessageOutlined } from '@ant-design/icons';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import dayjs from 'dayjs';
|
||||
import { getCompetitionDetail, getProjects, getMyCompetitionEnrollment, enrollCompetition } from '../../api';
|
||||
import { getCompetitionDetail, getProjects, getMyCompetitionEnrollment, enrollCompetition, getComments } from '../../api';
|
||||
import ProjectSubmission from './ProjectSubmission';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import 'github-markdown-css/github-markdown-dark.css';
|
||||
@@ -84,6 +84,9 @@ const CompetitionDetail = () => {
|
||||
const [activeTab, setActiveTab] = useState('details');
|
||||
const [submissionModalVisible, setSubmissionModalVisible] = useState(false);
|
||||
const [editingProject, setEditingProject] = useState(null);
|
||||
const [commentsModalVisible, setCommentsModalVisible] = useState(false);
|
||||
const [currentProjectComments, setCurrentProjectComments] = useState([]);
|
||||
const [commentsLoading, setCommentsLoading] = useState(false);
|
||||
|
||||
// Fetch competition details
|
||||
const { data: competition, isLoading: loadingDetail } = useQuery({
|
||||
@@ -94,7 +97,7 @@ const CompetitionDetail = () => {
|
||||
// Fetch projects (for leaderboard/display)
|
||||
const { data: projects } = useQuery({
|
||||
queryKey: ['projects', id],
|
||||
queryFn: () => getProjects({ competition: id, status: 'submitted' }).then(res => res.data)
|
||||
queryFn: () => getProjects({ competition: id, status: 'submitted', page_size: 100 }).then(res => res.data)
|
||||
});
|
||||
|
||||
// Check enrollment status
|
||||
@@ -128,6 +131,22 @@ const CompetitionDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewComments = async (project) => {
|
||||
if (!project) return;
|
||||
setCommentsLoading(true);
|
||||
setCommentsModalVisible(true);
|
||||
try {
|
||||
const res = await getComments({ project: project.id });
|
||||
// Support pagination result or list result
|
||||
setCurrentProjectComments(res.data?.results || res.data || []);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('获取评语失败');
|
||||
} finally {
|
||||
setCommentsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loadingDetail) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
|
||||
if (!competition) return <Empty description="比赛不存在" />;
|
||||
|
||||
@@ -232,7 +251,8 @@ const CompetitionDetail = () => {
|
||||
hoverable
|
||||
cover={<img alt={project.title} src={getImageUrl(project.display_cover_image) || 'placeholder.jpg'} style={{ height: 180, objectFit: 'cover' }} />}
|
||||
actions={[
|
||||
<Button type="link" onClick={() => navigate(`/projects/${project.id}`)}>查看详情</Button>
|
||||
<Button type="link" onClick={() => navigate(`/projects/${project.id}`)}>查看详情</Button>,
|
||||
<Button type="link" icon={<MessageOutlined />} onClick={() => handleViewComments(project)}>评语</Button>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
@@ -257,9 +277,9 @@ const CompetitionDetail = () => {
|
||||
key: 'leaderboard',
|
||||
label: '排行榜',
|
||||
children: (
|
||||
<Card title="实时排名" bordered={false} style={{ background: 'transparent' }}>
|
||||
<Card title="实时排名" variant="borderless" style={{ background: 'transparent' }}>
|
||||
{/* Leaderboard Logic: sort by final_score descending */}
|
||||
{projects?.results?.sort((a, b) => b.final_score - a.final_score).map((project, index) => (
|
||||
{[...(projects?.results || [])].sort((a, b) => b.final_score - a.final_score).map((project, index) => (
|
||||
<div key={project.id} style={{ display: 'flex', alignItems: 'center', padding: '12px 0', borderBottom: '1px solid #333' }}>
|
||||
<div style={{ width: 40, fontSize: 20, fontWeight: 'bold', color: index < 3 ? '#gold' : '#fff' }}>
|
||||
#{index + 1}
|
||||
@@ -307,16 +327,27 @@ const CompetitionDetail = () => {
|
||||
</Button>
|
||||
)}
|
||||
{isContestant && (
|
||||
<Button
|
||||
icon={<CloudUploadOutlined />}
|
||||
loading={loadingMyProject}
|
||||
onClick={() => {
|
||||
setEditingProject(myProject || null);
|
||||
setSubmissionModalVisible(true);
|
||||
}}
|
||||
>
|
||||
{myProject ? '管理/修改作品' : '提交作品'}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
icon={<CloudUploadOutlined />}
|
||||
loading={loadingMyProject}
|
||||
onClick={() => {
|
||||
setEditingProject(myProject || null);
|
||||
setSubmissionModalVisible(true);
|
||||
}}
|
||||
>
|
||||
{myProject ? '管理/修改作品' : '提交作品'}
|
||||
</Button>
|
||||
{myProject && (
|
||||
<Button
|
||||
icon={<MessageOutlined />}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => handleViewComments(myProject)}
|
||||
>
|
||||
查看评语
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,6 +378,36 @@ const CompetitionDetail = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title="评委评语"
|
||||
open={commentsModalVisible}
|
||||
onCancel={() => setCommentsModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<List
|
||||
loading={commentsLoading}
|
||||
itemLayout="horizontal"
|
||||
dataSource={currentProjectComments}
|
||||
renderItem={item => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar icon={<UserOutlined />} />}
|
||||
title={item.judge_name || '评委'}
|
||||
description={
|
||||
<div>
|
||||
<div style={{ color: 'inherit' }}>{item.content}</div>
|
||||
<div style={{ fontSize: 12, color: '#999', marginTop: 4 }}>
|
||||
{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
locale={{ emptyText: '暂无评语' }}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Button, Form, Input, Upload, message, Modal, Select } from 'antd';
|
||||
import { Card, Button, Form, Input, Upload, App, Modal, Select } from 'antd';
|
||||
import { UploadOutlined, CloudUploadOutlined, LinkOutlined } from '@ant-design/icons';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { createProject, updateProject, submitProject, uploadProjectFile } from '../../api';
|
||||
@@ -8,6 +8,7 @@ const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }) => {
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState([]);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -82,15 +83,14 @@ const ProjectSubmission = ({ competitionId, initialValues, onCancel, onSuccess }
|
||||
formData.append('file', file);
|
||||
formData.append('project', initialValues?.id || ''); // Need project ID first usually
|
||||
|
||||
// Upload logic might need adjustment: create project first, then upload files?
|
||||
// Or upload to temp storage then link?
|
||||
// For simplicity, let's assume we create project first if not exists
|
||||
if (!initialValues?.id) {
|
||||
// Already handled above
|
||||
return;
|
||||
}
|
||||
|
||||
uploadMutation.mutate(formData);
|
||||
uploadMutation.mutate(formData, {
|
||||
onSuccess: (data) => {
|
||||
onSuccess(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
onError(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user