feat: 更新考试相关页面,添加刷新功能,修改考试状态显示

- 注:当前代码还存在bug:查看结果暂出现错误。
- 在 SubjectSelectionPage 页面中添加刷新按钮,允许用户手动刷新考试任务列表。
- 修改 UserTaskPage 页面,重构考试任务为答题记录,更新数据结构和状态显示。
- 在 AdminDashboardPage、UserManagePage、UserRecordsPage 等管理页面中添加考试状态显示,使用不同颜色区分状态(不及格、合格、优秀)。
- 在 ResultPage 中显示考试状态,确保用户能够清晰了解考试结果。
- 添加约束,确保单次考试试卷中题目不可重复出现,并记录相关规范。
- 添加评分状态约束,根据得分占比自动计算考试状态,并在结果页面显示。
This commit is contained in:
2025-12-29 20:28:33 +08:00
parent 03eb858749
commit 57101fac37
26 changed files with 480 additions and 216 deletions

View File

@@ -17,7 +17,6 @@ import {
} from '@ant-design/icons';
import { useAdmin } from '../contexts';
import LOGO from '../assets/主要LOGO.svg';
import LOGO from '../assets/纯字母LOGO.svg';
import LOGO from '../assets/正方形LOGO.svg';
const { Header, Sider, Content, Footer } = Layout;
@@ -144,13 +143,10 @@ const AdminLayout = ({ children }: { children: React.ReactNode }) => {
{children}
</Content>
<Footer className="bg-white text-center py-3 px-8 text-gray-400 text-xs flex flex-col md:flex-row justify-between items-center fixed bottom-0 left-0 right-0 shadow-sm" style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.3s' }}>
<div>
<Footer className="bg-white text-center py-1.5 px-2 text-gray-400 text-xs flex flex-col md:flex-row justify-center items-center fixed bottom-0 left-0 right-0 shadow-sm" style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.3s' }}>
<div className="whitespace-nowrap">
&copy; {new Date().getFullYear()} Boonlive OA System. All Rights Reserved.
</div>
<div className="mt-2 md:mt-0">
<img src={LOGO} alt="纯字母LOGO" style={{ width: '100px', height: '38px' }} />
</div>
</Footer>
</Layout>
</Layout>

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Layout } from 'antd';
import LOGO from '../assets/主要LOGO.svg';
import LOGO from '../assets/纯字母LOGO.svg';
const { Header, Content, Footer } = Layout;
@@ -18,11 +17,10 @@ export const UserLayout: React.FC<{ children: React.ReactNode }> = ({ children }
</div>
</Content>
<Footer className="bg-white border-t border-gray-100 py-6 px-8 flex flex-col md:flex-row justify-between items-center text-gray-400 text-sm">
<div className="mb-4 md:mb-0">
<Footer className="bg-white border-t border-gray-100 py-3 px-2 flex flex-col md:flex-row justify-center items-center text-gray-400 text-sm">
<div className="md:mb-0 whitespace-nowrap">
&copy; {new Date().getFullYear()} Boonlive OA System. All Rights Reserved.
</div>
<img src={LOGO} alt="纯字母LOGO" style={{ width: '136px', height: '51px' }} />
</Footer>
</Layout>
);

View File

@@ -50,9 +50,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
headerBg: '#ffffff',
siderBg: '#ffffff',
},
Pagination: {
size: 'small',
},
}
}}
>

View File

@@ -5,7 +5,6 @@ import { useUser } from '../contexts';
import { userAPI } from '../services/api';
import { validateUserForm } from '../utils/validation';
import LOGO from '../assets/主要LOGO.svg';
import LOGO from '../assets/纯字母LOGO.svg';
const { Header, Content, Footer } = Layout;
const { Title } = Typography;
@@ -218,11 +217,10 @@ const HomePage = () => {
</div>
</Content>
<Footer className="bg-white border-t border-gray-100 py-3 px-4 flex flex-col md:flex-row justify-between items-center text-gray-400 text-xs">
<div className="mb-2 md:mb-0">
<Footer className="bg-white border-t border-gray-100 py-1.5 px-2 flex flex-col md:flex-row justify-center items-center text-gray-400 text-xs">
<div className="mb-2 md:mb-0 whitespace-nowrap">
&copy; {new Date().getFullYear()} Boonlive OA System. All Rights Reserved.
</div>
<img src={LOGO} alt="纯字母LOGO" style={{ width: '100px', height: '38px' }} />
</Footer>
</Layout>
);

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Card, Result, Button, Descriptions, message } from 'antd';
import { Card, Button, Descriptions, message } from 'antd';
import { useParams, useNavigate } from 'react-router-dom';
import { useUser } from '../contexts';
import { quizAPI } from '../services/api';
@@ -14,6 +14,8 @@ interface QuizRecord {
totalScore: number;
correctCount: number;
totalCount: number;
scorePercentage: number;
status: '不及格' | '合格' | '优秀';
createdAt: string;
}
@@ -52,8 +54,8 @@ const ResultPage = () => {
try {
setLoading(true);
const response = await quizAPI.getRecordDetail(recordId!);
setRecord(response.data.record);
setAnswers(response.data.answers);
setRecord(response.record);
setAnswers(response.answers);
} catch (error: any) {
message.error(error.message || '获取答题结果失败');
navigate('/');
@@ -63,7 +65,7 @@ const ResultPage = () => {
};
const handleBackToHome = () => {
navigate('/');
navigate('/tasks');
};
const getTagColor = (type: string) => {
@@ -181,35 +183,69 @@ const ResultPage = () => {
}
const correctRate = ((record.correctCount / record.totalCount) * 100).toFixed(1);
const status = record.totalScore >= record.totalCount * 0.6 ? 'success' : 'warning';
const getStatusColor = (status: string) => {
switch (status) {
case '不及格':
return 'bg-red-100 text-red-800 border-red-200';
case '合格':
return 'bg-blue-100 text-blue-800 border-blue-200';
case '优秀':
return 'bg-green-100 text-green-800 border-green-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
return (
<UserLayout>
<div className="max-w-md mx-auto px-4">
{/* 结果概览 */}
<Card className="shadow-lg mb-6 rounded-xl border-t-4 border-t-mars-500">
<Result
status={status as any}
title={`答题完成!您的得分是 ${record.totalScore}`}
subTitle={`正确率 ${correctRate}% (${record.correctCount}/${record.totalCount})`}
extra={[
<Button key="back" type="primary" onClick={handleBackToHome} className="bg-mars-500 hover:bg-mars-600 border-none px-6 h-9 text-sm">
<Card className="shadow-lg mb-6 rounded-xl border-t-4 border-t-mars-500" bodyStyle={{ padding: '12px' }}>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 mt-0.5">
{record.status === '优秀' ? (
<svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="32" height="32" fill="currentColor" aria-hidden="true" className="text-green-500">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
</svg>
) : record.status === '合格' ? (
<svg viewBox="64 64 896 896" focusable="false" data-icon="check-circle" width="32" height="32" fill="currentColor" aria-hidden="true" className="text-blue-500">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
</svg>
) : (
<svg viewBox="64 64 896 896" focusable="false" data-icon="close-circle" width="32" height="32" fill="currentColor" aria-hidden="true" className="text-red-500">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
</svg>
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-gray-800 mb-0.5">
{record.totalScore}
</div>
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-0.5 rounded text-xs font-medium border ${getStatusColor(record.status)}`}>
{record.status}
</span>
<span className="text-xs text-gray-600">
{correctRate}% ({record.correctCount}/{record.totalCount})
</span>
</div>
<Button type="primary" onClick={handleBackToHome} className="bg-mars-500 hover:bg-mars-600 border-none px-4 h-7 text-xs">
</Button>
]}
/>
</div>
</div>
</Card>
{/* 基本信息 */}
<Card className="shadow-lg mb-6 rounded-xl">
<h3 className="text-base font-semibold mb-3 text-gray-800 border-l-4 border-mars-500 pl-3"></h3>
<Descriptions bordered column={1} size="small">
<Item label="姓名">{user?.name}</Item>
<Item label="手机号">{user?.phone}</Item>
<Card className="shadow-lg mb-6 rounded-xl" bodyStyle={{ padding: '12px' }}>
<h3 className="text-sm font-semibold mb-2 text-gray-800 border-l-4 border-mars-500 pl-3"></h3>
<Descriptions bordered column={1} size="small" className="text-xs">
<Item label="答题时间">{formatDateTime(record.createdAt)}</Item>
<Item label="总题数">{record.totalCount} </Item>
<Item label="正确数">{record.correctCount} </Item>
<Item label="总得分">{record.totalScore} </Item>
<Item label="得分占比">{record.scorePercentage.toFixed(1)}%</Item>
<Item label="考试状态">{record.status}</Item>
</Descriptions>
</Card>

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Button, Typography, Tag, Space, Spin, message, Modal } from 'antd';
import { ClockCircleOutlined, BookOutlined, RightOutlined } from '@ant-design/icons';
import { ClockCircleOutlined, BookOutlined, RightOutlined, ReloadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useUser } from '../contexts';
import { examSubjectAPI, examTaskAPI, quizAPI } from '../services/api';
@@ -164,6 +164,15 @@ export const SubjectSelectionPage: React.FC = () => {
<div className="flex items-center mb-3 border-b border-gray-200 pb-1">
<BookOutlined className="text-lg mr-2 text-mars-400" />
<Title level={4} className="!mb-0 !text-gray-700 !text-base"></Title>
<Button
type="default"
size="small"
icon={<ReloadOutlined />}
className="ml-auto"
onClick={fetchData}
>
</Button>
</div>
<div className="space-y-4">
@@ -187,15 +196,18 @@ export const SubjectSelectionPage: React.FC = () => {
className="h-auto py-3 px-4 text-left border-l-4 border-l-green-500 hover:border-l-green-600 hover:shadow-md transition-all duration-300"
onClick={() => startQuiz(task.id)}
>
<div className="flex justify-between items-start w-full">
<div className="flex justify-between items-center w-full">
<div className="flex-1">
<div className="flex justify-between items-center mb-1">
<div className="flex items-center mb-1">
<Title level={5} className="mb-0 text-gray-800 !text-sm">
{task.name}
</Title>
<div className="text-green-600">
<Text className="text-xs"></Text>
</div>
{subject && (
<div className="flex items-center ml-auto">
<span className="mr-1 text-gray-400 text-xs">:</span>
<Text className="text-gray-600 text-xs">{subject.timeLimitMinutes}</Text>
</div>
)}
</div>
<div className="flex justify-between items-center mb-2">
<div className="flex items-center">
@@ -218,12 +230,9 @@ export const SubjectSelectionPage: React.FC = () => {
{new Date(task.startAt).toLocaleDateString()} - {new Date(task.endAt).toLocaleDateString()}
</Text>
</div>
{subject && (
<div className="flex items-center">
<span className="mr-1 text-gray-400 text-xs">:</span>
<Text className="text-gray-600 text-xs">{subject.timeLimitMinutes}</Text>
</div>
)}
<div className="text-green-600">
<Text className="text-xs px-2 py-1 rounded border border-green-200 bg-green-50"></Text>
</div>
</div>
</div>
</div>
@@ -260,15 +269,16 @@ export const SubjectSelectionPage: React.FC = () => {
className={`h-auto py-3 px-4 text-left border-l-4 border-l-gray-400 hover:border-l-gray-500 hover:shadow-md transition-all duration-300 ${attemptsExhausted ? 'opacity-50 cursor-not-allowed' : ''}`}
onClick={() => !attemptsExhausted && startQuiz(task.id)}
>
<div className="flex justify-between items-start w-full">
<div className="flex justify-between items-center w-full">
<div className="flex-1">
<div className="flex justify-between items-center mb-1">
<Title level={5} className="mb-0 text-gray-800 !text-sm">
{task.name}
</Title>
{!attemptsExhausted && (
<div className="text-gray-400">
<Text className="text-xs"></Text>
{subject && (
<div className="flex items-center ml-auto">
<span className="mr-1 text-gray-400 text-xs">:</span>
<Text className="text-gray-600 text-xs">{subject.timeLimitMinutes}</Text>
</div>
)}
</div>
@@ -294,10 +304,9 @@ export const SubjectSelectionPage: React.FC = () => {
{new Date(task.startAt).toLocaleDateString()} - {new Date(task.endAt).toLocaleDateString()}
</Text>
</div>
{subject && (
<div className="flex items-center">
<span className="mr-1 text-gray-400 text-xs">:</span>
<Text className="text-gray-600 text-xs">{subject.timeLimitMinutes}</Text>
{!attemptsExhausted && (
<div className="text-gray-400">
<Text className="text-xs px-2 py-1 rounded border border-gray-200 bg-gray-50"></Text>
</div>
)}
</div>
@@ -334,11 +343,19 @@ export const SubjectSelectionPage: React.FC = () => {
disabled
className="h-auto py-3 px-4 text-left border-l-4 border-l-blue-400 opacity-75 cursor-not-allowed"
>
<div className="flex justify-between items-start w-full">
<div className="flex justify-between items-center w-full">
<div className="flex-1">
<Title level={5} className="mb-1 text-gray-800 !text-sm">
{task.name}
</Title>
<div className="flex justify-between items-center mb-1">
<Title level={5} className="mb-0 text-gray-800 !text-sm">
{task.name}
</Title>
{subject && (
<div className="flex items-center ml-auto">
<span className="mr-1 text-gray-400 text-xs">:</span>
<Text className="text-gray-600 text-xs">{subject.timeLimitMinutes}</Text>
</div>
)}
</div>
<div className="flex justify-between items-center mb-2">
<div className="flex items-center">
<BookOutlined className="mr-1 text-gray-400 text-xs" />
@@ -360,12 +377,9 @@ export const SubjectSelectionPage: React.FC = () => {
{new Date(task.startAt).toLocaleDateString()} - {new Date(task.endAt).toLocaleDateString()}
</Text>
</div>
{subject && (
<div className="flex items-center">
<span className="mr-1 text-gray-400 text-xs">:</span>
<Text className="text-gray-600 text-xs">{subject.timeLimitMinutes}</Text>
</div>
)}
<div className="text-blue-600">
<Text className="text-xs px-2 py-1 rounded border border-blue-200 bg-blue-50"></Text>
</div>
</div>
</div>
</div>
@@ -396,7 +410,7 @@ export const SubjectSelectionPage: React.FC = () => {
className="px-4 h-10 text-sm hover:border-mars-500 hover:text-mars-500"
onClick={() => navigate('/tasks')}
>
</Button>
</div>
</div>

View File

@@ -1,98 +1,93 @@
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 { EyeOutlined, BookOutlined, CalendarOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useUser } from '../contexts';
import { examTaskAPI } from '../services/api';
import { quizAPI } from '../services/api';
import { UserLayout } from '../layouts/UserLayout';
const { Title, Text } = Typography;
interface ExamTask {
interface QuizRecord {
id: string;
name: string;
subjectId: string;
subjectName: string;
startAt: string;
endAt: string;
userId: string;
subjectId?: string;
taskId?: string;
subjectName?: string;
taskName?: string;
totalScore: number;
timeLimitMinutes: number;
usedAttempts: number;
maxAttempts: number;
bestScore: number;
correctCount: number;
totalCount: number;
scorePercentage: number;
status: '不及格' | '合格' | '优秀';
createdAt: string;
}
export const UserTaskPage: React.FC = () => {
const [tasks, setTasks] = useState<ExamTask[]>([]);
const [records, setRecords] = useState<QuizRecord[]>([]);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const { user } = useUser();
useEffect(() => {
if (user) {
fetchUserTasks();
fetchUserRecords();
}
}, [user]);
const fetchUserTasks = async () => {
const fetchUserRecords = async () => {
try {
setLoading(true);
if (!user?.id) return;
const response = await examTaskAPI.getUserTasks(user.id) as any;
setTasks(response.data);
const response = await quizAPI.getUserRecords(user.id) as any;
setRecords(response.data);
} catch (error) {
message.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 viewResult = (recordId: string) => {
navigate(`/result/${recordId}`);
};
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';
return 'cyan'; // Using cyan to match Mars Green family better than pure green
};
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 getStatusColor = (status: string) => {
switch (status) {
case '不及格':
return 'red';
case '合格':
return 'blue';
case '优秀':
return 'green';
default:
return 'default';
}
};
const columns = [
{
title: '操作',
key: 'action',
width: 80,
render: (record: QuizRecord) => (
<Button
type="link"
size="small"
icon={<EyeOutlined />}
onClick={() => viewResult(record.id)}
style={{ fontSize: '12px', padding: 0 }}
>
</Button>
)
},
{
title: '任务名称',
dataIndex: 'name',
key: 'name',
dataIndex: 'taskName',
key: 'taskName',
width: 100,
render: (text: string) => <Text strong style={{ fontSize: '12px' }}>{text}</Text>
render: (text: string) => <Text strong style={{ fontSize: '12px' }}>{text || '-'}</Text>
},
{
title: '考试科目',
@@ -102,76 +97,51 @@ export const UserTaskPage: React.FC = () => {
render: (text: string) => (
<Space size={4}>
<BookOutlined style={{ fontSize: '12px' }} className="text-mars-600" />
<Text style={{ fontSize: '12px' }}>{text}</Text>
<Text style={{ fontSize: '12px' }}>{text || '-'}</Text>
</Space>
)
},
{
title: '分',
title: '分',
dataIndex: 'totalScore',
key: 'totalScore',
width: 60,
render: (score: number) => <Text strong style={{ fontSize: '12px' }}>{score}</Text>
},
{
title: '时长',
dataIndex: 'timeLimitMinutes',
key: 'timeLimitMinutes',
width: 70,
render: (minutes: number) => (
<Space size={4}>
<ClockCircleOutlined style={{ fontSize: '12px' }} className="text-gray-500" />
<Text style={{ fontSize: '12px' }}>{minutes}</Text>
</Space>
)
},
{
title: '时间范围',
key: 'timeRange',
width: 110,
render: (record: ExamTask) => (
<Space direction="vertical" size={0}>
<Space size={4}>
<CalendarOutlined style={{ fontSize: '11px' }} className="text-gray-500" />
<Text type="secondary" style={{ fontSize: '11px' }}>
{new Date(record.startAt).toLocaleDateString()}
</Text>
</Space>
<Space size={4}>
<CalendarOutlined style={{ fontSize: '11px' }} className="text-gray-500" />
<Text type="secondary" style={{ fontSize: '11px' }}>
{new Date(record.endAt).toLocaleDateString()}
</Text>
</Space>
</Space>
)
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 70,
render: (record: ExamTask) => (
<Tag color={getStatusColor(record)} className="rounded-full px-2" style={{ fontSize: '11px' }}>
{getStatusText(record)}
width: 60,
render: (status: string) => (
<Tag color={getStatusColor(status)} style={{ fontSize: '11px' }}>
{status}
</Tag>
)
},
{
title: '数',
key: 'attempts',
width: 50,
render: (record: ExamTask) => (
title: '正确题数',
key: 'correctCount',
width: 80,
render: (record: QuizRecord) => (
<Text style={{ fontSize: '12px' }}>
{record.usedAttempts}/{record.maxAttempts}
{record.correctCount}/{record.totalCount}
</Text>
),
},
{
title: '最高分',
dataIndex: 'bestScore',
key: 'bestScore',
width: 60,
render: (score: number) => <Text strong style={{ fontSize: '12px' }}>{score}</Text>,
title: '答题时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 110,
render: (date: string) => (
<Space size={4}>
<CalendarOutlined style={{ fontSize: '11px' }} className="text-gray-500" />
<Text type="secondary" style={{ fontSize: '11px' }}>
{new Date(date).toLocaleString('zh-CN')}
</Text>
</Space>
)
}
];
@@ -191,7 +161,7 @@ export const UserTaskPage: React.FC = () => {
<Table
columns={columns}
dataSource={tasks}
dataSource={records}
rowKey="id"
size="small"
pagination={{
@@ -201,9 +171,8 @@ export const UserTaskPage: React.FC = () => {
size: 'small'
}}
locale={{
emptyText: '暂无考试任务'
emptyText: '暂无答题记录'
}}
size="small"
scroll={{ x: 'max-content' }}
className="mobile-table"
style={{

View File

@@ -39,6 +39,8 @@ interface RecentRecord {
totalScore: number;
correctCount: number;
totalCount: number;
scorePercentage: number;
status: '不及格' | '合格' | '优秀';
createdAt: string;
subjectName?: string;
examCount?: number;
@@ -161,6 +163,19 @@ const AdminDashboardPage = () => {
Number(overview.taskStatusDistribution.notStarted || 0)
: 0;
const getStatusColor = (status: string) => {
switch (status) {
case '不及格':
return 'red';
case '合格':
return 'blue';
case '优秀':
return 'green';
default:
return 'default';
}
};
const columns = [
{
title: '姓名',
@@ -178,6 +193,15 @@ const AdminDashboardPage = () => {
key: 'totalScore',
render: (score: number) => <span className="font-semibold text-mars-600">{score} </span>,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const color = getStatusColor(status);
return <span className={`px-2 py-1 rounded text-xs font-medium bg-${color}-100 text-${color}-800`}>{status}</span>;
},
},
{
title: '正确率',
key: 'correctRate',

View File

@@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom';
import { useAdmin } from '../../contexts';
import { adminAPI } from '../../services/api';
import LOGO from '../../assets/主要LOGO.svg';
import LOGO from '../../assets/纯字母LOGO.svg';
const { Header, Content, Footer } = Layout;
@@ -197,11 +196,10 @@ const AdminLoginPage = () => {
</div>
</Content>
<Footer className="bg-white border-t border-gray-100 py-3 px-4 flex flex-col md:flex-row justify-between items-center text-gray-400 text-xs">
<div className="mb-2 md:mb-0">
<Footer className="bg-white border-t border-gray-100 py-1.5 px-2 flex flex-col md:flex-row justify-center items-center text-gray-400 text-xs">
<div className="mb-2 md:mb-0 whitespace-nowrap">
&copy; {new Date().getFullYear()} Boonlive OA System. All Rights Reserved.
</div>
<img src={LOGO} alt="纯字母LOGO" style={{ width: '100px', height: '38px' }} />
</Footer>
</Layout>
);

View File

@@ -234,6 +234,7 @@ const QuestionTextImportPage = () => {
setTablePagination({
current: pagination.current ?? 1,
pageSize: pagination.pageSize ?? 10,
size: 'small' as const,
})
}
/>

View File

@@ -23,10 +23,27 @@ interface Record {
totalScore: number;
correctCount: number;
totalCount: number;
scorePercentage: number;
status: '不及格' | '合格' | '优秀';
createdAt: string;
answers: Answer[];
}
const COLORS = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'];
const getStatusColor = (status: string) => {
switch (status) {
case '不及格':
return 'red';
case '合格':
return 'blue';
case '优秀':
return 'green';
default:
return 'default';
}
};
const RecordDetailPage = ({ recordId }: { recordId: string }) => {
const [record, setRecord] = useState<Record | null>(null);
const [loading, setLoading] = useState(false);
@@ -142,22 +159,27 @@ const RecordDetailPage = ({ recordId }: { recordId: string }) => {
</div>
<Row gutter={16} className="mb-6">
<Col span={6}>
<Col span={5}>
<Card>
<Statistic title="考试状态" value={record.status} valueStyle={{ color: getStatusColor(record.status) === 'red' ? '#f5222d' : getStatusColor(record.status) === 'green' ? '#52c41a' : '#1890ff' }} />
</Card>
</Col>
<Col span={5}>
<Card>
<Statistic title="总得分" value={record.totalScore} suffix="分" />
</Card>
</Col>
<Col span={6}>
<Col span={5}>
<Card>
<Statistic title="正确题数" value={record.correctCount} />
</Card>
</Col>
<Col span={6}>
<Col span={5}>
<Card>
<Statistic title="总题数" value={record.totalCount} />
</Card>
</Col>
<Col span={6}>
<Col span={4}>
<Card>
<Statistic
title="正确率"

View File

@@ -24,6 +24,8 @@ interface QuizRecord {
totalScore: number;
correctCount: number;
totalCount: number;
scorePercentage: number;
status: '不及格' | '合格' | '优秀';
createdAt: string;
}
@@ -54,6 +56,19 @@ interface TaskStats {
const COLORS = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'];
const getStatusColor = (status: string) => {
switch (status) {
case '不及格':
return 'red';
case '合格':
return 'blue';
case '优秀':
return 'green';
default:
return 'default';
}
};
const StatisticsPage = () => {
const [statistics, setStatistics] = useState<Statistics | null>(null);
const [records, setRecords] = useState<QuizRecord[]>([]);
@@ -181,11 +196,9 @@ const StatisticsPage = () => {
};
const scoreDistribution = [
{ range: '0-59分', count: records.filter(r => r.totalScore < 60).length },
{ range: '60-69分', count: records.filter(r => r.totalScore >= 60 && r.totalScore < 70).length },
{ range: '70-79分', count: records.filter(r => r.totalScore >= 70 && r.totalScore < 80).length },
{ range: '80-89分', count: records.filter(r => r.totalScore >= 80 && r.totalScore < 90).length },
{ range: '90-100分', count: records.filter(r => r.totalScore >= 90).length },
{ range: '不及格', count: records.filter(r => r.status === '不及格').length },
{ range: '合格', count: records.filter(r => r.status === '合格').length },
{ range: '优秀', count: records.filter(r => r.status === '优秀').length },
].filter(item => item.count > 0);
const overviewColumns = [
@@ -193,6 +206,15 @@ const StatisticsPage = () => {
{ title: '手机号', dataIndex: 'userPhone', key: 'userPhone' },
{ title: '科目', dataIndex: 'subjectName', key: 'subjectName' },
{ title: '任务', dataIndex: 'taskName', key: 'taskName' },
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const color = getStatusColor(status);
return <span className={`px-2 py-1 rounded text-xs font-medium bg-${color}-100 text-${color}-800`}>{status}</span>;
},
},
{ title: '得分', dataIndex: 'totalScore', key: 'totalScore', render: (score: number) => <span className="font-semibold text-blue-600">{score} </span> },
{ title: '正确率', key: 'correctRate', render: (record: QuizRecord) => {
const rate = ((record.correctCount / record.totalCount) * 100).toFixed(1);

View File

@@ -22,6 +22,8 @@ interface QuizRecord {
userName: string;
totalScore: number;
obtainedScore: number;
scorePercentage: number;
status: '不及格' | '合格' | '优秀';
createdAt: string;
subjectName?: string;
taskName?: string;
@@ -33,6 +35,19 @@ interface UserGroup {
isSystem: boolean;
}
const getStatusColor = (status: string) => {
switch (status) {
case '不及格':
return 'red';
case '合格':
return 'blue';
case '优秀':
return 'green';
default:
return 'default';
}
};
const UserManagePage = () => {
const [users, setUsers] = useState<User[]>([]);
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
@@ -81,6 +96,7 @@ const UserManagePage = () => {
current: page,
pageSize,
total: (res as any).pagination?.total || res.data.length,
size: 'small' as const,
});
} catch (error) {
message.error('获取用户列表失败');
@@ -246,6 +262,7 @@ const UserManagePage = () => {
current: page,
pageSize,
total: (res as any).pagination?.total || res.data.length,
size: 'small' as const,
});
} catch (error) {
message.error('获取答题记录失败');
@@ -467,12 +484,20 @@ const UserManagePage = () => {
key: 'totalScore',
render: (score: number) => score || 0,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const color = getStatusColor(status);
return <span className={`px-2 py-1 rounded text-xs font-medium bg-${color}-100 text-${color}-800`}>{status}</span>;
},
},
{
title: '得分',
dataIndex: 'obtainedScore',
key: 'obtainedScore',
render: (score: number, record: QuizRecord) => {
// 确保score有默认值
const actualScore = score || 0;
const totalScore = record.totalScore || 0;
return (

View File

@@ -12,9 +12,26 @@ interface Record {
totalScore: number;
correctCount: number;
totalCount: number;
scorePercentage: number;
status: '不及格' | '合格' | '优秀';
createdAt: string;
}
const COLORS = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'];
const getStatusColor = (status: string) => {
switch (status) {
case '不及格':
return 'red';
case '合格':
return 'blue';
case '优秀':
return 'green';
default:
return 'default';
}
};
const UserRecordsPage = ({ userId }: { userId: string }) => {
const [records, setRecords] = useState<Record[]>([]);
const [loading, setLoading] = useState(false);
@@ -34,6 +51,7 @@ const UserRecordsPage = ({ userId }: { userId: string }) => {
current: page,
pageSize,
total: res.data.pagination.total,
size: 'small' as const,
});
} catch (error) {
message.error('获取答题记录失败');
@@ -61,6 +79,16 @@ const UserRecordsPage = ({ userId }: { userId: string }) => {
key: 'createdAt',
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<span className={`px-2 py-1 rounded text-xs font-medium bg-${getStatusColor(status)}-100 text-${getStatusColor(status)}-800`}>
{status}
</span>
),
},
{
title: '总得分',
dataIndex: 'totalScore',

View File

@@ -65,7 +65,7 @@ export const OptionList = ({ type, options, value, onChange, disabled }: OptionL
key={opt.key}
onClick={() => handleSelect(opt.value)}
className={`
relative flex items-start p-1.5 rounded-xl border-2 transition-all duration-200 active:scale-[0.99] cursor-pointer
relative flex items-center p-1.5 rounded-xl border-2 transition-all duration-200 active:scale-[0.99] cursor-pointer
${selected
? 'border-[#00897B] bg-[#E0F2F1]'
: 'border-transparent bg-white shadow-sm hover:border-gray-200'}
@@ -73,7 +73,7 @@ export const OptionList = ({ type, options, value, onChange, disabled }: OptionL
>
{/* 选项圆圈 */}
<div className={`
flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium border mr-1.5 mt-0.5 transition-colors
flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium border mr-1.5 transition-colors
${selected
? 'bg-[#00897B] border-[#00897B] text-white'
: 'bg-white border-gray-300 text-gray-500'}