2025-12-18 19:07:21 +08:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
|
import { Card, Table, Tag, Button, Space, Spin, message, Typography } from 'antd';
|
|
|
|
|
|
import { ClockCircleOutlined, BookOutlined, CalendarOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
|
import { request } from '../utils/request';
|
|
|
|
|
|
import { useUserStore } from '../stores/userStore';
|
2025-12-19 16:02:38 +08:00
|
|
|
|
import { UserLayout } from '../layouts/UserLayout';
|
2025-12-18 19:07:21 +08:00
|
|
|
|
|
|
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
interface ExamTask {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
subjectId: string;
|
|
|
|
|
|
subjectName: string;
|
|
|
|
|
|
startAt: string;
|
|
|
|
|
|
endAt: string;
|
|
|
|
|
|
totalScore: number;
|
|
|
|
|
|
timeLimitMinutes: number;
|
|
|
|
|
|
completed?: boolean;
|
|
|
|
|
|
score?: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const UserTaskPage: React.FC = () => {
|
|
|
|
|
|
const [tasks, setTasks] = useState<ExamTask[]>([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const { user } = useUserStore();
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
fetchUserTasks();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [user]);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchUserTasks = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const response = await request.get(`/api/exam-tasks/user/${user?.id}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data.success) {
|
|
|
|
|
|
setTasks(response.data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取考试任务失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const startTask = (task: ExamTask) => {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const startAt = new Date(task.startAt);
|
|
|
|
|
|
const endAt = new Date(task.endAt);
|
|
|
|
|
|
|
|
|
|
|
|
if (now < startAt) {
|
|
|
|
|
|
message.warning('考试任务尚未开始');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (now > endAt) {
|
|
|
|
|
|
message.warning('考试任务已结束');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 跳转到科目选择页面,带上任务ID
|
|
|
|
|
|
navigate('/subjects', { state: { selectedTask: task.id } });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getStatusColor = (task: ExamTask) => {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const startAt = new Date(task.startAt);
|
|
|
|
|
|
const endAt = new Date(task.endAt);
|
|
|
|
|
|
|
|
|
|
|
|
if (now < startAt) return 'blue';
|
|
|
|
|
|
if (now > endAt) return 'red';
|
2025-12-19 16:02:38 +08:00
|
|
|
|
return 'cyan'; // Using cyan to match Mars Green family better than pure green
|
2025-12-18 19:07:21 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getStatusText = (task: ExamTask) => {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const startAt = new Date(task.startAt);
|
|
|
|
|
|
const endAt = new Date(task.endAt);
|
|
|
|
|
|
|
|
|
|
|
|
if (now < startAt) return '未开始';
|
|
|
|
|
|
if (now > endAt) return '已结束';
|
|
|
|
|
|
return '进行中';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '任务名称',
|
|
|
|
|
|
dataIndex: 'name',
|
|
|
|
|
|
key: 'name',
|
|
|
|
|
|
render: (text: string) => <Text strong>{text}</Text>
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '考试科目',
|
|
|
|
|
|
dataIndex: 'subjectName',
|
|
|
|
|
|
key: 'subjectName',
|
|
|
|
|
|
render: (text: string) => (
|
|
|
|
|
|
<Space>
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<BookOutlined className="text-mars-600" />
|
2025-12-18 19:07:21 +08:00
|
|
|
|
<Text>{text}</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '总分',
|
|
|
|
|
|
dataIndex: 'totalScore',
|
|
|
|
|
|
key: 'totalScore',
|
|
|
|
|
|
render: (score: number) => <Text strong>{score}分</Text>
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '时长',
|
|
|
|
|
|
dataIndex: 'timeLimitMinutes',
|
|
|
|
|
|
key: 'timeLimitMinutes',
|
|
|
|
|
|
render: (minutes: number) => (
|
|
|
|
|
|
<Space>
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<ClockCircleOutlined className="text-gray-500" />
|
2025-12-18 19:07:21 +08:00
|
|
|
|
<Text>{minutes}分钟</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '时间范围',
|
|
|
|
|
|
key: 'timeRange',
|
|
|
|
|
|
render: (record: ExamTask) => (
|
|
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<Space>
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<CalendarOutlined className="text-gray-500" />
|
2025-12-18 19:07:21 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
{new Date(record.startAt).toLocaleDateString()}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
<Space>
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<CalendarOutlined className="text-gray-500" />
|
2025-12-18 19:07:21 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
{new Date(record.endAt).toLocaleDateString()}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
key: 'status',
|
|
|
|
|
|
render: (record: ExamTask) => (
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<Tag color={getStatusColor(record)} className="rounded-full px-3">
|
2025-12-18 19:07:21 +08:00
|
|
|
|
{getStatusText(record)}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
render: (record: ExamTask) => {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const startAt = new Date(record.startAt);
|
|
|
|
|
|
const endAt = new Date(record.endAt);
|
|
|
|
|
|
const canStart = now >= startAt && now <= endAt;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => startTask(record)}
|
|
|
|
|
|
disabled={!canStart}
|
|
|
|
|
|
icon={<CheckCircleOutlined />}
|
2025-12-19 16:02:38 +08:00
|
|
|
|
className={canStart ? "bg-mars-500 hover:bg-mars-600 border-none" : ""}
|
2025-12-18 19:07:21 +08:00
|
|
|
|
>
|
|
|
|
|
|
{canStart ? '开始考试' : '不可用'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<UserLayout>
|
|
|
|
|
|
<div className="flex justify-center items-center h-full min-h-[500px]">
|
|
|
|
|
|
<Spin size="large" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</UserLayout>
|
2025-12-18 19:07:21 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<UserLayout>
|
|
|
|
|
|
<div className="container mx-auto max-w-6xl">
|
|
|
|
|
|
<div className="mb-8 text-center">
|
|
|
|
|
|
<Title level={2} className="!text-mars-600 mb-2">
|
|
|
|
|
|
我的考试任务
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<Text type="secondary" className="block text-lg">
|
|
|
|
|
|
查看您被分派的所有考试任务
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
2025-12-18 19:07:21 +08:00
|
|
|
|
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<Card className="shadow-md border-t-4 border-t-mars-500 rounded-xl">
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={tasks}
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showTotal: (total) => `共 ${total} 条记录`
|
|
|
|
|
|
}}
|
|
|
|
|
|
locale={{
|
|
|
|
|
|
emptyText: '暂无考试任务'
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
2025-12-18 19:07:21 +08:00
|
|
|
|
|
2025-12-19 16:02:38 +08:00
|
|
|
|
<div className="mt-8 text-center">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="default"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
onClick={() => navigate('/subjects')}
|
|
|
|
|
|
icon={<BookOutlined />}
|
|
|
|
|
|
className="px-8 h-12 hover:border-mars-500 hover:text-mars-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
返回科目选择
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-12-18 19:07:21 +08:00
|
|
|
|
</div>
|
2025-12-19 16:02:38 +08:00
|
|
|
|
</UserLayout>
|
2025-12-18 19:07:21 +08:00
|
|
|
|
);
|
2025-12-19 16:02:38 +08:00
|
|
|
|
};
|